init
This commit is contained in:
17
.codeclimate.yml
Normal file
17
.codeclimate.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
engines:
|
||||||
|
eslint:
|
||||||
|
enabled: true
|
||||||
|
duplication:
|
||||||
|
enabled: true
|
||||||
|
config:
|
||||||
|
languages:
|
||||||
|
- javascript:
|
||||||
|
fixme:
|
||||||
|
enabled: true
|
||||||
|
ratings:
|
||||||
|
paths:
|
||||||
|
- context/**
|
||||||
|
- lib/**
|
||||||
|
- "**.js"
|
||||||
|
exclude_paths:
|
||||||
|
- "**/test/**/*"
|
||||||
20
.eslintrc.yml
Normal file
20
.eslintrc.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
env:
|
||||||
|
es6: true
|
||||||
|
node: true
|
||||||
|
mocha: true
|
||||||
|
extends: 'eslint:recommended'
|
||||||
|
rules:
|
||||||
|
indent:
|
||||||
|
- error
|
||||||
|
- 2
|
||||||
|
linebreak-style:
|
||||||
|
- error
|
||||||
|
- unix
|
||||||
|
quotes:
|
||||||
|
- error
|
||||||
|
- single
|
||||||
|
semi:
|
||||||
|
- error
|
||||||
|
- never
|
||||||
|
no-console:
|
||||||
|
- 0
|
||||||
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# application config file
|
||||||
|
context/config.js
|
||||||
|
|
||||||
|
# generated data json file
|
||||||
|
data.json
|
||||||
22
LICENSE
Normal file
22
LICENSE
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Brightscout, Inc.
|
||||||
|
Copyright (c) 2026 ООО "Виликс"
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
67
README.md
Normal file
67
README.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# loop-etl-rocketchat
|
||||||
|
|
||||||
|
An ETL framework to migrate data from RocketChat to Loop. This utility exports data from a source RocketChat database and generates a Loop import file.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
1. Install [Node.js](https://nodejs.org/en/) Version [6.11.0 LTS](https://nodejs.org/en/download/) or greater
|
||||||
|
|
||||||
|
2. Clone this repo
|
||||||
|
`$ git clone https://git.wilix.dev/loop/loop-etl-rocketchat`
|
||||||
|
|
||||||
|
3. Install dependencies
|
||||||
|
`$ cd loop-etl-rocketchat`
|
||||||
|
`$ npm install`
|
||||||
|
|
||||||
|
4. Run tests
|
||||||
|
`$ npm test`
|
||||||
|
|
||||||
|
## Export RocketChat
|
||||||
|
|
||||||
|
Export supports the following entities:
|
||||||
|
|
||||||
|
1. Users and roles. Only global roles are supported
|
||||||
|
2. Custom emoji. File storage: FileSystem and GridFS
|
||||||
|
3. Channels
|
||||||
|
4. Direct channels
|
||||||
|
5. RocketChat Discussions (subchannels) are partially supported, see below
|
||||||
|
7. User uploads. File storage: FileSystem and GridFS
|
||||||
|
8. Posts with per user flag. Channel pins are not supported by import specification.
|
||||||
|
9. Replies and Reactions
|
||||||
|
|
||||||
|
## RocketChat exporting
|
||||||
|
|
||||||
|
1. Copy the example config file to config.js
|
||||||
|
```
|
||||||
|
cp context/config.example.rocketchat.js context/config.js
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Prepare your source and target configuration
|
||||||
|
|
||||||
|
1. Set `source.uploadsPath` for file uploads (user avatars and file uploads)
|
||||||
|
2. Set `source.customEmojiPath` for custom emojies
|
||||||
|
3. Set `target.filesPath` for Loop output directory
|
||||||
|
|
||||||
|
3. If you have LDAP enabled, Community version of Loop doesn't support LDAP.
|
||||||
|
If you have Community version, consider configuring ldap mapping to gitlab or disable it to use default login.
|
||||||
|
|
||||||
|
Specify it in `config.js`. Set `ldap_auth_service` to map ldap to a Loop login service.
|
||||||
|
If it's GitLab, configure `gitlab` with `host` and `token` registered in Gitlab with User access.
|
||||||
|
It is used for id mapping, without it Loop won't import the users and throw an error.
|
||||||
|
Before migrating to Loop, ensure you have Gitlab integration enabled in Loop and all users are present in Gitlab.
|
||||||
|
|
||||||
|
4. If you used RocketChat discussions, they will migrate in separate channels with random names.
|
||||||
|
You can merge discussions in parent channel with `mergeDiscussionIntoParent`
|
||||||
|
5. Global channel in Rocket Chat is **General** and in Loop - **Town Square**.
|
||||||
|
To have only 1 global channel, the configuration provides default example in `channels.map`.
|
||||||
|
You can specify migration for other channels as well.
|
||||||
|
6. Run migrate script with `npm run start:rocketchat`
|
||||||
|
7. Configure Loop (DB and Gitlab integration) before running the migration
|
||||||
|
8. Run the migration in Loop
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
1. Run the Loop import command as explained in the [documentation](https://loop.yonote.ru/share/750208a7-e016-4319-8c7e-fb08a91f51f6)
|
||||||
|
---
|
||||||
|
|
||||||
|
Based on [mattermost-etl](https://github.com/Brightscout/mattermost-etl) and [mattermost-etl-rocketchat](https://github.com/lexbritvin/mattermost-etl-rocketchat), both licensed under MIT.
|
||||||
17
circle.yml
Normal file
17
circle.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
general:
|
||||||
|
artifacts:
|
||||||
|
- coverage
|
||||||
|
|
||||||
|
machine:
|
||||||
|
node:
|
||||||
|
version: 6.11.0
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
override:
|
||||||
|
- yarn
|
||||||
|
|
||||||
|
test:
|
||||||
|
override:
|
||||||
|
- yarn run test-coverage
|
||||||
|
post:
|
||||||
|
- node_modules/codeclimate-test-reporter/bin/codeclimate.js < coverage/lcov.info
|
||||||
36
context/config.example.js
Normal file
36
context/config.example.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
module.exports = {
|
||||||
|
source: {
|
||||||
|
uri: 'mongodb://rocketchat:rocketchat@localhost:27017/rocketchat?replicaSet=rs0',
|
||||||
|
uploadsPath: './uploads',
|
||||||
|
customEmojiPath: './custom_emoji',
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
filename: './import.jsonl',
|
||||||
|
filesPath: './data/bulk-export-attachments'
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
team: {
|
||||||
|
// Уникальное системное имя команды (используется в URL, только латиница, цифры и дефисы)
|
||||||
|
name: 'loop',
|
||||||
|
// Отображаемое название команды, которое видят пользователи
|
||||||
|
display_name: 'loop',
|
||||||
|
// Краткое описание команды
|
||||||
|
description: 'An example of a team',
|
||||||
|
// Тип команды: 'I' — закрытая (invite-only), 'O' — открытая (open)
|
||||||
|
type: 'I',
|
||||||
|
// Разрешить пользователям приглашать других в команду без участия администратора
|
||||||
|
allow_open_invite: false
|
||||||
|
},
|
||||||
|
channels: {
|
||||||
|
mergeDiscussionIntoParent: true,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
globalRoleMap: {
|
||||||
|
admin: 'system_admin',
|
||||||
|
user: 'system_user',
|
||||||
|
},
|
||||||
|
'auth_service': null,
|
||||||
|
'auth_data': null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
context/index.js
Normal file
13
context/index.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const config = require('./config')
|
||||||
|
const jabber = require('../lib/mssql')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
config,
|
||||||
|
jabber,
|
||||||
|
values: {
|
||||||
|
//
|
||||||
|
// Cached values can be stored
|
||||||
|
// here
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
38
context/rocketchat.js
Normal file
38
context/rocketchat.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const config = require('./config')
|
||||||
|
const rocketchat = require('../lib/mongo')
|
||||||
|
|
||||||
|
|
||||||
|
rocketchat.messagesCollection = function () {
|
||||||
|
return rocketchat.collection('rocketchat_message')
|
||||||
|
}
|
||||||
|
|
||||||
|
rocketchat.roomsCollection = function () {
|
||||||
|
return rocketchat.collection('rocketchat_room')
|
||||||
|
}
|
||||||
|
|
||||||
|
rocketchat.usersCollection = function () {
|
||||||
|
return rocketchat.collection('users')
|
||||||
|
}
|
||||||
|
|
||||||
|
rocketchat.uploadsCollection = function () {
|
||||||
|
return rocketchat.collection('rocketchat_uploads')
|
||||||
|
}
|
||||||
|
|
||||||
|
rocketchat.avatarsCollection = function () {
|
||||||
|
return rocketchat.collection('rocketchat_avatars')
|
||||||
|
}
|
||||||
|
|
||||||
|
rocketchat.emojiCollection = function () {
|
||||||
|
return rocketchat.collection('rocketchat_custom_emoji')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
config,
|
||||||
|
rocketchat,
|
||||||
|
values: {
|
||||||
|
//
|
||||||
|
// Cached values can be stored
|
||||||
|
// here
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
49
index.js
Normal file
49
index.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
const context = require('./context')
|
||||||
|
const log = require('./lib/log')
|
||||||
|
const {
|
||||||
|
start,
|
||||||
|
version,
|
||||||
|
team,
|
||||||
|
channels,
|
||||||
|
users,
|
||||||
|
posts,
|
||||||
|
directChannels,
|
||||||
|
directPosts,
|
||||||
|
end
|
||||||
|
} = require('./lib/modules')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Common function log errors and
|
||||||
|
// terminate the process
|
||||||
|
//
|
||||||
|
const abort = function(err) {
|
||||||
|
log.error(err)
|
||||||
|
//
|
||||||
|
// We set a timeout here to
|
||||||
|
// allow the log streams to finish
|
||||||
|
// writing.
|
||||||
|
//
|
||||||
|
setTimeout(function() {
|
||||||
|
process.exit(1)
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Ensure we trap uncaught exceptions
|
||||||
|
// and properly abort
|
||||||
|
//
|
||||||
|
process.on('uncaughtException', abort)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Let's do it
|
||||||
|
//
|
||||||
|
start(context)
|
||||||
|
.then(version)
|
||||||
|
.then(team)
|
||||||
|
.then(channels)
|
||||||
|
.then(users)
|
||||||
|
.then(posts)
|
||||||
|
.then(directChannels)
|
||||||
|
.then(directPosts)
|
||||||
|
.then(end)
|
||||||
|
.catch(abort)
|
||||||
31
lib/datafile.js
Normal file
31
lib/datafile.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const os = require('os')
|
||||||
|
const { Transform } = require('stream')
|
||||||
|
|
||||||
|
module.exports = function(filename='data.json', onFinish=()=>{}) {
|
||||||
|
//
|
||||||
|
// Create an object to string transform
|
||||||
|
// stream
|
||||||
|
//
|
||||||
|
var stream = new Transform({
|
||||||
|
writableObjectMode: true,
|
||||||
|
|
||||||
|
transform(chunk, encoding, callback) {
|
||||||
|
this.push(JSON.stringify(chunk))
|
||||||
|
this.push(os.EOL)
|
||||||
|
return callback()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//
|
||||||
|
// Pipe that to the file write
|
||||||
|
// stream and set up an onFinish
|
||||||
|
// handler
|
||||||
|
//
|
||||||
|
stream.pipe(
|
||||||
|
fs.createWriteStream(filename).on('finish', onFinish)
|
||||||
|
)
|
||||||
|
//
|
||||||
|
// Return the new write stream
|
||||||
|
//
|
||||||
|
return stream
|
||||||
|
}
|
||||||
24
lib/factory/channel.js
Normal file
24
lib/factory/channel.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const Joi = require('joi')
|
||||||
|
const validate = require('./validate')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Define the schema
|
||||||
|
//
|
||||||
|
const schema = {
|
||||||
|
team: Joi.string(),
|
||||||
|
name: Joi.string().regex(/^[a-z0-9_-]+$/),
|
||||||
|
display_name: Joi.string(),
|
||||||
|
header: Joi.string().allow('').optional(),
|
||||||
|
purpose: Joi.string().allow('').optional(),
|
||||||
|
type: Joi.string().valid('O', 'P')
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Generate a valid object
|
||||||
|
//
|
||||||
|
module.exports = function (props) {
|
||||||
|
return {
|
||||||
|
type: 'channel',
|
||||||
|
channel: validate(schema, props)
|
||||||
|
}
|
||||||
|
}
|
||||||
20
lib/factory/directChannel.js
Normal file
20
lib/factory/directChannel.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const Joi = require('joi')
|
||||||
|
const validate = require('./validate')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Define the schema
|
||||||
|
//
|
||||||
|
const schema = {
|
||||||
|
header: Joi.string().allow('').optional(),
|
||||||
|
members: Joi.array().items(Joi.string()).min(2).max(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Generate a valid object
|
||||||
|
//
|
||||||
|
module.exports = function (props) {
|
||||||
|
return {
|
||||||
|
type: 'direct_channel',
|
||||||
|
direct_channel: validate(schema, props)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
lib/factory/directPost.js
Normal file
21
lib/factory/directPost.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const Joi = require('joi')
|
||||||
|
const validate = require('./validate')
|
||||||
|
const postPartial = require('./postPartial')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Define the schema
|
||||||
|
//
|
||||||
|
const schema = {
|
||||||
|
channel_members: Joi.array().items(Joi.string()).min(2),
|
||||||
|
...postPartial,
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Generate a valid object
|
||||||
|
//
|
||||||
|
module.exports = function (props) {
|
||||||
|
return {
|
||||||
|
type: 'direct_post',
|
||||||
|
direct_post: validate(schema, props)
|
||||||
|
}
|
||||||
|
}
|
||||||
20
lib/factory/emoji.js
Normal file
20
lib/factory/emoji.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const Joi = require('joi')
|
||||||
|
const validate = require('./validate')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Define the schema
|
||||||
|
//
|
||||||
|
const schema = {
|
||||||
|
name: Joi.string(),
|
||||||
|
image: Joi.string(),
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Generate a valid object
|
||||||
|
//
|
||||||
|
module.exports = function (props) {
|
||||||
|
return {
|
||||||
|
type: 'emoji',
|
||||||
|
emoji: validate(schema, props)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
lib/factory/index.js
Normal file
19
lib/factory/index.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const version = require('./version')
|
||||||
|
const emoji = require('./emoji')
|
||||||
|
const team = require('./team')
|
||||||
|
const channel = require('./channel')
|
||||||
|
const user = require('./user')
|
||||||
|
const post = require('./post')
|
||||||
|
const directChannel = require('./directChannel')
|
||||||
|
const directPost = require('./directPost')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
version,
|
||||||
|
emoji,
|
||||||
|
team,
|
||||||
|
channel,
|
||||||
|
user,
|
||||||
|
post,
|
||||||
|
directChannel,
|
||||||
|
directPost
|
||||||
|
}
|
||||||
22
lib/factory/post.js
Normal file
22
lib/factory/post.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const Joi = require('joi')
|
||||||
|
const validate = require('./validate')
|
||||||
|
const postPartial = require('./postPartial')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Define the schema
|
||||||
|
//
|
||||||
|
const schema = {
|
||||||
|
team: Joi.string(),
|
||||||
|
channel: Joi.string(),
|
||||||
|
...postPartial,
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Generate a valid object
|
||||||
|
//
|
||||||
|
module.exports = function (props) {
|
||||||
|
return {
|
||||||
|
type: 'post',
|
||||||
|
post: validate(schema, props)
|
||||||
|
}
|
||||||
|
}
|
||||||
31
lib/factory/postPartial.js
Normal file
31
lib/factory/postPartial.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const Joi = require('joi')
|
||||||
|
|
||||||
|
const attachment = Joi.object({
|
||||||
|
path: Joi.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
const reaction = Joi.object({
|
||||||
|
user: Joi.string(),
|
||||||
|
emoji_name: Joi.string(),
|
||||||
|
create_at: Joi.number(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const messagePartial = {
|
||||||
|
user: Joi.string(),
|
||||||
|
message: Joi.string().allow(''),
|
||||||
|
attachments: Joi.array().items(attachment).optional(),
|
||||||
|
flagged_by: Joi.array().items(Joi.string()).optional(),
|
||||||
|
reactions: Joi.array().items(reaction).optional(),
|
||||||
|
create_at: Joi.number(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const reply = Joi.object().keys(messagePartial)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Define the schema
|
||||||
|
//
|
||||||
|
module.exports = {
|
||||||
|
...messagePartial,
|
||||||
|
replies: Joi.array().items(reply).optional(),
|
||||||
|
}
|
||||||
|
|
||||||
23
lib/factory/team.js
Normal file
23
lib/factory/team.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const Joi = require('joi')
|
||||||
|
const validate = require('./validate')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Define the schema
|
||||||
|
//
|
||||||
|
const schema = {
|
||||||
|
name: Joi.string(),
|
||||||
|
display_name: Joi.string(),
|
||||||
|
description: Joi.string(),
|
||||||
|
type: Joi.string().valid('O', 'I'),
|
||||||
|
allow_open_invite: Joi.boolean()
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Generate a valid object
|
||||||
|
//
|
||||||
|
module.exports = function (props) {
|
||||||
|
return {
|
||||||
|
type: 'team',
|
||||||
|
team: validate(schema, props)
|
||||||
|
}
|
||||||
|
}
|
||||||
35
lib/factory/test/channel.js
Normal file
35
lib/factory/test/channel.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
const expect = require('chai').expect
|
||||||
|
const channel = require('../channel')
|
||||||
|
|
||||||
|
const basic = {
|
||||||
|
team: 'test-team',
|
||||||
|
name: 'test-channel',
|
||||||
|
display_name: 'Test Channel',
|
||||||
|
header: 'Test Channel Header',
|
||||||
|
purpose: 'Test the channel generator',
|
||||||
|
type: 'P'
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('team factory', function() {
|
||||||
|
|
||||||
|
it('should produce a valid object', function() {
|
||||||
|
var c = channel(basic)
|
||||||
|
expect(c).to.be.an('object')
|
||||||
|
expect(c).to.deep.equal({
|
||||||
|
type: 'channel',
|
||||||
|
channel: basic
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should prevent an invalid type', function() {
|
||||||
|
try {
|
||||||
|
channel(Object.assign({}, basic, {
|
||||||
|
type: 'X'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
expect(e).to.be.an('error')
|
||||||
|
expect(e.details[0].message).to.equal('"type" must be one of [O, P]')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
35
lib/factory/test/directChannel.js
Normal file
35
lib/factory/test/directChannel.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
const expect = require('chai').expect
|
||||||
|
const directChannel = require('../directChannel')
|
||||||
|
|
||||||
|
const basic = {
|
||||||
|
members: [
|
||||||
|
'username1',
|
||||||
|
'username2'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('team factory', function() {
|
||||||
|
|
||||||
|
it('should produce a valid object', function() {
|
||||||
|
var c = directChannel(basic)
|
||||||
|
expect(c).to.be.an('object')
|
||||||
|
expect(c).to.deep.equal({
|
||||||
|
type: 'direct_channel',
|
||||||
|
direct_channel: basic
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should prevent less than 2 members', function() {
|
||||||
|
try {
|
||||||
|
directChannel(Object.assign({}, basic, {
|
||||||
|
members:[
|
||||||
|
'username1'
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
expect(e).to.be.an('error')
|
||||||
|
expect(e.details[0].message).to.equal('"members" must contain at least 2 items')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
53
lib/factory/test/directPost.js
Normal file
53
lib/factory/test/directPost.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
const expect = require('chai').expect
|
||||||
|
const directPost = require('../directPost')
|
||||||
|
|
||||||
|
const basic = {
|
||||||
|
channel_members: [
|
||||||
|
'username1',
|
||||||
|
'username2'
|
||||||
|
],
|
||||||
|
user: 'username1',
|
||||||
|
message: 'carpe diem',
|
||||||
|
create_at: new Date().getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('post factory', function() {
|
||||||
|
|
||||||
|
it('should produce a valid object', function() {
|
||||||
|
var p = directPost(basic)
|
||||||
|
expect(p).to.be.an('object')
|
||||||
|
expect(p).to.deep.equal({
|
||||||
|
type: 'direct_post',
|
||||||
|
direct_post: basic
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should ensure message is not empty', function() {
|
||||||
|
try {
|
||||||
|
var p = directPost(Object.assign({}, basic, {
|
||||||
|
message: ''
|
||||||
|
}))
|
||||||
|
expect(p).to.be.undefined
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
expect(e).to.be.an('error')
|
||||||
|
expect(e.details[0].message).to.equal('"message" is not allowed to be empty')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should prevent less than 2 members', function() {
|
||||||
|
try {
|
||||||
|
var p = directPost(Object.assign({}, basic, {
|
||||||
|
channel_members:[
|
||||||
|
'username1'
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
expect(p).to.be.undefined
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
expect(e).to.be.an('error')
|
||||||
|
expect(e.details[0].message).to.equal('"channel_members" must contain at least 2 items')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
35
lib/factory/test/post.js
Normal file
35
lib/factory/test/post.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
const expect = require('chai').expect
|
||||||
|
const post = require('../post')
|
||||||
|
|
||||||
|
const basic = {
|
||||||
|
team: 'test',
|
||||||
|
channel: 'channel',
|
||||||
|
user: 'user',
|
||||||
|
message: 'carpe diem',
|
||||||
|
create_at: new Date().getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('post factory', function() {
|
||||||
|
|
||||||
|
it('should produce a valid object', function() {
|
||||||
|
var p = post(basic)
|
||||||
|
expect(p).to.be.an('object')
|
||||||
|
expect(p).to.deep.equal({
|
||||||
|
type: 'post',
|
||||||
|
post: basic
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should ensure message is not empty', function() {
|
||||||
|
try {
|
||||||
|
var p = post(Object.assign({}, basic, {
|
||||||
|
message: ''
|
||||||
|
}))
|
||||||
|
expect(p).to.be.undefined
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
expect(e).to.be.an('error')
|
||||||
|
expect(e.details[0].message).to.equal('"message" is not allowed to be empty')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
34
lib/factory/test/team.js
Normal file
34
lib/factory/test/team.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const expect = require('chai').expect
|
||||||
|
const team = require('../team')
|
||||||
|
|
||||||
|
const basic = {
|
||||||
|
name: 'test-team',
|
||||||
|
display_name: 'Test Team',
|
||||||
|
description: 'A test team for testing',
|
||||||
|
type: 'O',
|
||||||
|
allow_open_invite: true
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('team factory', function() {
|
||||||
|
|
||||||
|
it('should produce a valid object', function() {
|
||||||
|
var t = team(basic)
|
||||||
|
expect(t).to.be.an('object')
|
||||||
|
expect(t).to.deep.equal({
|
||||||
|
type: 'team',
|
||||||
|
team: basic
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should prevent an invalid type', function() {
|
||||||
|
try {
|
||||||
|
team(Object.assign({}, basic, {
|
||||||
|
type: 'X'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
expect(e).to.be.an('error')
|
||||||
|
expect(e.details[0].message).to.equal('"type" must be one of [O, I]')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
42
lib/factory/test/user.js
Normal file
42
lib/factory/test/user.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const expect = require('chai').expect
|
||||||
|
const user = require('../user')
|
||||||
|
|
||||||
|
const basic = {
|
||||||
|
username: 'user.name',
|
||||||
|
email: 'user@example.gov',
|
||||||
|
auth_service: 'ldap',
|
||||||
|
auth_data: 'username-field-reference',
|
||||||
|
teams: [{
|
||||||
|
name: 'test',
|
||||||
|
channels: [{
|
||||||
|
name: 'channel-1'
|
||||||
|
}, {
|
||||||
|
name: 'channel-2'
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('user factory', function() {
|
||||||
|
|
||||||
|
it('should produce a valid object', function() {
|
||||||
|
var u = user(basic)
|
||||||
|
expect(u).to.be.an('object')
|
||||||
|
expect(u).to.deep.equal({
|
||||||
|
type: 'user',
|
||||||
|
user: basic
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should ensure email is valid', function() {
|
||||||
|
try {
|
||||||
|
var u = user(Object.assign({}, basic, {
|
||||||
|
email: 'foo@bar'
|
||||||
|
}))
|
||||||
|
expect(u).to.be.undefined
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
expect(e).to.be.an('error')
|
||||||
|
expect(e.details[0].message).to.equal('"email" must be a valid email')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
13
lib/factory/test/version.js
Normal file
13
lib/factory/test/version.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const expect = require('chai').expect
|
||||||
|
const version = require('../version')
|
||||||
|
|
||||||
|
describe('version factory', function() {
|
||||||
|
it('should produce a valid version', function() {
|
||||||
|
var v = version()
|
||||||
|
expect(v).to.be.an('object')
|
||||||
|
expect(v).to.deep.equal({
|
||||||
|
type: 'version',
|
||||||
|
version: 1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
72
lib/factory/user.js
Normal file
72
lib/factory/user.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
const Joi = require('joi')
|
||||||
|
const validate = require('./validate')
|
||||||
|
|
||||||
|
const validateRoles = function (value, helper, allowed) {
|
||||||
|
const roles = value.split(' ')
|
||||||
|
if (value.length < 8) {
|
||||||
|
return helper.message('Password must be at least 8 characters long')
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Define the schema
|
||||||
|
//
|
||||||
|
const schema = {
|
||||||
|
profile_image: Joi.string().optional(),
|
||||||
|
username: Joi.string(),
|
||||||
|
email: Joi.string().email({
|
||||||
|
errorLevel: true,
|
||||||
|
minDomainAtoms: 2
|
||||||
|
}),
|
||||||
|
auth_service: Joi.string().valid(
|
||||||
|
'',
|
||||||
|
'gitlab',
|
||||||
|
'ldap',
|
||||||
|
'saml',
|
||||||
|
'google',
|
||||||
|
'office365'
|
||||||
|
).optional(),
|
||||||
|
auth_data: Joi.string().optional().allow(''),
|
||||||
|
password: Joi.string().optional(),
|
||||||
|
nickname: Joi.string().optional(),
|
||||||
|
first_name: Joi.string().optional(),
|
||||||
|
last_name: Joi.string().optional(),
|
||||||
|
position: Joi.string().optional(),
|
||||||
|
roles: Joi.string().optional().valid(
|
||||||
|
'system_user',
|
||||||
|
'system_admin system_user'
|
||||||
|
),
|
||||||
|
teams: Joi.array().items(
|
||||||
|
Joi.object({
|
||||||
|
name: Joi.string(),
|
||||||
|
roles: Joi.string().optional().valid(
|
||||||
|
'team_user',
|
||||||
|
'team_admin team_user'
|
||||||
|
),
|
||||||
|
channels: Joi.array().items(
|
||||||
|
Joi.object({
|
||||||
|
name: Joi.string(),
|
||||||
|
roles: Joi.string().optional().valid(
|
||||||
|
'channel_user',
|
||||||
|
'channel_user channel_admin'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
),
|
||||||
|
notify_props: Joi.object({
|
||||||
|
mention_keys: Joi.string().optional().allow(''),
|
||||||
|
}).optional(),
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Generate a valid object
|
||||||
|
//
|
||||||
|
module.exports = function (props) {
|
||||||
|
return {
|
||||||
|
type: 'user',
|
||||||
|
user: validate(schema, props)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
lib/factory/validate.js
Normal file
21
lib/factory/validate.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const Joi = require('joi')
|
||||||
|
|
||||||
|
module.exports = function (schema, props) {
|
||||||
|
//
|
||||||
|
// Validate and remove unknown keys
|
||||||
|
//
|
||||||
|
var {error, value} = Joi.validate(props, schema, {
|
||||||
|
presence: 'required',
|
||||||
|
stripUnknown: true
|
||||||
|
})
|
||||||
|
//
|
||||||
|
// Throw validation errors
|
||||||
|
//
|
||||||
|
if (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Return the value
|
||||||
|
//
|
||||||
|
return value
|
||||||
|
}
|
||||||
11
lib/factory/version.js
Normal file
11
lib/factory/version.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//
|
||||||
|
// Generate a version object. This
|
||||||
|
// is the only valid value as of
|
||||||
|
// now
|
||||||
|
//
|
||||||
|
module.exports = function() {
|
||||||
|
return {
|
||||||
|
type: 'version',
|
||||||
|
version: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
15
lib/log.js
Normal file
15
lib/log.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const bunyan = require('bunyan')
|
||||||
|
|
||||||
|
module.exports = bunyan.createLogger({
|
||||||
|
name: 'mm-etl',
|
||||||
|
streams: [
|
||||||
|
{
|
||||||
|
level: 'info',
|
||||||
|
stream: process.stdout
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 'info',
|
||||||
|
path: 'mm-etl.log'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
114
lib/modules/channels.js
Normal file
114
lib/modules/channels.js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
const _ = require('lodash')
|
||||||
|
const slug = require('slug')
|
||||||
|
const XML = require('xmldoc')
|
||||||
|
const Factory = require('../factory')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initialize the child logger for
|
||||||
|
// the module
|
||||||
|
//
|
||||||
|
const log = require('../log').child({
|
||||||
|
module: 'channels'
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = function(context) {
|
||||||
|
//
|
||||||
|
// Select all of the rooms
|
||||||
|
//
|
||||||
|
return context.jabber.fetch(
|
||||||
|
'SELECT room_jid, subject, config FROM dbo.tc_rooms'
|
||||||
|
)
|
||||||
|
//
|
||||||
|
// The convert them to channels and
|
||||||
|
// write them to the datafile
|
||||||
|
//
|
||||||
|
.then(function(results) {
|
||||||
|
log.info(`${results.recordset.length} records found`)
|
||||||
|
//
|
||||||
|
// Define a map to hold the channels
|
||||||
|
// info for downstream modules
|
||||||
|
//
|
||||||
|
var channels = {}
|
||||||
|
//
|
||||||
|
// Iterate over the record set and
|
||||||
|
// create a channel for each room
|
||||||
|
//
|
||||||
|
results.recordset.forEach(function(room) {
|
||||||
|
log.debug(room)
|
||||||
|
//
|
||||||
|
// Define the channel and add
|
||||||
|
// it to the context
|
||||||
|
//
|
||||||
|
var channel = channels[room.room_jid] = {
|
||||||
|
team: context.values.team.name,
|
||||||
|
name: slug(toName(room.room_jid)),
|
||||||
|
display_name: toDisplayName(room.room_jid),
|
||||||
|
header: toDescription(room.subject),
|
||||||
|
purpose: toDescription(room.subject),
|
||||||
|
type: toType(room.config)
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Write the channel data to the
|
||||||
|
// output
|
||||||
|
//
|
||||||
|
log.info(`... writing ${channel.name}`)
|
||||||
|
context.output.write(
|
||||||
|
Factory.channel(channel)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
//
|
||||||
|
// Add the channel map to the context
|
||||||
|
//
|
||||||
|
context.values.channels = channels
|
||||||
|
//
|
||||||
|
// Resolve the promise
|
||||||
|
//
|
||||||
|
return context
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Parses the name from a room jid
|
||||||
|
//
|
||||||
|
const toName = function (jid='') {
|
||||||
|
return jid.substr(0, jid.indexOf('@')).replace(/\\20/g, ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Formats the display name
|
||||||
|
//
|
||||||
|
const toDisplayName = function (jid='') {
|
||||||
|
return _.startCase(_.toLower(toName(jid)))
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Returns a description from the subject
|
||||||
|
//
|
||||||
|
const toDescription = function (subject='') {
|
||||||
|
return subject
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Parses the channel type from the
|
||||||
|
// config field
|
||||||
|
//
|
||||||
|
const toType = function (xml='<x/>') {
|
||||||
|
//
|
||||||
|
// Parse the config
|
||||||
|
//
|
||||||
|
var config = new XML.XmlDocument(xml)
|
||||||
|
//
|
||||||
|
// Extract the value. If it does not exist,
|
||||||
|
// we default to '1' - private
|
||||||
|
//
|
||||||
|
var value = _.get(
|
||||||
|
config.childWithAttribute('var', 'members-only'),
|
||||||
|
'firstChild.val',
|
||||||
|
'1'
|
||||||
|
)
|
||||||
|
//
|
||||||
|
// If members-only is '1', return private
|
||||||
|
// otherwise, return public
|
||||||
|
//
|
||||||
|
return value === '1' ? 'P' : 'O'
|
||||||
|
}
|
||||||
89
lib/modules/directChannels.js
Normal file
89
lib/modules/directChannels.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
const Factory = require('../factory')
|
||||||
|
const transform = require('./transform')
|
||||||
|
const Utils = require('./utils')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initialize the child logger for
|
||||||
|
// the module
|
||||||
|
//
|
||||||
|
const log = require('../log').child({
|
||||||
|
module: 'directChannels'
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = function(context) {
|
||||||
|
return new Promise(function(resolve /*, reject */) {
|
||||||
|
log.info('streaming records')
|
||||||
|
//
|
||||||
|
// Array to accumulate the channel
|
||||||
|
// member pairs
|
||||||
|
//
|
||||||
|
var channels = {}
|
||||||
|
//
|
||||||
|
// Query messages from Jabber and pipe
|
||||||
|
// through the post transform and
|
||||||
|
// then to the output. We use pipe to
|
||||||
|
// handle very large data sets using
|
||||||
|
// streams
|
||||||
|
//
|
||||||
|
context.jabber.pipe(
|
||||||
|
//
|
||||||
|
// Define the query
|
||||||
|
//
|
||||||
|
`SELECT DISTINCT to_jid, from_jid FROM dbo.jm
|
||||||
|
WHERE msg_type = 'c'
|
||||||
|
AND direction = 'I'
|
||||||
|
AND (body_string != '' or datalength(body_text) > 0)`,
|
||||||
|
//
|
||||||
|
// Define the tranform
|
||||||
|
//
|
||||||
|
transform(function(result, encoding, callback) {
|
||||||
|
try {
|
||||||
|
log.debug(result)
|
||||||
|
//
|
||||||
|
// Generate the members array for the
|
||||||
|
// direct channel
|
||||||
|
//
|
||||||
|
let members = Utils.members(
|
||||||
|
context.values.users,
|
||||||
|
result.to_jid,
|
||||||
|
result.from_jid
|
||||||
|
)
|
||||||
|
//
|
||||||
|
// If there at least two members
|
||||||
|
//
|
||||||
|
if(Utils.membersAreValid(members)) {
|
||||||
|
var key = `${members[0]}|${members[1]}`
|
||||||
|
//
|
||||||
|
// And we haven't processed this pair
|
||||||
|
// already
|
||||||
|
//
|
||||||
|
if(!channels[key]) {
|
||||||
|
channels[key] = members
|
||||||
|
log.info(`... writing ${members}`)
|
||||||
|
context.output.write(
|
||||||
|
Factory.directChannel({
|
||||||
|
members
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.error(`... ignoring directChannel from: ${result.from_jid} to: ${result.to_jid} on error: ${err.message}.`)
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Invoke the call to mark that we are
|
||||||
|
// done with the chunk
|
||||||
|
//
|
||||||
|
return callback()
|
||||||
|
},
|
||||||
|
//
|
||||||
|
// Define the callback to be invoked
|
||||||
|
// on finish
|
||||||
|
//
|
||||||
|
function() {
|
||||||
|
log.info('... finished')
|
||||||
|
resolve(context)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
106
lib/modules/directPosts.js
Normal file
106
lib/modules/directPosts.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
const Factory = require('../factory')
|
||||||
|
const transform = require('./transform')
|
||||||
|
const Utils = require('./utils')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initialize the child logger for
|
||||||
|
// the module
|
||||||
|
//
|
||||||
|
const log = require('../log').child({
|
||||||
|
module: 'directPosts'
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = function(context) {
|
||||||
|
return new Promise(function(resolve /*, reject */) {
|
||||||
|
log.info('streaming records')
|
||||||
|
//
|
||||||
|
// Keep track of the number of posts
|
||||||
|
// written for logging
|
||||||
|
//
|
||||||
|
var written = 0
|
||||||
|
//
|
||||||
|
// Query messages from Jabber and pipe
|
||||||
|
// through the post transform and
|
||||||
|
// then to the output. We use pipe to
|
||||||
|
// handle very large data sets using
|
||||||
|
// streams
|
||||||
|
//
|
||||||
|
context.jabber.pipe(
|
||||||
|
//
|
||||||
|
// Define the query
|
||||||
|
//
|
||||||
|
`SELECT to_jid, from_jid, sent_date, body_string, body_text FROM dbo.jm
|
||||||
|
WHERE msg_type = 'c'
|
||||||
|
AND direction = 'I'
|
||||||
|
AND (body_string != '' or datalength(body_text) > 0)`,
|
||||||
|
//
|
||||||
|
// Define the tranform
|
||||||
|
//
|
||||||
|
transform(function(message, encoding, callback) {
|
||||||
|
try {
|
||||||
|
log.debug(message)
|
||||||
|
//
|
||||||
|
// Generate the members array for the
|
||||||
|
// direct channel
|
||||||
|
//
|
||||||
|
let members = Utils.members(
|
||||||
|
context.values.users,
|
||||||
|
message.to_jid,
|
||||||
|
message.from_jid
|
||||||
|
)
|
||||||
|
//
|
||||||
|
// Ensure we have at least two channel
|
||||||
|
// members before we can write the message
|
||||||
|
//
|
||||||
|
if (Utils.membersAreValid(members)) {
|
||||||
|
//
|
||||||
|
// Base post props
|
||||||
|
//
|
||||||
|
var post = {
|
||||||
|
channel_members: members,
|
||||||
|
user: Utils.username(context.values.users, message.from_jid),
|
||||||
|
create_at: Utils.millis(message.sent_date)
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Process each chunk
|
||||||
|
//
|
||||||
|
Utils.body(message).forEach(function(chunk) {
|
||||||
|
context.output.write(
|
||||||
|
Factory.directPost(Object.assign({}, post, {
|
||||||
|
message: chunk
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
//
|
||||||
|
// Log progress periodically
|
||||||
|
//
|
||||||
|
written += 1
|
||||||
|
if(written % 1000 == 0) {
|
||||||
|
log.info(`... wrote ${written} posts`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
log.warn({
|
||||||
|
to_jid: message.to_jid,
|
||||||
|
from_jid: message.from_jid
|
||||||
|
}, '... skipping message with invalid channel members')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.error(`... ignoring directPost from: ${message.from_jid} to: ${message.to_jid} on error: ${err.message}.`)
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Invoke the call to mark that we are
|
||||||
|
// done with the chunk
|
||||||
|
//
|
||||||
|
return callback()
|
||||||
|
},
|
||||||
|
//
|
||||||
|
// Define the callback to be invoked
|
||||||
|
// on finish
|
||||||
|
//
|
||||||
|
function() {
|
||||||
|
log.info(`... finished writing ${written} posts`)
|
||||||
|
resolve(context)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
12
lib/modules/end.js
Normal file
12
lib/modules/end.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
//
|
||||||
|
// Initialize the child logger for
|
||||||
|
// the module
|
||||||
|
//
|
||||||
|
const log = require('../log').child({
|
||||||
|
module: 'end'
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = function(context) {
|
||||||
|
log.info('end')
|
||||||
|
context.output.end()
|
||||||
|
}
|
||||||
21
lib/modules/index.js
Normal file
21
lib/modules/index.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const start = require('./start')
|
||||||
|
const version = require('./version')
|
||||||
|
const team = require('./team')
|
||||||
|
const channels = require('./channels')
|
||||||
|
const users = require('./users')
|
||||||
|
const posts = require('./posts')
|
||||||
|
const directChannels = require('./directChannels')
|
||||||
|
const directPosts = require('./directPosts')
|
||||||
|
const end = require('./end')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
start,
|
||||||
|
version,
|
||||||
|
team,
|
||||||
|
channels,
|
||||||
|
users,
|
||||||
|
posts,
|
||||||
|
directChannels,
|
||||||
|
directPosts,
|
||||||
|
end
|
||||||
|
}
|
||||||
100
lib/modules/posts.js
Normal file
100
lib/modules/posts.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
const Factory = require('../factory')
|
||||||
|
const transform = require('./transform')
|
||||||
|
const Utils = require('./utils')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initialize the child logger for
|
||||||
|
// the module
|
||||||
|
//
|
||||||
|
const log = require('../log').child({
|
||||||
|
module: 'posts'
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = function(context) {
|
||||||
|
return new Promise(function(resolve /*, reject */) {
|
||||||
|
log.info('streaming records')
|
||||||
|
//
|
||||||
|
// Keep track of the number of posts
|
||||||
|
// written for logging
|
||||||
|
//
|
||||||
|
var written = 0
|
||||||
|
//
|
||||||
|
// Query messages from Jabber and pipe
|
||||||
|
// through the post transform and
|
||||||
|
// then to the output. We use pipe to
|
||||||
|
// handle very large data sets using
|
||||||
|
// streams
|
||||||
|
//
|
||||||
|
context.jabber.pipe(
|
||||||
|
//
|
||||||
|
// Define the query
|
||||||
|
//
|
||||||
|
`SELECT
|
||||||
|
msg_id,
|
||||||
|
to_jid,
|
||||||
|
from_jid,
|
||||||
|
sent_date,
|
||||||
|
body_string,
|
||||||
|
body_text
|
||||||
|
FROM dbo.tc_msgarchive WHERE msg_type = 'g' AND (body_string != '' OR datalength(body_text) > 0) `,
|
||||||
|
//
|
||||||
|
// Define the tranform
|
||||||
|
//
|
||||||
|
transform(function(message, encoding, callback) {
|
||||||
|
try {
|
||||||
|
log.debug(message)
|
||||||
|
//
|
||||||
|
// Base post props
|
||||||
|
//
|
||||||
|
var post = {
|
||||||
|
team: context.values.team.name,
|
||||||
|
channel: Utils.channelName(
|
||||||
|
context.values.channels, message.to_jid
|
||||||
|
),
|
||||||
|
user: Utils.username(
|
||||||
|
context.values.users, message.from_jid
|
||||||
|
),
|
||||||
|
create_at: Utils.millis(message.sent_date)
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Process each chunk
|
||||||
|
//
|
||||||
|
Utils.body(message).forEach(function(chunk) {
|
||||||
|
//
|
||||||
|
// Write the post object to the
|
||||||
|
// output
|
||||||
|
//
|
||||||
|
context.output.write(
|
||||||
|
Factory.post(Object.assign({}, post, {
|
||||||
|
message: chunk
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
//
|
||||||
|
// Log progress periodically
|
||||||
|
//
|
||||||
|
written += 1
|
||||||
|
if(written % 1000 == 0) {
|
||||||
|
log.info(`... wrote ${written} posts`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
log.error(`... ignoring message id:${message.msg_id} on error: ${err.message}.`)
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// Invoke the call to mark that we are
|
||||||
|
// done with the chunk
|
||||||
|
//
|
||||||
|
return callback()
|
||||||
|
},
|
||||||
|
//
|
||||||
|
// Define the callback to be invoked
|
||||||
|
// on finish
|
||||||
|
//
|
||||||
|
function() {
|
||||||
|
log.info(`... finished writing ${written} posts`)
|
||||||
|
resolve(context)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
32
lib/modules/start.js
Normal file
32
lib/modules/start.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const datafile = require('../datafile')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initialize the child logger for
|
||||||
|
// the module
|
||||||
|
//
|
||||||
|
const log = require('../log').child({
|
||||||
|
module: 'start'
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = function(context) {
|
||||||
|
log.info('connecting to jabber')
|
||||||
|
//
|
||||||
|
// Connect to the jabber database
|
||||||
|
//
|
||||||
|
return context.jabber.connect(context.config)
|
||||||
|
.then(function() {
|
||||||
|
log.info(`creating file '${context.config.target.filename}'`)
|
||||||
|
//
|
||||||
|
// Create the datafile and add
|
||||||
|
// it to the context
|
||||||
|
//
|
||||||
|
context.output = datafile(
|
||||||
|
context.config.target.filename,
|
||||||
|
process.exit
|
||||||
|
)
|
||||||
|
//
|
||||||
|
// Resolve the promise
|
||||||
|
//
|
||||||
|
return context
|
||||||
|
})
|
||||||
|
}
|
||||||
29
lib/modules/team.js
Normal file
29
lib/modules/team.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const Factory = require('../factory')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initialize the child logger for
|
||||||
|
// the module
|
||||||
|
//
|
||||||
|
const log = require('../log').child({
|
||||||
|
module: 'team'
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = function(context) {
|
||||||
|
log.info(`writing team '${context.config.define.team.name}'`)
|
||||||
|
//
|
||||||
|
// Store the team object for use
|
||||||
|
// by other modules
|
||||||
|
//
|
||||||
|
context.values.team = context.config.define.team
|
||||||
|
//
|
||||||
|
// Write the team object to the
|
||||||
|
// output
|
||||||
|
//
|
||||||
|
context.output.write(Factory.team(
|
||||||
|
context.config.define.team
|
||||||
|
))
|
||||||
|
//
|
||||||
|
// Return a resolved promise
|
||||||
|
//
|
||||||
|
return Promise.resolve(context)
|
||||||
|
}
|
||||||
61
lib/modules/test/channels.js
Normal file
61
lib/modules/test/channels.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
const expect = require('chai').expect
|
||||||
|
const channels = require('../channels')
|
||||||
|
const Fixtures = require('./fixtures')
|
||||||
|
const context = require('./context')()
|
||||||
|
|
||||||
|
describe('modules.channels', function() {
|
||||||
|
|
||||||
|
it('should process channel objects', function(done) {
|
||||||
|
|
||||||
|
context.values.team = context.config.define.team
|
||||||
|
|
||||||
|
context.jabber.fetch.returns(Promise.resolve({
|
||||||
|
recordset: Fixtures.channels
|
||||||
|
}))
|
||||||
|
|
||||||
|
channels(context)
|
||||||
|
.then(function (c) {
|
||||||
|
expect(c).to.equal(context)
|
||||||
|
expect(Object.keys(c.values.channels).length).equals(3)
|
||||||
|
expect(c.output.write.args[0][0]).to.deep.equal({
|
||||||
|
type: 'channel',
|
||||||
|
channel: {
|
||||||
|
team: 'test',
|
||||||
|
name: 'admin',
|
||||||
|
display_name: 'Admin',
|
||||||
|
header: 'Admin Room',
|
||||||
|
purpose: 'Admin Room',
|
||||||
|
type: 'O'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(c.output.write.args[1][0]).to.deep.equal({
|
||||||
|
type: 'channel',
|
||||||
|
channel: {
|
||||||
|
team: 'test',
|
||||||
|
name: 'test-room',
|
||||||
|
display_name: 'Test Room',
|
||||||
|
header: '',
|
||||||
|
purpose: '',
|
||||||
|
type: 'O'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(c.output.write.args[2][0]).to.deep.equal({
|
||||||
|
type: 'channel',
|
||||||
|
channel: {
|
||||||
|
team: 'test',
|
||||||
|
name: 'tonyd_20161206_1432',
|
||||||
|
display_name: 'Tonyd 20161206 1432',
|
||||||
|
header: '',
|
||||||
|
purpose: '',
|
||||||
|
type: 'O'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
context.jabber.fetch.reset()
|
||||||
|
context.output.write.reset()
|
||||||
|
})
|
||||||
|
})
|
||||||
38
lib/modules/test/context.js
Normal file
38
lib/modules/test/context.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const { spy, stub } = require('sinon')
|
||||||
|
|
||||||
|
module.exports = function() {
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
source: {
|
||||||
|
uri: 'mssql://username:password@server:1433/jabber?encrypt=true'
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
filename: 'data.json'
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
team: {
|
||||||
|
name: 'test',
|
||||||
|
display_name: 'Test Team',
|
||||||
|
description: 'Our Test Team',
|
||||||
|
type: 'I',
|
||||||
|
allow_open_invite: false
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
auth_service: 'ldap',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
jabber: {
|
||||||
|
connect: stub().returns(Promise.resolve()),
|
||||||
|
fetch: stub(),
|
||||||
|
pipe: stub()
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
write: spy(),
|
||||||
|
end: spy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
lib/modules/test/directChannels.js
Normal file
70
lib/modules/test/directChannels.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
const expect = require('chai').expect
|
||||||
|
const directChannels = require('../directChannels')
|
||||||
|
const Fixtures = require('./fixtures')
|
||||||
|
const FakeDB = require('./fakedb')
|
||||||
|
const sink = require('./sink')
|
||||||
|
const context = require('./context')()
|
||||||
|
|
||||||
|
describe('modules.directChannels', function() {
|
||||||
|
|
||||||
|
before(function() {
|
||||||
|
//
|
||||||
|
// Set up context values
|
||||||
|
//
|
||||||
|
context.values = {
|
||||||
|
users: {
|
||||||
|
'person.one@example.com': {
|
||||||
|
username: 'person.one'
|
||||||
|
},
|
||||||
|
'person.two@example.com': {
|
||||||
|
username: 'person.two'
|
||||||
|
},
|
||||||
|
'person.three@example.com': {
|
||||||
|
username: 'person.three'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
//
|
||||||
|
// Stub the output stream
|
||||||
|
//
|
||||||
|
context.output = sink()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should process direct channel objects', function(done) {
|
||||||
|
//
|
||||||
|
// Set up the DB
|
||||||
|
//
|
||||||
|
context.jabber = new FakeDB(Fixtures.directChannels)
|
||||||
|
//
|
||||||
|
// Process the data
|
||||||
|
//
|
||||||
|
directChannels(context).then(function(c) {
|
||||||
|
expect(c).to.equal(context)
|
||||||
|
expect(c.output.write.args).to.deep.equal(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'direct_channel',
|
||||||
|
direct_channel: {
|
||||||
|
members: ['person.one', 'person.two']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'direct_channel',
|
||||||
|
direct_channel: {
|
||||||
|
members: ['person.one', 'person.three']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
91
lib/modules/test/directPosts.js
Normal file
91
lib/modules/test/directPosts.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
const expect = require('chai').expect
|
||||||
|
const directPosts = require('../directPosts')
|
||||||
|
const Fixtures = require('./fixtures')
|
||||||
|
const FakeDB = require('./fakedb')
|
||||||
|
const sink = require('./sink')
|
||||||
|
const context = require('./context')()
|
||||||
|
|
||||||
|
describe('modules.directPosts', function() {
|
||||||
|
|
||||||
|
before(function() {
|
||||||
|
//
|
||||||
|
// Set up context values
|
||||||
|
//
|
||||||
|
context.values = {
|
||||||
|
users: {
|
||||||
|
'person.one@example.com': {
|
||||||
|
username: 'person.one'
|
||||||
|
},
|
||||||
|
'person.two@example.com': {
|
||||||
|
username: 'person.two'
|
||||||
|
},
|
||||||
|
'person.three@example.com': {
|
||||||
|
username: 'person.three'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
//
|
||||||
|
// Stub the output stream
|
||||||
|
//
|
||||||
|
context.output = sink()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should process direct channel objects', function(done) {
|
||||||
|
//
|
||||||
|
// Set up the DB
|
||||||
|
//
|
||||||
|
context.jabber = new FakeDB(Fixtures.directPosts)
|
||||||
|
//
|
||||||
|
// Process the data
|
||||||
|
//
|
||||||
|
directPosts(context).then(function(c) {
|
||||||
|
expect(c).to.equal(context)
|
||||||
|
expect(c.output.write.args).to.deep.equal([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'direct_post',
|
||||||
|
direct_post: {
|
||||||
|
channel_members: [
|
||||||
|
'person.one', 'person.two'
|
||||||
|
],
|
||||||
|
user: 'person.two',
|
||||||
|
message: 'message 01',
|
||||||
|
create_at: 1497066628513
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'direct_post',
|
||||||
|
direct_post: {
|
||||||
|
channel_members: [
|
||||||
|
'person.one', 'person.three'
|
||||||
|
],
|
||||||
|
user: 'person.three',
|
||||||
|
message: 'message 02',
|
||||||
|
create_at: 1497066628514
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'direct_post',
|
||||||
|
direct_post: {
|
||||||
|
channel_members: [
|
||||||
|
'person.one', 'person.two'
|
||||||
|
],
|
||||||
|
user: 'person.one',
|
||||||
|
message: 'message 03',
|
||||||
|
create_at: 1497066628515
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
])
|
||||||
|
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
14
lib/modules/test/end.js
Normal file
14
lib/modules/test/end.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const expect = require('chai').expect
|
||||||
|
const end = require('../end')
|
||||||
|
const context = require('./context')()
|
||||||
|
|
||||||
|
describe('modules.end', function() {
|
||||||
|
it('should close the output stream', function() {
|
||||||
|
end(context)
|
||||||
|
expect(context.output.end.called).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
context.output.end.reset()
|
||||||
|
})
|
||||||
|
})
|
||||||
50
lib/modules/test/fakedb.js
Normal file
50
lib/modules/test/fakedb.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
//
|
||||||
|
// Implements a fake db for testing
|
||||||
|
//
|
||||||
|
class FakeDB {
|
||||||
|
//
|
||||||
|
// Constructor
|
||||||
|
//
|
||||||
|
constructor(data) {
|
||||||
|
this.data = data
|
||||||
|
this.fetch = this.fetch.bind(this)
|
||||||
|
this.pipe = this.pipe.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Simulate a db connection by just returning
|
||||||
|
// a promise
|
||||||
|
//
|
||||||
|
connect() {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Returns all results in one batch
|
||||||
|
//
|
||||||
|
fetch() {
|
||||||
|
return Promise.resolve({
|
||||||
|
recordset: this.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Delivers results to the specified write
|
||||||
|
// stream
|
||||||
|
//
|
||||||
|
pipe(query, writable) {
|
||||||
|
|
||||||
|
this.data.forEach(function(record) {
|
||||||
|
writable.write(record)
|
||||||
|
})
|
||||||
|
|
||||||
|
writable.end()
|
||||||
|
|
||||||
|
return writable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Export the class
|
||||||
|
//
|
||||||
|
module.exports = FakeDB
|
||||||
15
lib/modules/test/fixtures/channels.js
vendored
Normal file
15
lib/modules/test/fixtures/channels.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
room_jid: 'admin@room.example.com',
|
||||||
|
subject: 'Admin Room',
|
||||||
|
config: '<x type=\'submit\' xmlns=\'jabber:x:data\'><field type=\'boolean\' var=\'persistent\'><value>1</value></field><field type=\'text-single\' var=\'max-persistent-history\'><value>15</value></field><field type=\'boolean\' var=\'open-membership\'><value>1</value></field><field type=\'boolean\' var=\'members-only\'><value>0</value></field><field type=\'boolean\' var=\'allow-gc\'><value>1</value></field><field type=\'boolean\' var=\'anonymous\'><value>1</value></field><field type=\'boolean\' var=\'show-unavailable\'><value>1</value></field><field type=\'list-single\' var=\'invite-role\'><value>participant</value></field><field type=\'boolean\' var=\'password\'><value>0</value></field><field type=\'text-private\' var=\'secret\'><value/></field><field type=\'list-single\' var=\'subject-acl\'><value>moderator</value></field><field type=\'boolean\' var=\'strip-xhtml\'><value>0</value></field><field type=\'boolean\' var=\'moderated-room\'><value>0</value></field></x>',
|
||||||
|
}, {
|
||||||
|
room_jid: 'test\\20room@room.example.com',
|
||||||
|
subject: '',
|
||||||
|
config: '<x type=\'submit\' xmlns=\'jabber:x:data\'><field type=\'boolean\' var=\'persistent\'><value>1</value></field><field type=\'text-single\' var=\'max-persistent-history\'><value>15</value></field><field type=\'boolean\' var=\'open-membership\'><value>1</value></field><field type=\'boolean\' var=\'members-only\'><value>0</value></field><field type=\'boolean\' var=\'allow-gc\'><value>1</value></field><field type=\'boolean\' var=\'anonymous\'><value>0</value></field><field type=\'boolean\' var=\'show-unavailable\'><value>1</value></field><field type=\'list-single\' var=\'invite-role\'><value>participant</value></field><field type=\'boolean\' var=\'password\'><value>0</value></field><field type=\'text-private\' var=\'secret\'><value/></field><field type=\'list-single\' var=\'subject-acl\'><value>moderator</value></field><field type=\'boolean\' var=\'strip-xhtml\'><value>0</value></field><field type=\'boolean\' var=\'moderated-room\'><value>0</value></field></x>',
|
||||||
|
}, {
|
||||||
|
room_jid: 'tonyd_20161206_1432@room.example.com',
|
||||||
|
subject: '',
|
||||||
|
config: '<x type=\'submit\' xmlns=\'jabber:x:data\'><field type=\'boolean\' var=\'persistent\'><value>1</value></field><field type=\'text-single\' var=\'max-persistent-history\'><value>15</value></field><field type=\'boolean\' var=\'open-membership\'><value>1</value></field><field type=\'boolean\' var=\'members-only\'><value>0</value></field><field type=\'boolean\' var=\'allow-gc\'><value>1</value></field><field type=\'boolean\' var=\'anonymous\'><value>0</value></field><field type=\'boolean\' var=\'show-unavailable\'><value>1</value></field><field type=\'list-single\' var=\'invite-role\'><value>participant</value></field><field type=\'boolean\' var=\'password\'><value>0</value></field><field type=\'text-private\' var=\'secret\'><value/></field><field type=\'list-single\' var=\'subject-acl\'><value>moderator</value></field><field type=\'boolean\' var=\'strip-xhtml\'><value>0</value></field><field type=\'boolean\' var=\'moderated-room\'><value>0</value></field></x>',
|
||||||
|
}
|
||||||
|
]
|
||||||
15
lib/modules/test/fixtures/directChannels.js
vendored
Normal file
15
lib/modules/test/fixtures/directChannels.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
to_jid: 'person.one@example.com/rnq1gmague',
|
||||||
|
from_jid: 'person.two@example.com/objc04v73q'
|
||||||
|
}, {
|
||||||
|
to_jid: 'person.one@example.com/rnq1gmague',
|
||||||
|
from_jid: 'person.three@example.com/w32ugtn6ch'
|
||||||
|
}, {
|
||||||
|
to_jid: 'person.two@example.com',
|
||||||
|
from_jid: 'person.one@example.com/blrjlefa3a'
|
||||||
|
}, {
|
||||||
|
to_jid: 'person.two@example.com/lhdms8czsw',
|
||||||
|
from_jid: 'person.one@example.com/blrjlefa3a'
|
||||||
|
}
|
||||||
|
]
|
||||||
27
lib/modules/test/fixtures/directPosts.js
vendored
Normal file
27
lib/modules/test/fixtures/directPosts.js
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
to_jid: 'person.one@example.com/Jabber Messenger Desktop',
|
||||||
|
from_jid: 'person.two@example.com/g1b9b8hs09',
|
||||||
|
sent_date: '2017-06-10T03:50:28.513Z',
|
||||||
|
body_string: 'message 01',
|
||||||
|
body_text: ''
|
||||||
|
}, {
|
||||||
|
to_jid: 'person.one@example.com/Jabber Messenger Desktop',
|
||||||
|
from_jid: 'person.three@example.com/g1b9b8hs09',
|
||||||
|
sent_date: '2017-06-10T03:50:28.514Z',
|
||||||
|
body_string: 'message 02',
|
||||||
|
body_text: ''
|
||||||
|
}, {
|
||||||
|
to_jid: 'person.two@example.com/g1b9b8hs09',
|
||||||
|
from_jid: 'person.one@example.com/Jabber Messenger Desktop',
|
||||||
|
sent_date: '2017-06-10T03:50:28.515Z',
|
||||||
|
body_string: '',
|
||||||
|
body_text: 'message 03'
|
||||||
|
}, {
|
||||||
|
to_jid: 'person.two@example.com/g1b9b8hs09',
|
||||||
|
from_jid: 'person.two@example.com/g1b9b8hs08',
|
||||||
|
sent_date: '2017-06-10T03:50:28.515Z',
|
||||||
|
body_string: 'This should not be exported',
|
||||||
|
body_text: ''
|
||||||
|
}
|
||||||
|
]
|
||||||
13
lib/modules/test/fixtures/index.js
vendored
Normal file
13
lib/modules/test/fixtures/index.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const channels = require('./channels')
|
||||||
|
const users = require('./users')
|
||||||
|
const posts = require('./posts')
|
||||||
|
const directChannels = require('./directChannels')
|
||||||
|
const directPosts = require('./directPosts')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
channels,
|
||||||
|
users,
|
||||||
|
posts,
|
||||||
|
directChannels,
|
||||||
|
directPosts
|
||||||
|
}
|
||||||
43
lib/modules/test/fixtures/posts.js
vendored
Normal file
43
lib/modules/test/fixtures/posts.js
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
module.exports = {
|
||||||
|
|
||||||
|
ok: [{
|
||||||
|
msg_id: '3315',
|
||||||
|
to_jid: 'uat-appsupport@conference.example.com',
|
||||||
|
from_jid: 'micahel.cross@example.com',
|
||||||
|
sent_date: '2017-06-05T20:08:38.263Z',
|
||||||
|
body_string: 'I meant thick',
|
||||||
|
body_text: ''
|
||||||
|
}, {
|
||||||
|
msg_id: '3334',
|
||||||
|
to_jid: 'uat-appsupport@conference.example.com',
|
||||||
|
from_jid: 'micahel.cross@example.com',
|
||||||
|
sent_date: '2017-06-05T20:08:38.263Z',
|
||||||
|
body_string: 'that is when I came on again',
|
||||||
|
body_text: ''
|
||||||
|
}],
|
||||||
|
|
||||||
|
userNotFound: [{
|
||||||
|
msg_id: '3334',
|
||||||
|
to_jid: 'uat-appsupport@conference.example.com',
|
||||||
|
from_jid: 'terrence.flynn@example.com',
|
||||||
|
sent_date: '2017-06-05T20:08:38.263Z',
|
||||||
|
body_string: 'that is when I came on again',
|
||||||
|
body_text: ''
|
||||||
|
}, {
|
||||||
|
msg_id: '3315',
|
||||||
|
to_jid: 'uat-appsupport@conference.example.com',
|
||||||
|
from_jid: 'micahel.cross@example.com',
|
||||||
|
sent_date: '2017-06-05T20:08:38.263Z',
|
||||||
|
body_string: 'I meant thick',
|
||||||
|
body_text: ''
|
||||||
|
}],
|
||||||
|
|
||||||
|
bodyNotFound: [{
|
||||||
|
msg_id: '3315',
|
||||||
|
to_jid: 'uat-appsupport@conference.example.com',
|
||||||
|
from_jid: 'micahel.cross@example.com',
|
||||||
|
sent_date: '2017-06-05T20:08:38.263Z',
|
||||||
|
body_string: '',
|
||||||
|
body_text: ''
|
||||||
|
}]
|
||||||
|
}
|
||||||
57
lib/modules/test/fixtures/users.js
vendored
Normal file
57
lib/modules/test/fixtures/users.js
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
room_jid: 'admin@conference.example.com',
|
||||||
|
real_jid: 'micahel.cross@example.com'
|
||||||
|
}, {
|
||||||
|
room_jid: 'admin@conference.example.com',
|
||||||
|
real_jid: 'sbarclay@example.com'
|
||||||
|
}, {
|
||||||
|
room_jid: 'uat-appsupport@conference.example.com',
|
||||||
|
real_jid: 'anthony.brown@example.com'
|
||||||
|
}, {
|
||||||
|
room_jid: 'uat-appsupport@conference.example.com',
|
||||||
|
real_jid: 'james.seegar1@example.com'
|
||||||
|
}, {
|
||||||
|
room_jid: 'uat-appsupport@conference.example.com',
|
||||||
|
real_jid: 'jarrett.jennings2@example.com'
|
||||||
|
}, {
|
||||||
|
room_jid: 'uat-appsupport@conference.example.com',
|
||||||
|
real_jid: 'micahel.cross@example.com'
|
||||||
|
}, {
|
||||||
|
room_jid: 'uat-appsupport@conference.example.com',
|
||||||
|
real_jid: 'michael.rhoades3@example.com'
|
||||||
|
}, {
|
||||||
|
room_jid: 'uat-appsupport@conference.example.com',
|
||||||
|
real_jid: 'michael.rock@example.com'
|
||||||
|
}, {
|
||||||
|
room_jid: 'uat-appsupport@conference.example.com',
|
||||||
|
real_jid: 'terrence.flynn@example.com'
|
||||||
|
}, {
|
||||||
|
room_jid: 'uat-appsupport@conference.example.com',
|
||||||
|
real_jid: 'tony.dilisio2@example.com'
|
||||||
|
}, {
|
||||||
|
room_jid: 'uat-appsupport@conference.example.com',
|
||||||
|
real_jid: 'william.fleming2@example.com'
|
||||||
|
}, {
|
||||||
|
room_jid: 'test\\20room@conference.example.com',
|
||||||
|
real_jid: 'lt.u755@example.com'
|
||||||
|
}, {
|
||||||
|
room_jid: 'test\\20room@conference.example.com',
|
||||||
|
real_jid: 'lt.u755@example.com/9fyg1zayoi'
|
||||||
|
}, {
|
||||||
|
room_jid: 'the\\20cool\\20room@conference.example.com',
|
||||||
|
real_jid: 'cody.webb@example.com'
|
||||||
|
}, {
|
||||||
|
room_jid: 'tonyd_20161206_1432@conference.example.com',
|
||||||
|
real_jid: 'tony.dilisio2@example.com'
|
||||||
|
}, {
|
||||||
|
room_jid: 'tonyd_20161206_1432@conference.example.com',
|
||||||
|
real_jid: 'julie..sokol@example.com'
|
||||||
|
}, {
|
||||||
|
real_jid: 'mbcross',
|
||||||
|
room_jid: null
|
||||||
|
},{
|
||||||
|
real_jid: 'cross,\\20michael@chat.dhs.gov',
|
||||||
|
room_jid: null
|
||||||
|
}
|
||||||
|
]
|
||||||
131
lib/modules/test/posts.js
Normal file
131
lib/modules/test/posts.js
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
const expect = require('chai').expect
|
||||||
|
const posts = require('../posts')
|
||||||
|
const Fixtures = require('./fixtures')
|
||||||
|
const FakeDB = require('./fakedb')
|
||||||
|
const sink = require('./sink')
|
||||||
|
const context = require('./context')()
|
||||||
|
|
||||||
|
describe('modules.posts', function() {
|
||||||
|
|
||||||
|
before(function() {
|
||||||
|
//
|
||||||
|
// Set up context values
|
||||||
|
//
|
||||||
|
context.values = {
|
||||||
|
team: context.config.define.team,
|
||||||
|
channels: {
|
||||||
|
'uat-appsupport@conference.example.com': {
|
||||||
|
team: 'test',
|
||||||
|
name: 'uat-appsupport',
|
||||||
|
display_name: 'Uat Appsupport',
|
||||||
|
header: '',
|
||||||
|
purpose: '',
|
||||||
|
type: 'P'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
'micahel.cross@example.com': {
|
||||||
|
username: 'micahel.cross',
|
||||||
|
email: 'micahel.cross@example.com',
|
||||||
|
auth_service: 'ldap',
|
||||||
|
teams: [
|
||||||
|
{
|
||||||
|
name: 'test',
|
||||||
|
channels: [
|
||||||
|
{
|
||||||
|
name: 'uat-appsupport'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
//
|
||||||
|
// Stub the output stream
|
||||||
|
//
|
||||||
|
context.output = sink()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should process post objects', function(done) {
|
||||||
|
//
|
||||||
|
// Set up the DB
|
||||||
|
//
|
||||||
|
context.jabber = new FakeDB(Fixtures.posts.ok)
|
||||||
|
//
|
||||||
|
// Process the posts
|
||||||
|
//
|
||||||
|
posts(context).then(function(c) {
|
||||||
|
expect(c).to.equal(context)
|
||||||
|
expect(context.output.write.callCount).to.equal(2)
|
||||||
|
|
||||||
|
let post = c.output.write.args[0][0]
|
||||||
|
expect(post).to.deep.equal({
|
||||||
|
type: 'post',
|
||||||
|
post: {
|
||||||
|
team: 'test',
|
||||||
|
channel: 'uat-appsupport',
|
||||||
|
user: 'micahel.cross',
|
||||||
|
message: 'I meant thick',
|
||||||
|
create_at: 1496693318263
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(new Date(post.post.create_at).toISOString()).to.equal('2017-06-05T20:08:38.263Z')
|
||||||
|
done()
|
||||||
|
}).catch(function(e){
|
||||||
|
expect(e).to.be.undefined
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it('should fail on user not found', function(done) {
|
||||||
|
//
|
||||||
|
// Set up the DB
|
||||||
|
//
|
||||||
|
context.jabber = new FakeDB(Fixtures.posts.userNotFound)
|
||||||
|
//
|
||||||
|
// Process the posts
|
||||||
|
//
|
||||||
|
posts(context).then(function(c) {
|
||||||
|
expect(c).to.equal(context)
|
||||||
|
expect(c.output.write.callCount).to.equal(1)
|
||||||
|
let post = c.output.write.args[0][0]
|
||||||
|
expect(post).to.deep.equal({
|
||||||
|
type: 'post',
|
||||||
|
post: {
|
||||||
|
team: 'test',
|
||||||
|
channel: 'uat-appsupport',
|
||||||
|
user: 'micahel.cross',
|
||||||
|
message: 'I meant thick',
|
||||||
|
create_at: 1496693318263
|
||||||
|
}
|
||||||
|
})
|
||||||
|
done()
|
||||||
|
}).catch(function(e){
|
||||||
|
expect(e).to.be.null
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it('should fail on body not found', function(done) {
|
||||||
|
//
|
||||||
|
// Set up the DB
|
||||||
|
//
|
||||||
|
context.jabber = new FakeDB(Fixtures.posts.bodyNotFound)
|
||||||
|
//
|
||||||
|
// Process the posts
|
||||||
|
//
|
||||||
|
posts(context).then(function(c) {
|
||||||
|
expect(c).to.equal(context)
|
||||||
|
expect(c.output.write.callCount).to.equal(0)
|
||||||
|
done()
|
||||||
|
}).catch(function(e){
|
||||||
|
expect(e).to.be.null
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
18
lib/modules/test/sink.js
Normal file
18
lib/modules/test/sink.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const { spy } = require('sinon')
|
||||||
|
const { Writable } = require('stream')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Returns a spied writable stream
|
||||||
|
//
|
||||||
|
module.exports = function() {
|
||||||
|
|
||||||
|
var writable = new Writable({
|
||||||
|
objectMode: true,
|
||||||
|
write(chunk, encoding, callback) {
|
||||||
|
return callback()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
spy(writable, 'write')
|
||||||
|
|
||||||
|
return writable
|
||||||
|
}
|
||||||
30
lib/modules/test/start.js
Normal file
30
lib/modules/test/start.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const expect = require('chai').expect
|
||||||
|
const fs = require('fs')
|
||||||
|
const start = require('../start')
|
||||||
|
const context = require('./context')()
|
||||||
|
|
||||||
|
describe('modules.start', function() {
|
||||||
|
var resultHandler = function(err) {
|
||||||
|
if(err) {
|
||||||
|
console.log('unlink failed', err)
|
||||||
|
} else {
|
||||||
|
console.log('file deleted')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should set up the source and output', function(done) {
|
||||||
|
start(context)
|
||||||
|
.then(function (c) {
|
||||||
|
expect(c).to.equal(context)
|
||||||
|
expect(fs.existsSync(context.config.target.filename)).to.be.true
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
.catch(function(err){
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
fs.unlink(context.config.target.filename, resultHandler)
|
||||||
|
})
|
||||||
|
})
|
||||||
27
lib/modules/test/team.js
Normal file
27
lib/modules/test/team.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const expect = require('chai').expect
|
||||||
|
const team = require('../team')
|
||||||
|
const context = require('./context')()
|
||||||
|
|
||||||
|
describe('modules.team', function() {
|
||||||
|
it('should write a team object', function(done) {
|
||||||
|
team(context)
|
||||||
|
.then(function (c) {
|
||||||
|
expect(c).to.equal(context)
|
||||||
|
expect(c.output.write.args[0][0]).to.deep.equal({
|
||||||
|
type: 'team',
|
||||||
|
team: {
|
||||||
|
name: 'test',
|
||||||
|
display_name: 'Test Team',
|
||||||
|
description: 'Our Test Team',
|
||||||
|
type: 'I',
|
||||||
|
allow_open_invite: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
context.output.write.reset()
|
||||||
|
})
|
||||||
|
})
|
||||||
88
lib/modules/test/users.js
Normal file
88
lib/modules/test/users.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
const expect = require('chai').expect
|
||||||
|
const users = require('../users')
|
||||||
|
const Fixtures = require('./fixtures')
|
||||||
|
const context = require('./context')()
|
||||||
|
|
||||||
|
describe('modules.users', function() {
|
||||||
|
|
||||||
|
before(function(){
|
||||||
|
context.values = {
|
||||||
|
team: context.config.define.team,
|
||||||
|
channels: {
|
||||||
|
'admin@conference.example.com': {
|
||||||
|
team: 'test',
|
||||||
|
name: 'admin',
|
||||||
|
display_name: 'Admin',
|
||||||
|
header: 'Admin Test room',
|
||||||
|
purpose: 'Admin Test room',
|
||||||
|
type: 'O'
|
||||||
|
},
|
||||||
|
'uat-appsupport@conference.example.com': {
|
||||||
|
team: 'hsin',
|
||||||
|
name: 'uat-appsupport',
|
||||||
|
display_name: 'Uat Appsupport',
|
||||||
|
header: '',
|
||||||
|
purpose: '',
|
||||||
|
type: 'P'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should process user objects', function(done) {
|
||||||
|
|
||||||
|
context.jabber.fetch.returns(Promise.resolve({recordset: Fixtures.users}))
|
||||||
|
|
||||||
|
users(context).then(function(c) {
|
||||||
|
expect(c).to.equal(context)
|
||||||
|
expect(Object.keys(c.values.users).length).equals(13)
|
||||||
|
expect(c.output.write.args[0][0]).to.deep.equal({
|
||||||
|
type: 'user',
|
||||||
|
user: {
|
||||||
|
username: 'micahel.cross',
|
||||||
|
email: 'micahel.cross@example.com',
|
||||||
|
auth_service: 'ldap',
|
||||||
|
auth_data: 'MICAHEL.CROSS',
|
||||||
|
teams: [
|
||||||
|
{
|
||||||
|
name: 'test',
|
||||||
|
channels: [
|
||||||
|
{
|
||||||
|
name: 'admin'
|
||||||
|
}, {
|
||||||
|
name: 'uat-appsupport'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(c.output.write.args[1][0]).to.deep.equal({
|
||||||
|
type: 'user',
|
||||||
|
user: {
|
||||||
|
username: 'sbarclay',
|
||||||
|
email: 'sbarclay@example.com',
|
||||||
|
auth_service: 'ldap',
|
||||||
|
auth_data: 'SBARCLAY',
|
||||||
|
teams: [
|
||||||
|
{
|
||||||
|
name: 'test',
|
||||||
|
channels: [
|
||||||
|
{
|
||||||
|
name: 'admin'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
context.jabber.fetch.reset()
|
||||||
|
context.output.write.reset()
|
||||||
|
})
|
||||||
|
})
|
||||||
21
lib/modules/test/version.js
Normal file
21
lib/modules/test/version.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const expect = require('chai').expect
|
||||||
|
const version = require('../version')
|
||||||
|
const context = require('./context')()
|
||||||
|
|
||||||
|
describe('modules.version', function() {
|
||||||
|
it('should write a version object', function(done) {
|
||||||
|
version(context)
|
||||||
|
.then(function (c) {
|
||||||
|
expect(c).to.equal(context)
|
||||||
|
expect(c.output.write.args[0][0]).to.deep.equal({
|
||||||
|
type: 'version',
|
||||||
|
version: 1
|
||||||
|
})
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
context.output.write.reset()
|
||||||
|
})
|
||||||
|
})
|
||||||
9
lib/modules/transform.js
Normal file
9
lib/modules/transform.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const { Transform } = require('stream')
|
||||||
|
|
||||||
|
module.exports = function(transform, callback) {
|
||||||
|
return new Transform({
|
||||||
|
readableObjectMode: true,
|
||||||
|
writableObjectMode: true,
|
||||||
|
transform
|
||||||
|
}).on('finish', callback)
|
||||||
|
}
|
||||||
115
lib/modules/users.js
Normal file
115
lib/modules/users.js
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
const _ = require('lodash')
|
||||||
|
const Factory = require('../factory')
|
||||||
|
const Utils = require('./utils')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initialize the child logger for
|
||||||
|
// the module
|
||||||
|
//
|
||||||
|
const log = require('../log').child({
|
||||||
|
module: 'users'
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = function(context) {
|
||||||
|
//
|
||||||
|
// Select all of the users
|
||||||
|
//
|
||||||
|
const where = 'msg_type = \'c\''
|
||||||
|
|
||||||
|
return context.jabber.fetch(`
|
||||||
|
SELECT real_jid, room_jid FROM dbo.tc_users
|
||||||
|
UNION ALL (
|
||||||
|
SELECT from_jid AS real_jid, NULL AS room_jid FROM dbo.jm WHERE ${where}
|
||||||
|
UNION
|
||||||
|
SELECT to_jid AS real_jid, NULL AS room_jid FROM dbo.jm WHERE ${where}
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
//
|
||||||
|
// Build up the user objects and then
|
||||||
|
// write them to the output
|
||||||
|
//
|
||||||
|
.then(function(results) {
|
||||||
|
log.info(`${results.recordset.length} records found`)
|
||||||
|
//
|
||||||
|
// Map of users
|
||||||
|
//
|
||||||
|
var users = {}
|
||||||
|
//
|
||||||
|
// Iterate over the record set and
|
||||||
|
// assemble the user objects
|
||||||
|
//
|
||||||
|
results.recordset.forEach(function(record) {
|
||||||
|
log.debug(record)
|
||||||
|
//
|
||||||
|
// Clean the real_jid to ensure we don't
|
||||||
|
// have duplicates with /<string> suffixes
|
||||||
|
//
|
||||||
|
var real_jid = Utils.realJID(record.real_jid)
|
||||||
|
//
|
||||||
|
// Generate the username fro the real_jid
|
||||||
|
//
|
||||||
|
var username = toUsername(real_jid)
|
||||||
|
//
|
||||||
|
// Return a reference to the user in the user map
|
||||||
|
// or add one if it doesn't yet exist
|
||||||
|
//
|
||||||
|
var user = users[real_jid] = _.get(users, real_jid, {
|
||||||
|
username,
|
||||||
|
email: real_jid,
|
||||||
|
auth_service: context.config.define.user.auth_service,
|
||||||
|
auth_data: username.toUpperCase(),
|
||||||
|
teams: [{
|
||||||
|
name: context.values.team.name,
|
||||||
|
channels: []
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
//
|
||||||
|
// Look up the channel based on the
|
||||||
|
// room id. For direct messages, the room_jid
|
||||||
|
// will be null.
|
||||||
|
//
|
||||||
|
var channel = context.values.channels[record.room_jid]
|
||||||
|
//
|
||||||
|
// Add it to the user
|
||||||
|
//
|
||||||
|
if (channel) {
|
||||||
|
user.teams[0].channels = _.unionBy(user.teams[0].channels, [{
|
||||||
|
name: channel.name
|
||||||
|
}], 'name')
|
||||||
|
} else {
|
||||||
|
record.room_jid && log.warn(`... channel not found for ${record.room_jid}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//
|
||||||
|
// Now that the users are assembled, write
|
||||||
|
// them to the output
|
||||||
|
//
|
||||||
|
_.forEach(users, function(user, key) {
|
||||||
|
try {
|
||||||
|
context.output.write(
|
||||||
|
Factory.user(user)
|
||||||
|
)
|
||||||
|
log.info(`... writing ${user.username}`)
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
log.error(`... ignoring ${user.username} on error: ${err.message}.`)
|
||||||
|
delete users[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//
|
||||||
|
// Add the users map to the context
|
||||||
|
//
|
||||||
|
context.values.users = users
|
||||||
|
//
|
||||||
|
// Return the context
|
||||||
|
//
|
||||||
|
return context
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Parse the username
|
||||||
|
//
|
||||||
|
const toUsername = function (jid='') {
|
||||||
|
return jid.split('@')[0]
|
||||||
|
}
|
||||||
103
lib/modules/utils.js
Normal file
103
lib/modules/utils.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
const _ = require('lodash')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Declare utils object
|
||||||
|
//
|
||||||
|
const utils = {}
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
utils.chunk = function(body) {
|
||||||
|
//
|
||||||
|
// Use regex to create an array
|
||||||
|
// of strings of max length
|
||||||
|
//
|
||||||
|
return body.match(/[\s\S]{1,4000}/g)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Removes trailing /<string> suffixes that
|
||||||
|
// may exist in the user ids. This happens
|
||||||
|
// if a user logs in to jabber more than
|
||||||
|
// once at the same time
|
||||||
|
//
|
||||||
|
utils.realJID = function (jid='') {
|
||||||
|
return jid.split('/')[0]
|
||||||
|
.replace(/\\20+/g, '.')
|
||||||
|
.replace(/\.\.+/, '.')
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lookup values
|
||||||
|
//
|
||||||
|
utils.lookup = function (type, map, key) {
|
||||||
|
var found = map[key]
|
||||||
|
|
||||||
|
if(!found) {
|
||||||
|
throw new Error(`${type} ${key} not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Obtain the username from a jid
|
||||||
|
//
|
||||||
|
utils.username = function (users, jid) {
|
||||||
|
return utils.lookup('user', users, utils.realJID(jid)).username
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Obtain the channel name from a jid
|
||||||
|
//
|
||||||
|
utils.channelName = function (channels, jid) {
|
||||||
|
return utils.lookup('channel', channels, jid).name
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Find the message body
|
||||||
|
//
|
||||||
|
utils.body = function (message) {
|
||||||
|
var body = message.body_string || message.body_text
|
||||||
|
|
||||||
|
if(!body) {
|
||||||
|
throw new Error(`message ${message.msg_id} body is empty`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.chunk(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Coverts the to / from JIDs to a members
|
||||||
|
// array
|
||||||
|
//
|
||||||
|
utils.members = function(users, to, from) {
|
||||||
|
//
|
||||||
|
// We use uniq to remove duplicate
|
||||||
|
// members in the same channel
|
||||||
|
//
|
||||||
|
return _.uniq(_.sortBy([
|
||||||
|
utils.username(users, to),
|
||||||
|
utils.username(users, from),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Checks if the members list is valid
|
||||||
|
//
|
||||||
|
utils.membersAreValid = function(members) {
|
||||||
|
return _.isArray(members) && members.length > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Convert ISO to millis
|
||||||
|
//
|
||||||
|
utils.millis = function(date) {
|
||||||
|
return new Date(date).getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Export the functions
|
||||||
|
//
|
||||||
|
module.exports = utils
|
||||||
12
lib/modules/version.js
Normal file
12
lib/modules/version.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const Factory = require('../factory')
|
||||||
|
|
||||||
|
module.exports = function(context) {
|
||||||
|
//
|
||||||
|
// Write the version object
|
||||||
|
//
|
||||||
|
context.output.write(Factory.version())
|
||||||
|
//
|
||||||
|
// Return a resolved promise
|
||||||
|
//
|
||||||
|
return Promise.resolve(context)
|
||||||
|
}
|
||||||
28
lib/mongo.js
Normal file
28
lib/mongo.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const mongo = require('mongodb')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Declare internals
|
||||||
|
//
|
||||||
|
let client = {}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Connect to the db and create a connection
|
||||||
|
// pool
|
||||||
|
//
|
||||||
|
module.exports.connect = function (config) {
|
||||||
|
client = new mongo.MongoClient(config.source.uri)
|
||||||
|
return client.connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.close = function () {
|
||||||
|
return client.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.collection = function (collection) {
|
||||||
|
return client.db().collection(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.gridFsBucket = function (bucketName) {
|
||||||
|
return new mongo.GridFSBucket(client.db(), { bucketName })
|
||||||
|
}
|
||||||
|
|
||||||
57
lib/mssql.js
Normal file
57
lib/mssql.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
const SQL = require('mssql')
|
||||||
|
const log = require('./log')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Declare internals
|
||||||
|
//
|
||||||
|
const internals = {}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Connect to the db and create a connection
|
||||||
|
// pool
|
||||||
|
//
|
||||||
|
module.exports.connect = function(config) {
|
||||||
|
return SQL.connect(config.source.uri)
|
||||||
|
.then(function(pool) {
|
||||||
|
internals.pool = pool
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Returns all results in one batch
|
||||||
|
//
|
||||||
|
module.exports.fetch = function(query) {
|
||||||
|
return internals.pool.request().query(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Delivers results to the specified write
|
||||||
|
// stream. Use this for large result sets.
|
||||||
|
//
|
||||||
|
module.exports.pipe = function(query, writable) {
|
||||||
|
//
|
||||||
|
// Create the request
|
||||||
|
//
|
||||||
|
const request = internals.pool.request()
|
||||||
|
//
|
||||||
|
// Attach the writable stream
|
||||||
|
//
|
||||||
|
request.pipe(writable)
|
||||||
|
//
|
||||||
|
// Submit the query
|
||||||
|
//
|
||||||
|
request.query(query)
|
||||||
|
//
|
||||||
|
// Return the writable stream for
|
||||||
|
// chaining
|
||||||
|
//
|
||||||
|
return writable
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Set up an error handler
|
||||||
|
//
|
||||||
|
SQL.on('error', function(err) {
|
||||||
|
log.error(err)
|
||||||
|
throw(err)
|
||||||
|
})
|
||||||
55
lib/rocketchat/channels.js
Normal file
55
lib/rocketchat/channels.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
const _ = require('lodash')
|
||||||
|
const Factory = require('../factory')
|
||||||
|
const slug = require("slug");
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initialize the child logger for
|
||||||
|
// the module
|
||||||
|
//
|
||||||
|
const log = require('../log').child({
|
||||||
|
module: 'channels'
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = async function (context) {
|
||||||
|
const collection = context.rocketchat.roomsCollection()
|
||||||
|
const query = { t: { $in: ['c', 'p'] } }
|
||||||
|
|
||||||
|
let channels = {}
|
||||||
|
const cursor = collection.find(query)
|
||||||
|
const mergeDiscussions = _.get(context, 'config.define.channels.mergeDiscussionIntoParent', false)
|
||||||
|
const discussions = {}
|
||||||
|
while (await cursor.hasNext()) {
|
||||||
|
const result = await cursor.next()
|
||||||
|
if (mergeDiscussions && result.prid) {
|
||||||
|
discussions[result._id] = result.prid
|
||||||
|
log.info(`... skipping channel ${result._id} to merge discussion into parent ${result.prid}`)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the channel and add it to the context
|
||||||
|
let channel = channels[result._id] = {
|
||||||
|
team: context.values.team.name,
|
||||||
|
name: slug(_.toLower(result.name)),
|
||||||
|
display_name: result.name,
|
||||||
|
header: result.topic,
|
||||||
|
purpose: result.description,
|
||||||
|
type: result.t === 'p' ? 'P' : 'O'
|
||||||
|
}
|
||||||
|
const map = _.get(context, `config.define.channels.map.${result.name}`, false)
|
||||||
|
if (map) {
|
||||||
|
channel.name = map.name
|
||||||
|
channel.display_name = map.display_name
|
||||||
|
}
|
||||||
|
// Write the channel data to the output
|
||||||
|
log.info(`... writing ${channel.name} (${result.name})`)
|
||||||
|
context.output.write(
|
||||||
|
Factory.channel(channel)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!context.values.discussions) {
|
||||||
|
context.values.discussions = {}
|
||||||
|
}
|
||||||
|
context.values.discussions = Object.assign(context.values.discussions, discussions)
|
||||||
|
context.values.channels = channels
|
||||||
|
return context
|
||||||
|
}
|
||||||
60
lib/rocketchat/directChannels.js
Normal file
60
lib/rocketchat/directChannels.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
const Factory = require('../factory')
|
||||||
|
const Utils = require('./utils')
|
||||||
|
const _ = require("lodash");
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initialize the child logger for
|
||||||
|
// the module
|
||||||
|
//
|
||||||
|
const log = require('../log').child({
|
||||||
|
module: 'directChannels'
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = async function(context) {
|
||||||
|
// Select all of the rooms
|
||||||
|
const collection = context.rocketchat.roomsCollection()
|
||||||
|
const query = { t: 'd' }
|
||||||
|
|
||||||
|
const directChannels = {}
|
||||||
|
const cursor = collection.find(query)
|
||||||
|
const mergeDiscussions = _.get(context, 'config.define.channels.mergeDiscussionIntoParent', false)
|
||||||
|
const discussions = {}
|
||||||
|
while (await cursor.hasNext()) {
|
||||||
|
const result = await cursor.next()
|
||||||
|
if (mergeDiscussions && result.prid) {
|
||||||
|
discussions[result._id] = result.prid
|
||||||
|
log.info(`... skipping direct channel ${result._id} to merge discussion into parent ${result.prid}`)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Generate the members array for the direct channel
|
||||||
|
let members = Utils.members(
|
||||||
|
context.values.users,
|
||||||
|
result.uids,
|
||||||
|
)
|
||||||
|
if (members.length === 1) {
|
||||||
|
members.push(members[0])
|
||||||
|
}
|
||||||
|
// If there at least two members
|
||||||
|
if (Utils.membersAreValid(members)) {
|
||||||
|
log.info(`... writing members (${members.join(', ')})`)
|
||||||
|
let channel = directChannels[result._id] = {
|
||||||
|
header: result.topic,
|
||||||
|
members
|
||||||
|
}
|
||||||
|
context.output.write(
|
||||||
|
Factory.directChannel(channel)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.error(`... ignoring directChannel members (${result.usernames.join(', ')}) on error: ${err.message}.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!context.values.discussions) {
|
||||||
|
context.values.discussions = {}
|
||||||
|
}
|
||||||
|
context.values.discussions = Object.assign(context.values.discussions, discussions)
|
||||||
|
context.values.directChannels = directChannels
|
||||||
|
return context
|
||||||
|
}
|
||||||
75
lib/rocketchat/emoji.js
Normal file
75
lib/rocketchat/emoji.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
const Factory = require('../factory')
|
||||||
|
const Utils = require('./utils')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initialize the child logger for
|
||||||
|
// the module
|
||||||
|
//
|
||||||
|
const log = require('../log').child({
|
||||||
|
module: 'custom emoji'
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = async function (context) {
|
||||||
|
const collection = context.rocketchat.emojiCollection()
|
||||||
|
const fileCollection = context.rocketchat.collection('custom_emoji')
|
||||||
|
|
||||||
|
const cursor = collection.find()
|
||||||
|
while (await cursor.hasNext()) {
|
||||||
|
const result = await cursor.next()
|
||||||
|
|
||||||
|
// Prepare destination file
|
||||||
|
const filename = `${result.name}.${result.extension}`
|
||||||
|
const dest = `${context.config.target.filesPath}/custom_emoji/${filename}`
|
||||||
|
// Download emoji file
|
||||||
|
if (result.store.startsWith('FileSystem:')) {
|
||||||
|
const src = context.config.source.customEmojiPath
|
||||||
|
const srcFilename = Utils.srcPath(src, filename)
|
||||||
|
if (!srcFilename) {
|
||||||
|
return new Error(`source file "${filename}" not found`)
|
||||||
|
}
|
||||||
|
// Copy to output dir
|
||||||
|
Utils.copyFile(srcFilename, dest)
|
||||||
|
} else if (result.store.startsWith('GridFS:')) {
|
||||||
|
await Utils.downloadGridFS(context, fileCollection, filename, dest)
|
||||||
|
} else {
|
||||||
|
throw new Error(`file system ${file.store} is not supported. Migrate to FileSystem first, see readme.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export custom emoji
|
||||||
|
let emoji = {
|
||||||
|
name: result.name,
|
||||||
|
image: dest,
|
||||||
|
}
|
||||||
|
log.info(`... writing ${emoji.name}`)
|
||||||
|
context.output.write(
|
||||||
|
Factory.emoji(emoji)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportFile = async function (context, collection, fileId) {
|
||||||
|
const type = collection.collectionName
|
||||||
|
const file = await collection.findOne({ _id: fileId })
|
||||||
|
if (!file) {
|
||||||
|
throw new Error(`file ${fileId} from collection ${type} is missing`)
|
||||||
|
}
|
||||||
|
const dest = context.config.target.filesPath
|
||||||
|
const destFilename = utils.destPath(dest, collection.collectionName, file)
|
||||||
|
|
||||||
|
if (file.store.startsWith('FileSystem:')) {
|
||||||
|
const src = context.config.source.uploadsPath
|
||||||
|
const srcFilename = utils.srcPath(src, file._id)
|
||||||
|
if (!srcFilename) {
|
||||||
|
return new Error(`source file "${file._id}" not found`)
|
||||||
|
}
|
||||||
|
// Copy to output dir
|
||||||
|
utils.copyFile(srcFilename, destFilename)
|
||||||
|
} else if (file.store.startsWith('GridFS:')) {
|
||||||
|
await utils.downloadGridFS(context, collection, file._id, destFilename)
|
||||||
|
} else {
|
||||||
|
throw new Error(`file system ${file.store} is not supported. Migrate to FileSystem first, see readme.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return destFilename
|
||||||
|
}
|
||||||
13
lib/rocketchat/end.js
Normal file
13
lib/rocketchat/end.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//
|
||||||
|
// Initialize the child logger for
|
||||||
|
// the module
|
||||||
|
//
|
||||||
|
const log = require('../log').child({
|
||||||
|
module: 'end'
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = function(context) {
|
||||||
|
log.info('end')
|
||||||
|
context.rocketchat.close()
|
||||||
|
context.output.end()
|
||||||
|
}
|
||||||
21
lib/rocketchat/index.js
Normal file
21
lib/rocketchat/index.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const start = require('./start')
|
||||||
|
const version = require('./version')
|
||||||
|
const emoji = require('./emoji')
|
||||||
|
const team = require('./team')
|
||||||
|
const channels = require('./channels')
|
||||||
|
const users = require('./users')
|
||||||
|
const posts = require('./posts')
|
||||||
|
const directChannels = require('./directChannels')
|
||||||
|
const end = require('./end')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
start,
|
||||||
|
version,
|
||||||
|
emoji,
|
||||||
|
team,
|
||||||
|
channels,
|
||||||
|
users,
|
||||||
|
posts,
|
||||||
|
directChannels,
|
||||||
|
end
|
||||||
|
}
|
||||||
162
lib/rocketchat/posts.js
Normal file
162
lib/rocketchat/posts.js
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
const Factory = require('../factory')
|
||||||
|
const Utils = require('./utils')
|
||||||
|
const _ = require('lodash')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initialize the child logger for
|
||||||
|
// the module
|
||||||
|
//
|
||||||
|
const log = require('../log').child({
|
||||||
|
module: 'posts'
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = async function (context) {
|
||||||
|
const collection = context.rocketchat.messagesCollection()
|
||||||
|
// Keep track of the number of posts written for logging
|
||||||
|
let written = 0
|
||||||
|
|
||||||
|
let memReplyIds = {}
|
||||||
|
const total = await collection.count()
|
||||||
|
const cursor = collection.find()
|
||||||
|
while (await cursor.hasNext()) {
|
||||||
|
const result = await cursor.next()
|
||||||
|
let posts = await collectPostData(context, result, memReplyIds, false)
|
||||||
|
posts.forEach(function(post) {
|
||||||
|
context.output.write(post.isDirect ? Factory.directPost(post) : Factory.post(post))
|
||||||
|
})
|
||||||
|
// Log progress periodically
|
||||||
|
written += posts.length
|
||||||
|
if (written % 1000 == 0) {
|
||||||
|
log.info(`... wrote ${written} posts`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info(`... finished exporting ${written} posts, ignored ${total - written}`)
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectPostData(context, result, memReplyIds, isReply) {
|
||||||
|
if (memReplyIds.hasOwnProperty(result._id)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to get discussion channel id
|
||||||
|
let channelId = result.rid
|
||||||
|
const mergeDiscussions = _.get(context, 'config.define.channels.mergeDiscussionIntoParent', false)
|
||||||
|
const parentId = _.get(context, `values.discussions.${channelId}`, false)
|
||||||
|
if (mergeDiscussions && parentId) {
|
||||||
|
channelId = parentId
|
||||||
|
}
|
||||||
|
// Check if direct message
|
||||||
|
let isDirect = !context.values.channels[channelId] && !!context.values.directChannels[channelId]
|
||||||
|
let post = {}
|
||||||
|
let channelInfo = {}
|
||||||
|
|
||||||
|
if (isDirect) {
|
||||||
|
let { members: channel_members } = context.values.directChannels[channelId]
|
||||||
|
// Ensure we have at least two channel members before we can write the message
|
||||||
|
if (Utils.membersAreValid(channel_members)) {
|
||||||
|
channelInfo = { channel_members }
|
||||||
|
} else {
|
||||||
|
log.error(`... ignoring message id:${result._id} on error: directChannel ${channelId} not found.`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channelInfo = {
|
||||||
|
team: context.values.team.name,
|
||||||
|
channel: Utils.channelName(
|
||||||
|
context.values.channels, channelId
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isReply) {
|
||||||
|
Object.assign(post, channelInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
const reactions = Object.keys(result.reactions || {}).reduce((prev, code) => {
|
||||||
|
return result.reactions[code].usernames.map((u) => {
|
||||||
|
return {
|
||||||
|
user: u,
|
||||||
|
emoji_name: _.trim(code, ':'),
|
||||||
|
create_at: Utils.millis(result.ts),
|
||||||
|
}
|
||||||
|
}).concat(prev)
|
||||||
|
}, [])
|
||||||
|
const flagged_by = (result.starred || []).map(({ _id: uid }) => {
|
||||||
|
try {
|
||||||
|
return Utils.username(context.values.users, uid)
|
||||||
|
} catch (err) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}).filter(v => v)
|
||||||
|
|
||||||
|
Object.assign(post, {
|
||||||
|
user: Utils.username(
|
||||||
|
context.values.users, result.u._id
|
||||||
|
),
|
||||||
|
create_at: Utils.millis(result.ts),
|
||||||
|
reactions,
|
||||||
|
flagged_by,
|
||||||
|
isDirect,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Collect data from attachments
|
||||||
|
let attachments = Utils.processAttachments(context, result)
|
||||||
|
let file
|
||||||
|
let body = result.msg
|
||||||
|
|
||||||
|
await Promise.all(attachments.map(async (a) => {
|
||||||
|
if (a.type === 'file') {
|
||||||
|
file = await a.data
|
||||||
|
} else if (a.type === 'quote') {
|
||||||
|
body = a.data
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
if (file && file instanceof Error) {
|
||||||
|
throw file
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file && file.description) {
|
||||||
|
body = `File description: \n${file.description} \n\n ${body}`
|
||||||
|
}
|
||||||
|
const chunks = body ? Utils.body(body) : [body]
|
||||||
|
let posts = chunks.map((chunk) => {
|
||||||
|
return Object.assign({}, post, {
|
||||||
|
message: chunk
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!posts[0]) {
|
||||||
|
throw new Error(`Post is empty`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
posts[0].attachments = [{
|
||||||
|
path: file.path
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
const replies = []
|
||||||
|
if (!isReply) {
|
||||||
|
const collection = context.rocketchat.messagesCollection()
|
||||||
|
const cursor = collection.find({tmid: result._id })
|
||||||
|
while (await cursor.hasNext()) {
|
||||||
|
const reply = await cursor.next()
|
||||||
|
const replyData = await collectPostData(context, reply, memReplyIds, true)
|
||||||
|
replyData.forEach(r => replies.push(r))
|
||||||
|
memReplyIds[reply._id] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(posts[0], {
|
||||||
|
reactions,
|
||||||
|
flagged_by,
|
||||||
|
replies,
|
||||||
|
})
|
||||||
|
|
||||||
|
return posts
|
||||||
|
} catch (err) {
|
||||||
|
log.error(`... ignoring message id:${result._id} on error: ${err.message}.`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
32
lib/rocketchat/start.js
Normal file
32
lib/rocketchat/start.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const datafile = require('../datafile')
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initialize the child logger for
|
||||||
|
// the module
|
||||||
|
//
|
||||||
|
const log = require('../log').child({
|
||||||
|
module: 'start'
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = async function(context) {
|
||||||
|
log.info('preparing files paths')
|
||||||
|
const dest = context.config.target.filesPath
|
||||||
|
if (!dest || !fs.existsSync(dest)) {
|
||||||
|
fs.mkdirSync(dest)
|
||||||
|
}
|
||||||
|
if (!fs.lstatSync(dest).isDirectory() || fs.accessSync(dest, fs.constants.W_OK)) {
|
||||||
|
throw new Error(`Directory "${dest} is not writable"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('connecting to rocketchat')
|
||||||
|
await context.rocketchat.connect(context.config)
|
||||||
|
log.info(`creating file '${context.config.target.filename}'`)
|
||||||
|
// Create the datafile and add it to the context
|
||||||
|
context.output = datafile(
|
||||||
|
context.config.target.filename,
|
||||||
|
process.exit
|
||||||
|
)
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
29
lib/rocketchat/team.js
Normal file
29
lib/rocketchat/team.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const Factory = require('../factory')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initialize the child logger for
|
||||||
|
// the module
|
||||||
|
//
|
||||||
|
const log = require('../log').child({
|
||||||
|
module: 'team'
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = function(context) {
|
||||||
|
log.info(`writing team '${context.config.define.team.name}'`)
|
||||||
|
//
|
||||||
|
// Store the team object for use
|
||||||
|
// by other modules
|
||||||
|
//
|
||||||
|
context.values.team = context.config.define.team
|
||||||
|
//
|
||||||
|
// Write the team object to the
|
||||||
|
// output
|
||||||
|
//
|
||||||
|
context.output.write(Factory.team(
|
||||||
|
context.config.define.team
|
||||||
|
))
|
||||||
|
//
|
||||||
|
// Return a resolved promise
|
||||||
|
//
|
||||||
|
return Promise.resolve(context)
|
||||||
|
}
|
||||||
105
lib/rocketchat/users.js
Normal file
105
lib/rocketchat/users.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
const Factory = require('../factory')
|
||||||
|
const Utils = require('./utils')
|
||||||
|
const { Gitlab } = require('@gitbeaker/node')
|
||||||
|
const _ = require('lodash')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initialize the child logger for
|
||||||
|
// the module
|
||||||
|
//
|
||||||
|
const log = require('../log').child({
|
||||||
|
module: 'users'
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = async function(context) {
|
||||||
|
const collection = context.rocketchat.usersCollection()
|
||||||
|
|
||||||
|
let users = {}
|
||||||
|
const cursor = collection.find({ type: 'user' })
|
||||||
|
while (await cursor.hasNext()) {
|
||||||
|
const result = await cursor.next()
|
||||||
|
const id = result._id
|
||||||
|
const [first_name, last_name] = result.name.split(' ')
|
||||||
|
try {
|
||||||
|
const ldapAuthService = _.get(context, 'config.define.user.ldap_auth_service', '')
|
||||||
|
const roleMap = _.get(context, 'config.define.user.globalRoleMap', {})
|
||||||
|
const auth_service = result.ldap ? ldapAuthService : ''
|
||||||
|
if (result.roles.indexOf('admin') !== -1 && result.roles.indexOf('user') === -1) {
|
||||||
|
result.roles.push('user')
|
||||||
|
}
|
||||||
|
const highlights = _.get(result, 'settings.preferences.highlights', [])
|
||||||
|
let user = users[id] = {
|
||||||
|
username: result.username,
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
auth_service,
|
||||||
|
auth_data: await getAuthData(context, result, auth_service),
|
||||||
|
email: result.emails[0].address,
|
||||||
|
roles: result.roles.sort().reduce((roles, role) => {
|
||||||
|
if (roleMap.hasOwnProperty(role)) {
|
||||||
|
roles.push(roleMap[role])
|
||||||
|
}
|
||||||
|
return roles
|
||||||
|
}, []).join(' '),
|
||||||
|
teams: [{
|
||||||
|
name: context.values.team.name,
|
||||||
|
channels: result.__rooms.map((id) => {
|
||||||
|
const name = _.get(context, `values.channels.${id}.name`, false)
|
||||||
|
return name ? { name } : undefined
|
||||||
|
}).filter(v => v)
|
||||||
|
}],
|
||||||
|
notify_props: {
|
||||||
|
mention_keys: highlights.map(s => s.replace('@', '')).join(','),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (auth_service) {
|
||||||
|
user.auth_service = auth_service
|
||||||
|
user.auth_data = await getAuthData(context, result, auth_service)
|
||||||
|
}
|
||||||
|
if (!user.roles.length) {
|
||||||
|
delete user.roles
|
||||||
|
}
|
||||||
|
const avatar = await getAvatar(context, result)
|
||||||
|
if (avatar) {
|
||||||
|
user.profile_image = avatar
|
||||||
|
}
|
||||||
|
|
||||||
|
context.output.write(
|
||||||
|
Factory.user(user)
|
||||||
|
)
|
||||||
|
log.info(`... writing ${user.username}`)
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
log.error(`... ignoring ${result.username} on error: ${err.message}.`)
|
||||||
|
delete users[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.values.users = users
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAuthData(context, user, auth_service) {
|
||||||
|
switch (auth_service) {
|
||||||
|
case 'gitlab':
|
||||||
|
const {host, token} = _.get(context, 'config.define.user.gitlab')
|
||||||
|
const api = new Gitlab({ host, token });
|
||||||
|
const gitlabUser = await api.Users.username(user.username);
|
||||||
|
if (!gitlabUser || !gitlabUser.length) {
|
||||||
|
throw new Error(`user ${user.username} is missing in Gitlab`)
|
||||||
|
}
|
||||||
|
return `${gitlabUser[0].id}`
|
||||||
|
case 'ldap':
|
||||||
|
return user.username.toUpperCase()
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAvatar(context, user) {
|
||||||
|
const collection = context.rocketchat.avatarsCollection()
|
||||||
|
const avatar = await collection.findOne({userId: user._id}, {sort: {_updatedAt: -1}})
|
||||||
|
if (!avatar) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return Utils.exportFile(context, collection, avatar._id)
|
||||||
|
}
|
||||||
191
lib/rocketchat/utils.js
Normal file
191
lib/rocketchat/utils.js
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
const _ = require('lodash')
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Declare utils object
|
||||||
|
//
|
||||||
|
const utils = {}
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
utils.chunk = function(body) {
|
||||||
|
//
|
||||||
|
// Use regex to create an array
|
||||||
|
// of strings of max length
|
||||||
|
//
|
||||||
|
return body.match(/[\s\S]{1,4000}/g)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lookup values
|
||||||
|
//
|
||||||
|
utils.lookup = function (type, map, key) {
|
||||||
|
var found = map[key]
|
||||||
|
|
||||||
|
if(!found) {
|
||||||
|
throw new Error(`${type} ${key} not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Obtain the username from a jid
|
||||||
|
//
|
||||||
|
utils.username = function (users, id) {
|
||||||
|
return utils.lookup('user', users, id).username
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Obtain the channel name from a jid
|
||||||
|
//
|
||||||
|
utils.channelName = function (channels, id) {
|
||||||
|
return utils.lookup('channel', channels, id).name
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Find the message body
|
||||||
|
//
|
||||||
|
utils.body = function (body) {
|
||||||
|
return utils.chunk(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.processAttachments = function (context, message) {
|
||||||
|
if (!message.attachments) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return message.attachments.map((attachment) => {
|
||||||
|
if (attachment.type && attachment.type === 'file') {
|
||||||
|
return {
|
||||||
|
type: 'file',
|
||||||
|
data: utils.processFileAttachment(context, message, attachment),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (attachment.text) {
|
||||||
|
return {
|
||||||
|
type: 'quote',
|
||||||
|
data: utils.processQuoteAttachment(message, attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'unknown'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.processQuoteAttachment = function (message, attachment) {
|
||||||
|
if (!attachment.text) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const body = (message.msg || '').replace(/\[ \]\(.*msg=.*?\) /, '')
|
||||||
|
// Convert quote to markdown
|
||||||
|
return `@${attachment.author_name}:\n ${attachment.text.replace(/^/, '> ')} \n\n${body}`
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.processFileAttachment = async function (context, message, attachment) {
|
||||||
|
if (!message.file) {
|
||||||
|
return new Error(`message ${message._id} file is missing`)
|
||||||
|
}
|
||||||
|
const collection = context.rocketchat.uploadsCollection()
|
||||||
|
return {
|
||||||
|
description: attachment.description,
|
||||||
|
path: await utils.exportFile(context, collection, message.file._id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.srcPath = function (srcDir, filename) {
|
||||||
|
const files = fs.readdirSync(srcDir).filter((fn) => fn.startsWith(filename));
|
||||||
|
if (files.length !== 1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return `${srcDir}/${path.basename(files[0])}`
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.destPath = function (destDir, type, file) {
|
||||||
|
let dest = `${destDir}/${type}/${file._id}/${file.name}`;
|
||||||
|
const ext = path.extname(dest)
|
||||||
|
if (!ext || !/^\.[A-Za-z][A-Za-z0-9]*$/.test(ext)) {
|
||||||
|
if (file.identify && file.identify.format) {
|
||||||
|
dest += '.' + file.identify.format
|
||||||
|
} else {
|
||||||
|
dest += '.' + file.type.split('/')[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.exportFile = async function (context, collection, fileId) {
|
||||||
|
const type = collection.collectionName
|
||||||
|
const file = await collection.findOne({ _id: fileId })
|
||||||
|
if (!file) {
|
||||||
|
throw new Error(`file ${fileId} from collection ${type} is missing`)
|
||||||
|
}
|
||||||
|
const dest = context.config.target.filesPath
|
||||||
|
const destFilename = utils.destPath(dest, collection.collectionName, file)
|
||||||
|
|
||||||
|
if (file.store.startsWith('FileSystem:')) {
|
||||||
|
const src = context.config.source.uploadsPath
|
||||||
|
const srcFilename = utils.srcPath(src, file._id)
|
||||||
|
if (!srcFilename) {
|
||||||
|
return new Error(`source file "${file._id}" not found`)
|
||||||
|
}
|
||||||
|
// Copy to output dir
|
||||||
|
utils.copyFile(srcFilename, destFilename)
|
||||||
|
} else if (file.store.startsWith('GridFS:')) {
|
||||||
|
await utils.downloadGridFS(context, collection, file._id, destFilename)
|
||||||
|
} else {
|
||||||
|
throw new Error(`file system ${file.store} is not supported. Migrate to FileSystem first, see readme.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return destFilename
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.copyFile = function (src, dest) {
|
||||||
|
if (!fs.existsSync(path.dirname(dest))) {
|
||||||
|
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(dest)) {
|
||||||
|
fs.copyFileSync(src, dest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.downloadGridFS = async function (context, collection, id, dest) {
|
||||||
|
if (!fs.existsSync(path.dirname(dest))) {
|
||||||
|
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
||||||
|
}
|
||||||
|
if (fs.existsSync(dest)) {
|
||||||
|
fs.unlinkSync(dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucket = context.rocketchat.gridFsBucket(collection.collectionName)
|
||||||
|
const destStream = fs.createWriteStream(dest);
|
||||||
|
bucket.openDownloadStream(id).pipe(destStream);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
destStream.on('finish', resolve);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.members = function(users, usernames) {
|
||||||
|
return _.uniq(_.sortBy(usernames.map((username) => utils.username(users, username))))
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Checks if the members list is valid
|
||||||
|
//
|
||||||
|
utils.membersAreValid = function(members) {
|
||||||
|
return _.isArray(members) && members.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Convert ISO to millis
|
||||||
|
//
|
||||||
|
utils.millis = function(date) {
|
||||||
|
return new Date(date).getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Export the functions
|
||||||
|
//
|
||||||
|
module.exports = utils
|
||||||
12
lib/rocketchat/version.js
Normal file
12
lib/rocketchat/version.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const Factory = require('../factory')
|
||||||
|
|
||||||
|
module.exports = function(context) {
|
||||||
|
//
|
||||||
|
// Write the version object
|
||||||
|
//
|
||||||
|
context.output.write(Factory.version())
|
||||||
|
//
|
||||||
|
// Return a resolved promise
|
||||||
|
//
|
||||||
|
return Promise.resolve(context)
|
||||||
|
}
|
||||||
33
lib/test/datafile.js
Normal file
33
lib/test/datafile.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const expect = require('chai').expect
|
||||||
|
const fs = require('fs')
|
||||||
|
const datafile = require('../datafile')
|
||||||
|
|
||||||
|
const filename = 'test.json'
|
||||||
|
|
||||||
|
describe('datafile', function() {
|
||||||
|
|
||||||
|
var resultHandler = function(err) {
|
||||||
|
if(err) {
|
||||||
|
console.log('unlink failed', err)
|
||||||
|
} else {
|
||||||
|
console.log('file deleted')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should write objects to a file', function(done) {
|
||||||
|
var d = datafile(filename, () => {
|
||||||
|
expect(fs.existsSync(filename)).to.be.true
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
d.write({
|
||||||
|
foo: 'bar'
|
||||||
|
})
|
||||||
|
|
||||||
|
d.end()
|
||||||
|
})
|
||||||
|
|
||||||
|
after(function() {
|
||||||
|
fs.unlink(filename, resultHandler)
|
||||||
|
})
|
||||||
|
})
|
||||||
6330
package-lock.json
generated
Normal file
6330
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
package.json
Normal file
42
package.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "loop-etl-rocketchat",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "An ETL framework to migrate data to Loop",
|
||||||
|
"author": "Michael DeBonis",
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://git.wilix.dev/loop/loop-etl-rocketchat#readme",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://git.wilix.dev/loop/loop-etl-rocketchat.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://git.wilix.dev/loop/loop-etl-rocketchat/issues"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@gitbeaker/node": "^35.6.0",
|
||||||
|
"bunyan": "^1.8.10",
|
||||||
|
"joi": "^10.6.0",
|
||||||
|
"lodash": "^4.17.4",
|
||||||
|
"mongodb": "^4.7.0",
|
||||||
|
"mssql": "^4.0.4",
|
||||||
|
"slug": "^0.9.1",
|
||||||
|
"xmldoc": "^1.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"chai": "^4.0.2",
|
||||||
|
"codeclimate-test-reporter": "^0.5.0",
|
||||||
|
"eslint": "^4.1.0",
|
||||||
|
"istanbul": "^0.4.5",
|
||||||
|
"mocha": "^3.4.2",
|
||||||
|
"sinon": "^2.3.4"
|
||||||
|
},
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint *.js context lib --ext .js",
|
||||||
|
"test": "npm run lint && mocha lib --recursive",
|
||||||
|
"test-watch": "npm run test -- --watch",
|
||||||
|
"test-coverage": "istanbul cover _mocha lib -- --recursive",
|
||||||
|
"start": "node index.js | bunyan",
|
||||||
|
"start:rocketchat": "node rocketchat.js | bunyan"
|
||||||
|
}
|
||||||
|
}
|
||||||
46
rocketchat.js
Normal file
46
rocketchat.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
const context = require('./context/rocketchat')
|
||||||
|
const log = require('./lib/log')
|
||||||
|
const {
|
||||||
|
start,
|
||||||
|
version,
|
||||||
|
emoji,
|
||||||
|
team,
|
||||||
|
channels,
|
||||||
|
users,
|
||||||
|
posts,
|
||||||
|
directChannels,
|
||||||
|
end
|
||||||
|
} = require('./lib/rocketchat')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Common function log errors and
|
||||||
|
// terminate the process
|
||||||
|
//
|
||||||
|
const abort = function (err) {
|
||||||
|
log.error(err)
|
||||||
|
//
|
||||||
|
// We set a timeout here to
|
||||||
|
// allow the log streams to finish
|
||||||
|
// writing.
|
||||||
|
//
|
||||||
|
setTimeout(function () {
|
||||||
|
process.exit(1)
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Ensure we trap uncaught exceptions
|
||||||
|
// and properly abort
|
||||||
|
//
|
||||||
|
process.on('uncaughtException', abort)
|
||||||
|
|
||||||
|
start(context)
|
||||||
|
.then(version)
|
||||||
|
.then(emoji)
|
||||||
|
.then(team)
|
||||||
|
.then(channels)
|
||||||
|
.then(users)
|
||||||
|
.then(directChannels)
|
||||||
|
.then(posts)
|
||||||
|
.then(end)
|
||||||
|
.catch(abort)
|
||||||
Reference in New Issue
Block a user