diff --git a/.circleci/config.yml b/.circleci/config.yml index 41fd47d3f620..bac4aacfc953 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -142,6 +142,7 @@ jobs: TEST_MODE: "true" MONGO_URL: mongodb://localhost:27017/testwithoplog MONGO_OPLOG_URL: mongodb://localhost:27017/local + RETRY_TESTS: 5 steps: - attach_workspace: @@ -174,7 +175,7 @@ jobs: name: Run Tests command: | if [[ $DISABLE_SMARTI ]]; then rm -rf ./tests/end-to-end/ui_smarti; fi; - for i in $(seq 1 5); do npm test && s=0 && break || s=$? && sleep 1; done; (exit $s) + npm test - store_artifacts: path: .screenshots/ @@ -188,6 +189,7 @@ jobs: environment: TEST_MODE: "true" MONGO_URL: mongodb://localhost:27017/testwithoplog + RETRY_TESTS: 5 steps: - attach_workspace: @@ -212,7 +214,7 @@ jobs: name: Run Tests command: | if [[ $DISABLE_SMARTI ]]; then rm -rf ./tests/end-to-end/ui_smarti; fi; - for i in $(seq 1 5); do npm test && s=0 && break || s=$? && sleep 1; done; (exit $s) + npm test - store_artifacts: path: .screenshots/ diff --git a/.docker/Dockerfile.local b/.docker/Dockerfile.local index 9fc3eb43797b..60591366b41a 100644 --- a/.docker/Dockerfile.local +++ b/.docker/Dockerfile.local @@ -1,20 +1,55 @@ -FROM node:8 +FROM ubuntu:16.04 as builder -ADD . /app +RUN apt update && apt install curl git bzip2 g++ build-essential python -y -ENV RC_VERSION=0.57.0-designpreview \ - DEPLOY_METHOD=docker \ - NODE_ENV=production \ - PORT=3000 \ - ROOT_URL=http://localhost:3000 +# meteor installer doesn't work with the default tar binary +RUN apt-get install -y bsdtar \ + && cp $(which tar) $(which tar)~ \ + && ln -sf $(which bsdtar) $(which tar) \ + && curl "https://install.meteor.com/?release=1.6.0.1" \ + | sed 's/VERBOSITY="--silent"/VERBOSITY="--progress-bar"/' \ + | sh \ + && mv $(which tar)~ $(which tar) + +COPY . /app + +# Add Chatpal which is a submodule +WORKDIR /app +RUN meteor npm i \ + && meteor npm run postinstall \ + && set +e \ + && meteor add rocketchat:lib --allow-superuser \ + && set -e \ + && meteor build --allow-superuser --server-only --headless --directory /tmp/build + +FROM assistify/chat-base:latest + +MAINTAINER buildmaster@rocket.chat + +COPY --from=builder /tmp/build/bundle /app/bundle RUN set -x \ + && ls -l /app \ && cd /app/bundle/programs/server \ && npm install \ - && npm cache clear --force + && npm cache clear --force \ + && chown -R rocketchat:rocketchat /app + +USER rocketchat + +VOLUME /app/uploads WORKDIR /app/bundle +# needs a mongoinstance - defaults to container linking with alias 'mongo' +ENV DEPLOY_METHOD=docker \ + NODE_ENV=production \ + MONGO_URL=mongodb://mongo:27017/rocketchat \ + HOME=/tmp \ + PORT=3000 \ + ROOT_URL=http://localhost:3000 \ + Accounts_AvatarStorePath=/app/uploads + EXPOSE 3000 CMD ["node", "main.js"] diff --git a/.dockerignore b/.dockerignore index 594684228cd0..2f1ccc6d43e0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,82 @@ +**/bin/** +**/build/* +**/node_modules/* +**/tmp/* +**/.meteor/.id +**/.meteor/dev_bundle +**/.meteor/local* +**/.meteor/meteorite +/private/certs/* +*.bak +*.iml +*.ipr +*.iws +*.launch +*.log +*.pydevproject +*.sublime-project +*.sublime-workspace +*.swp +*.tmp +*.tokens +*.un~ +*~ +*~.nib +.*.sw[a-z] +.\#* +._* +.buildpath +.classpath +.clover +.cproject +.DS_Store +.elasticbeanstalk +.elc +.emacs.desktop +.emacs.desktop.lock +.env +.externalToolBuilders .git .gitignore -LICENSE -README.md -docker-compose.yml \ No newline at end of file +.idea +.vscode +.loadpath +.map +.metadata +packages/rocketchat-livechat/assets/rocketchat-livechat.min.js +.mule +.pmd +.project +.sass-cache +.settings +.Spotlight-V100 +tatus +.Trashes +.wtpmodules +\#*\# +Desktop.ini +docker-compose.yml +ehthumbs.db +example.css +jrat.output +jrat.xml +local.properties +meteor-vulcanize +nb-configuration.xml +nbactions.xml +nbproject +profiles.xml +Session.vim +smart.lock +temp_* +Thumbs.db +thumbs.db +tramp +ecosystem.json +pm2.json +settings.json +build.sh +/public/livechat +packages/rocketchat-i18n/i18n/livechat.* +tests/end-to-end/temporary_staged_test +.screenshots diff --git a/.scripts/separateTesting.sh b/.scripts/separateTesting.sh new file mode 100755 index 000000000000..47e477bb5465 --- /dev/null +++ b/.scripts/separateTesting.sh @@ -0,0 +1,23 @@ +#!/bin/bash +tmpPath=tests/end-to-end/temporary_staged_test +rm -rf $tmpPath +mkdir -p $tmpPath +[ -z "$RETRY_TESTS" ] && RETRY_TESTS=1 +for file in tests/end-to-end/*/*.js; do + failed=1 + for i in `seq 1 $RETRY_TESTS`; do + echo '-------------- '$i' try ---------------' + set -x + cp $file $tmpPath + CHIMP_PATH=$tmpPath npm run chimp-path + failed=$? + set +x + if [ $failed -eq 0 ]; then + break + fi + done + if [ $failed -ne 0 ]; then + exit 1 + fi + rm $tmpPath/${file##*/} +done diff --git a/.scripts/start.js b/.scripts/start.js index 2d377c36bd3d..d1c60c96c127 100644 --- a/.scripts/start.js +++ b/.scripts/start.js @@ -68,7 +68,7 @@ function startApp(callback) { function startChimp() { startProcess({ name: 'Chimp', - command: 'npm run chimp-test', + command: '.scripts/separateTesting.sh', options: { env: Object.assign({}, process.env, { NODE_PATH: `${ process.env.NODE_PATH + diff --git a/HISTORY.md b/HISTORY.md index 903ba5b47c54..433b87def473 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,12 @@ +# Assistify 0.9.2 + +This is a bugfix release. + +Major changes: + +- Threading can now be configured to limit the number of users invited +- A username is now being generated from the SAML Identity - this shall fix some other nasty bugs. + # Assistify 0.9.1 This is a bugfix release: @@ -2995,4 +3004,4 @@ Assistify.Chat is now based on Rocket.Chat 0.68.5! - [@marceloschmidt](https://github.com/marceloschmidt) - [@mrsimpson](https://github.com/mrsimpson) - [@rodrigok](https://github.com/rodrigok) -- [@sampaiodiego](https://github.com/sampaiodiego) \ No newline at end of file +- [@sampaiodiego](https://github.com/sampaiodiego) diff --git a/client/routes/roomRoute.js b/client/routes/roomRoute.js index 9e9f94bfbe8b..126b351abeaa 100644 --- a/client/routes/roomRoute.js +++ b/client/routes/roomRoute.js @@ -1,11 +1,12 @@ FlowRouter.goToRoomById = (roomId) => { - const subscription = ChatSubscription.findOne({rid: roomId}); - if (subscription) { - RocketChat.roomTypes.openRouteLink(subscription.t, subscription, FlowRouter.current().queryParams); + const room = ChatRoom.findOne({_id: roomId}); + if (room) { + RocketChat.roomTypes.openRouteLink(room.t, room, FlowRouter.current().queryParams); } else { - const room = ChatRoom.findOne({_id: roomId}); - if (room) { - RocketChat.roomTypes.openRouteLink(room.t, room, FlowRouter.current().queryParams); - } + Meteor.call('getRoomNameAndTypeByNameOrId', roomId, (err, room)=>{ + if (!err) { + RocketChat.roomTypes.openRouteLink(room.t, room, FlowRouter.current().queryParams); + } + }); } }; diff --git a/package-lock.json b/package-lock.json index c1137febd92c..af3fa5b20bae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4486,6 +4486,7 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "optional": true, "requires": { "prr": "1.0.1" } @@ -9495,7 +9496,8 @@ "natives": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.4.tgz", - "integrity": "sha512-Q29yeg9aFKwhLVdkTAejM/HvYG0Y1Am1+HUkFQGn5k2j8GS+v60TVmZh6nujpEAj/qql+wGUrlryO8bF+b1jEg==" + "integrity": "sha512-Q29yeg9aFKwhLVdkTAejM/HvYG0Y1Am1+HUkFQGn5k2j8GS+v60TVmZh6nujpEAj/qql+wGUrlryO8bF+b1jEg==", + "optional": true }, "natural-compare": { "version": "1.4.0", @@ -13982,7 +13984,8 @@ "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "optional": true }, "pseudomap": { "version": "1.0.2", diff --git a/package.json b/package.json index 4a582bba4934..6f273ba04b4b 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "lint-fix": "eslint . --fix", "stylelint": "stylelint packages/**/*.css", "test": "node .scripts/start.js", - "deploy": "npm run build && pm2 startOrRestart pm2.json", + "test-resume": "/bin/bash .scripts/continueTesting.sh", "chimp-path": "chimp tests/chimp-config.js --path=$CHIMP_PATH", "chimp-watch": "chimp --ddp=http://localhost:3000 --watch --mocha --path=tests/end-to-end", "chimp-test": "chimp tests/chimp-config.js", diff --git a/packages/assistify-ai/server/SmartiProxy.js b/packages/assistify-ai/server/SmartiProxy.js index e88e7ac93aaf..cc2106c0f048 100644 --- a/packages/assistify-ai/server/SmartiProxy.js +++ b/packages/assistify-ai/server/SmartiProxy.js @@ -28,12 +28,13 @@ export class SmartiProxy { * * @param {String} method - the HTTP method to use * @param {String} path - the path to call - * @param {Object} [body=null] - the payload to pass (optional) + * @param {Object} [parameters=null] - the http query params (optional) + * @param {String} [body=null] - the payload to pass (optional) * @param {Function} onError=null - custom error handler * * @returns {Object} */ - static propagateToSmarti(method, path, body = null, onError = null) { + static propagateToSmarti(method, path, parameters = null, body = null, onError = null) { const url = `${ SmartiProxy.smartiUrl }${ path }`; const header = { 'X-Auth-Token': SmartiProxy.smartiAuthToken, @@ -41,7 +42,11 @@ export class SmartiProxy { }; try { SystemLogger.debug('Sending request to Smarti', method, 'to', url, 'body', JSON.stringify(body)); - const response = HTTP.call(method, url, {data: body, headers: header}); + const response = HTTP.call(method, url, { + params: parameters, + data: body, + headers: header + }); if (response.statusCode < 400) { return response.data || response.content; //.data if it's a json-response } else { diff --git a/packages/assistify-ai/server/lib/SmartiAdapter.js b/packages/assistify-ai/server/lib/SmartiAdapter.js index 5ca6605d63e6..77066bda79d1 100644 --- a/packages/assistify-ai/server/lib/SmartiAdapter.js +++ b/packages/assistify-ai/server/lib/SmartiAdapter.js @@ -92,17 +92,17 @@ export class SmartiAdapter { if (message.editedAt) { SystemLogger.debug('Trying to update existing message...'); // update existing message - request_result = SmartiProxy.propagateToSmarti(verbs.put, `conversation/${ conversationId }/message/${ requestBodyMessage.id }`, requestBodyMessage, (error) => { + request_result = SmartiProxy.propagateToSmarti(verbs.put, `conversation/${ conversationId }/message/${ requestBodyMessage.id }`, null, requestBodyMessage, (error) => { // 404 is expected if message doesn't exist if (!error.response || error.response.statusCode === 404) { SystemLogger.debug('Message not found!'); SystemLogger.debug('Adding new message to conversation...'); - request_result = SmartiProxy.propagateToSmarti(verbs.post, `conversation/${ conversationId }/message`, requestBodyMessage); + request_result = SmartiProxy.propagateToSmarti(verbs.post, `conversation/${ conversationId }/message`, null, requestBodyMessage); } }); } else { SystemLogger.debug('Adding new message to conversation...'); - request_result = SmartiProxy.propagateToSmarti(verbs.post, `conversation/${ conversationId }/message`, requestBodyMessage); + request_result = SmartiProxy.propagateToSmarti(verbs.post, `conversation/${ conversationId }/message`, null, requestBodyMessage); } if (request_result) { @@ -150,7 +150,7 @@ export class SmartiAdapter { const conversationId = SmartiAdapter.getConversationId(room._id); if (conversationId) { - const res = SmartiProxy.propagateToSmarti(verbs.put, `/conversation/${ conversationId }/meta.status`, 'Complete'); + const res = SmartiProxy.propagateToSmarti(verbs.put, `/conversation/${ conversationId }/meta.status`, null, 'Complete'); if (!res) { Meteor.defer(() => SmartiAdapter._markRoomAsUnsynced(room._id)); } @@ -188,7 +188,7 @@ export class SmartiAdapter { let conversationId = null; // uncached conversation SystemLogger.debug('Trying Smarti legacy service to retrieve conversation...'); - const conversation = SmartiProxy.propagateToSmarti(verbs.get, `legacy/rocket.chat?channel_id=${ roomId }`, null, (error) => { + const conversation = SmartiProxy.propagateToSmarti(verbs.get, `legacy/rocket.chat?channel_id=${ roomId }`, null, null, (error) => { // 404 is expected if no mapping exists in Smarti if (error.response.statusCode === 404) { SystemLogger.warn(`No Smarti conversationId found (Server Error 404) for room: ${ roomId }`); @@ -237,7 +237,7 @@ export class SmartiAdapter { // conversation updated or created => request analysis results SystemLogger.debug(`Smarti - conversation updated or created -> get analysis result asynch [ callback=${ SmartiAdapter.rocketWebhookUrl } ] for conversation: ${ conversationId } and room: ${ roomId }`); - SmartiProxy.propagateToSmarti(verbs.get, `conversation/${ conversationId }/analysis?callback=${ SmartiAdapter.rocketWebhookUrl }`); // asynch + SmartiProxy.propagateToSmarti(verbs.get, `conversation/${ conversationId }/analysis`, { callback: SmartiAdapter.rocketWebhookUrl }); // asynch } /** @@ -376,7 +376,7 @@ export class SmartiAdapter { const conversationId = SmartiAdapter.getConversationId(room._id); if (conversationId) { SystemLogger.debug(`Conversation found ${ conversationId } - delete and create new conversation`); - SmartiProxy.propagateToSmarti(verbs.delete, `conversation/${ conversationId }`, null); + SmartiProxy.propagateToSmarti(verbs.delete, `conversation/${ conversationId }`); } // get the messages of the room and create a conversation from it @@ -456,7 +456,7 @@ export class SmartiAdapter { } // post the conversation - const conversation = SmartiProxy.propagateToSmarti(verbs.post, 'conversation', conversationBody, (error) => { + const conversation = SmartiProxy.propagateToSmarti(verbs.post, 'conversation', null, conversationBody, (error) => { SystemLogger.error(`Smarti - unexpected server error: ${ JSON.stringify(error, null, 2) } occured when creating a new conversation: ${ JSON.stringify(conversationBody, null, 2) }`); }); if (!conversation && !conversation.id) { @@ -563,7 +563,7 @@ export class SmartiAdapter { static _smartiAvailable() { // if Smarti is not available stop immediately - const resp = SmartiProxy.propagateToSmarti(verbs.get, 'system/health', null, (error) => { + const resp = SmartiProxy.propagateToSmarti(verbs.get, 'system/health', null, null, (error) => { if (error.statusCode !== 200) { const e = new Meteor.Error('Smarti not reachable!'); SystemLogger.error('Stop synchronizing with Smarti immediately:', e); diff --git a/packages/assistify-ai/server/methods/SmartiWidgetBackend.js b/packages/assistify-ai/server/methods/SmartiWidgetBackend.js index 2ddbffdba05d..c8d7331bb885 100644 --- a/packages/assistify-ai/server/methods/SmartiWidgetBackend.js +++ b/packages/assistify-ai/server/methods/SmartiWidgetBackend.js @@ -3,8 +3,6 @@ import {SystemLogger} from 'meteor/rocketchat:logger'; import {SmartiProxy, verbs} from '../SmartiProxy'; import {SmartiAdapter} from '../lib/SmartiAdapter'; -const querystring = require('querystring'); - /** @namespace RocketChat.RateLimiter.limitFunction */ /** @@ -44,7 +42,7 @@ Meteor.methods({ return !RocketChat.authz.hasPermission(userId, 'send-many-messages'); } } - )(verbs.get, `conversation/${ conversationId }/analysis`, null, (error) => { + )(verbs.get, `conversation/${ conversationId }/analysis`, null, null, (error) => { // 404 is expected if no mapping exists if (error.response && error.response.statusCode === 404) { return null; @@ -70,40 +68,34 @@ Meteor.methods({ return !RocketChat.authz.hasPermission(userId, 'send-many-messages'); } } - )(verbs.get, `conversation/${ conversationId }/analysis/template/${ templateIndex }/result/${ creator }?start=${ start }&rows=${ rows }`); + )(verbs.get, `conversation/${ conversationId }/analysis/template/${ templateIndex }/result/${ creator }`, { start, rows }); }, searchConversations(queryParams) { - const _getAclQuery = function() { - - function unique(value, index, array) { - return array.indexOf(value) === index; - } + function unique(value, index, array) { + return array.indexOf(value) === index; + } - const solrFilterBooleanLimit = 256; // there is a limit for boolean expressinos in a filter query of default 1024 and an additional limiter by the HTTP-server. Experiments showed this limit as magic number. - const findOptions = { limit: solrFilterBooleanLimit, sort: { ts: -1 }, fields: { _id: 1 } }; - const subscribedRooms = RocketChat.models.Subscriptions.find({'u._id': Meteor.userId()}, { limit: solrFilterBooleanLimit, sort: { ts: -1 }, fields: { rid: 1 } }).fetch().map(subscription => subscription.rid); - const publicChannels = RocketChat.authz.hasPermission(Meteor.userId(), 'view-c-room') ? RocketChat.models.Rooms.find({t: 'c'}, findOptions).fetch().map(room => room._id) : []; - const livechats = RocketChat.authz.hasPermission(Meteor.userId(), 'view-l-room') ? RocketChat.models.Rooms.find({t: 'l'}, findOptions).fetch().map(room => room._id) : []; + const solrFilterBooleanLimit = 256; // there is a limit for boolean expressinos in a filter query of default 1024 and an additional limiter by the HTTP-server. Experiments showed this limit as magic number. + const findOptions = { limit: solrFilterBooleanLimit, sort: { ts: -1 }, fields: { _id: 1 } }; + const subscribedRooms = RocketChat.models.Subscriptions.find({'u._id': Meteor.userId()}, { limit: solrFilterBooleanLimit, sort: { ts: -1 }, fields: { rid: 1 } }).fetch().map(subscription => subscription.rid); + const publicChannels = RocketChat.authz.hasPermission(Meteor.userId(), 'view-c-room') ? RocketChat.models.Rooms.find({t: 'c'}, findOptions).fetch().map(room => room._id) : []; + const livechats = RocketChat.authz.hasPermission(Meteor.userId(), 'view-l-room') ? RocketChat.models.Rooms.find({t: 'l'}, findOptions).fetch().map(room => room._id) : []; - const accessibleRooms = livechats.concat(subscribedRooms).concat(publicChannels); + const accessibleRooms = livechats.concat(subscribedRooms).concat(publicChannels); - const filterCriteria = `${ accessibleRooms.filter(unique).slice(0, solrFilterBooleanLimit).join(' OR ') }`; - return filterCriteria - ? `&fq=meta_channel_id:(${ filterCriteria })` - : '&fq=meta_channel_id:""'; //fallback: if the user's not authorized to view any room, filter for "nothing" - }; + let fq = `${ accessibleRooms.filter(unique).slice(0, solrFilterBooleanLimit).join(' OR ') }`; + fq = fq ? { fq: `meta_channel_id:(${ fq })` } : { fq: 'meta_channel_id:""'}; //fallback: if the user's not authorized to view any room, filter for "nothing" + const params = Object.assign(queryParams, fq); - const queryString = querystring.stringify(queryParams); - SystemLogger.debug('QueryString: ', queryString); const searchResult = RocketChat.RateLimiter.limitFunction( SmartiProxy.propagateToSmarti, 5, 1000, { userId(userId) { return !RocketChat.authz.hasPermission(userId, 'send-many-messages'); } } - )(verbs.get, `conversation/search?${ queryString }${ _getAclQuery() }`); + )(verbs.get, 'conversation/search', params); SystemLogger.debug('SearchResult: ', JSON.stringify(searchResult, null, 2)); return searchResult; } diff --git a/packages/assistify-migrations/server/startup/migrations.js b/packages/assistify-migrations/server/startup/migrations.js index da78b9b921da..f75770d83c9f 100644 --- a/packages/assistify-migrations/server/startup/migrations.js +++ b/packages/assistify-migrations/server/startup/migrations.js @@ -5,82 +5,18 @@ on startup which migrate data - ignoring the actual version */ Meteor.startup(() => { - const topics = RocketChat.models.Rooms.findByType('e').fetch(); - let counterRequests = 0; - // Update room type and parent room id for request - const mapRoomParentRoom = new Map(); - RocketChat.models.Rooms.findByType('r').forEach((request) => { - const update = {}; - update.$set = {}; - let parentTopic = null; - - if (request.expertise) { - parentTopic = topics.find(topic => { - return request.expertise === topic.name; - }); - if (!parentTopic) { - console.log('couldn\'t find topic', request.expertise, '- ignoring'); - } else { - update.$set.parentRoomId = parentTopic._id; - mapRoomParentRoom.set(request._id, parentTopic._id); // buffer the mapping for the subscriptions update lateron - } - } - update.$set.oldType = request.t; - update.$set.t = 'p'; - // update requests - RocketChat.models.Rooms.update({_id: request._id}, update); - counterRequests++; - }); - console.log('Migrated', counterRequests, 'requests to private groups'); - - //Update room type and parent room id for expertises - RocketChat.models.Rooms.update({ - t: 'e' - }, { - $set: { - oldType: 'e', // move the room type as old room type - t: 'c' // set new room type to public channel - } - }, { - multi: true - }); - - //update subscriptions for requests - RocketChat.models.Subscriptions.update({ - t: 'r' - }, { - $set: { - oldType: 'r', - t: 'p' - } - }, { - multi: true - }); - - // provide parent Room links in the subscriptions as well - mapRoomParentRoom.forEach((value, key)=>{ - RocketChat.models.Subscriptions.update({ - rid: key - }, { - $set: { - parentRoomId: value - } - }, { - multi: true - }); - }); - - //update subscriptions for expertises - RocketChat.models.Subscriptions.update({ - t: 'e' - }, { - $set: { - oldType: 'e', - t: 'c' - } - }, { - multi: true + const _guessNameFromUsername = function(username) { + return username + .replace(/\W/g, ' ') + .replace(/\s(.)/g, function($1) { return $1.toUpperCase(); }) + .replace(/^(.)/, function($1) { return $1.toLowerCase(); }) + .replace(/^\w/, function($1) { return $1.toUpperCase(); }); + }; + + const usersWithoutName = RocketChat.models.Users.find({name: null}).fetch(); + usersWithoutName.forEach((user)=>{ + RocketChat.models.Users.update({_id: user._id}, {$set: {name: _guessNameFromUsername(user.username)}}); }); }); diff --git a/packages/assistify-threading/client/views/creationDialog/CreateThread.js b/packages/assistify-threading/client/views/creationDialog/CreateThread.js index 2a6451eca070..cd61a25bd164 100755 --- a/packages/assistify-threading/client/views/creationDialog/CreateThread.js +++ b/packages/assistify-threading/client/views/creationDialog/CreateThread.js @@ -2,6 +2,7 @@ /* globals _ */ import { FlowRouter } from 'meteor/kadira:flow-router'; import { ReactiveVar } from 'meteor/reactive-var'; +import toastr from 'toastr'; const parent = document.querySelector('.main-content'); let oldRoute = ''; @@ -234,6 +235,7 @@ Template.CreateThread.events({ const parentChannel = instance.parentChannel.get(); const parentChannelId = instance.parentChannelId.get(); const openingQuestion = instance.openingQuestion.get(); + let errorText = ''; if (parentChannelId) { instance.error.set(null); Meteor.call('createThread', parentChannelId, { @@ -243,19 +245,19 @@ Template.CreateThread.events({ console.log(err); switch (err.error) { case 'error-invalid-name': - instance.error.set(TAPi18n.__('Invalid_room_name', `${ parentChannel }...`)); - return; + errorText = TAPi18n.__('Invalid_room_name', `${ parentChannel }...`); + break; case 'error-duplicate-channel-name': - instance.error.set(TAPi18n.__('Request_already_exists')); - return; + errorText = TAPi18n.__('Request_already_exists'); + break; case 'error-archived-duplicate-name': - instance.error.set(TAPi18n.__('Duplicate_archived_channel_name', name)); - return; + errorText = TAPi18n.__('Duplicate_archived_channel_name', name); + break; case 'error-invalid-room-name': console.log('room name slug error'); // toastr.error(TAPi18n.__('Duplicate_archived_channel_name', name)); - instance.error.set(TAPi18n.__('Invalid_room_name', err.details.channel_name)); - return; + errorText = TAPi18n.__('Invalid_room_name', err.details.channel_name); + break; default: return handleError(err); } @@ -267,6 +269,15 @@ Template.CreateThread.events({ RocketChat.roomTypes.openRouteLink(result.t, result); } }); + } else { + errorText = TAPi18n.__('Invalid_room_name', `${ parentChannel }...`); + } + + if (errorText) { + instance.parentChannelError.set(errorText); + if (!instance.selectParent.get()) { + toastr.error(errorText); + } } }, 'click .full-modal__back-button'() { @@ -346,7 +357,7 @@ Template.CreateThread.onCreated(function() { return Meteor.call('assistify:getParentChannelId', parentChannel, (error, result) => { if (!result) { instance.parentChannelId.set(false); - instance.parentChannelError.set('Parent_channel_doesnt_exist'); + instance.parentChannelError.set(TAPi18n.__('Invalid_room_name', `${ parentChannel }...`)); } else { instance.parentChannelError.set(''); instance.parentChannelId.set(result); //assign parent channel Id diff --git a/packages/assistify-threading/client/views/creationDialog/CreateThreadInputError.html b/packages/assistify-threading/client/views/creationDialog/CreateThreadInputError.html index f29f7a3f65f5..87879a5a215c 100644 --- a/packages/assistify-threading/client/views/creationDialog/CreateThreadInputError.html +++ b/packages/assistify-threading/client/views/creationDialog/CreateThreadInputError.html @@ -3,6 +3,6 @@
- + diff --git a/packages/assistify-threading/config.js b/packages/assistify-threading/config.js index ad595a6f0367..ea0ef8d7a85f 100644 --- a/packages/assistify-threading/config.js +++ b/packages/assistify-threading/config.js @@ -37,6 +37,14 @@ Meteor.startup(() => { RocketChat.models.Settings.updateValueById('Thread_default_parent_Channel', defaultChannel); } + RocketChat.settings.add('Thread_invitations_threshold', 10, { + group: 'Threading', + i18nLabel: 'Thread_invitations_threshold', + i18nDescription: 'Thread_invitations_threshold_description', + type: 'int', + public: true + }); + RocketChat.settings.add('Thread_from_context_menu', 'button', { group: 'Threading', i18nLabel: 'Thread_from_context_menu', diff --git a/packages/assistify-threading/server/methods/createThread.js b/packages/assistify-threading/server/methods/createThread.js index 4b17b6c61d87..1e40cc761259 100644 --- a/packages/assistify-threading/server/methods/createThread.js +++ b/packages/assistify-threading/server/methods/createThread.js @@ -98,7 +98,18 @@ export class ThreadBuilder { linkMessage.urls = [{url: this._getMessageUrl(repostedMessage._id)}]; - return RocketChat.models.Messages.createWithTypeRoomIdMessageAndUser('create-thread', parentRoom._id, this._getMessageUrl(repostedMessage._id), this.rocketCatUser, linkMessage, {ts: this._openingQuestion.ts}); + // we want to create a system message for linking the thread from the parent room - so the parent room + // has to support system messages at least for this interaction + if (!parentRoom.sysMes) { + RocketChat.models.Rooms.setSystemMessagesById(parentRoom._id, true); + } + RocketChat.models.Messages.createWithTypeRoomIdMessageAndUser('create-thread', parentRoom._id, this._getMessageUrl(repostedMessage._id), this.rocketCatUser, linkMessage, {ts: this._openingQuestion.ts}); + + // reset it if necessary + if (!parentRoom.sysMes) { + RocketChat.models.Rooms.setSystemMessagesById(parentRoom._id, false); + } + return true; } } @@ -121,6 +132,7 @@ export class ThreadBuilder { _getMembers() { const checkRoles = ['owner', 'moderator', 'leader']; + const maxInvitationCount = Math.max(RocketChat.models.Settings.findOneById('Thread_invitations_threshold').value, 0) || 0; let members = []; const admins = RocketChat.models.Subscriptions.findByRoomIdAndRoles(this._parentRoomId, checkRoles).fetch().map(s => { return { @@ -131,6 +143,10 @@ export class ThreadBuilder { fields: { 'u._id': 1, 'u.username': 1 + }, + sort: { + open: -1, + ls: -1 } }).fetch().map(s => { return { @@ -140,7 +156,7 @@ export class ThreadBuilder { }); if (this._parentRoom.t === 'c') { // only add online users - members = RocketChat.models.Users.findUsersWithUsernameByIdsNotOffline(users.map(user=>user.id)).fetch().map(user=>user.username); + members = RocketChat.models.Users.findUsersWithUsernameByIdsNotOffline(users.slice(0, maxInvitationCount).map(user=>user.id)).fetch().map(user=>user.username); // add admins to the member list and avoid duplicates members = Array.from(new Set(members.concat(admins.map(user=>user.username)))); } else { diff --git a/packages/meteor-accounts-saml/saml_server.js b/packages/meteor-accounts-saml/saml_server.js index 8e6dd13fdf95..80950f2b8498 100644 --- a/packages/meteor-accounts-saml/saml_server.js +++ b/packages/meteor-accounts-saml/saml_server.js @@ -86,6 +86,15 @@ Meteor.methods({ }); Accounts.registerLoginHandler(function(loginRequest) { + + const _guessNameFromUsername = function(username) { + return username + .replace(/\W/g, ' ') + .replace(/\s(.)/g, function($1) { return $1.toUpperCase(); }) + .replace(/^(.)/, function($1) { return $1.toLowerCase(); }) + .replace(/^\w/, function($1) { return $1.toUpperCase(); }); + }; + if (!loginRequest.saml || !loginRequest.credentialToken) { return undefined; } @@ -131,6 +140,8 @@ Accounts.registerLoginHandler(function(loginRequest) { newUser.username = loginResult.profile.username; } + newUser.name = newUser.name || _guessNameFromUsername(newUser.username); // Make sure every user has a name as well + const userId = Accounts.insertUserDoc({}, newUser); user = Meteor.users.findOne(userId); } diff --git a/packages/rocketchat-autotranslate/.npm/package/.gitignore b/packages/rocketchat-autotranslate/.npm/package/.gitignore new file mode 100644 index 000000000000..3c3629e647f5 --- /dev/null +++ b/packages/rocketchat-autotranslate/.npm/package/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/rocketchat-autotranslate/.npm/package/README b/packages/rocketchat-autotranslate/.npm/package/README new file mode 100644 index 000000000000..3d492553a438 --- /dev/null +++ b/packages/rocketchat-autotranslate/.npm/package/README @@ -0,0 +1,7 @@ +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/rocketchat-autotranslate/.npm/package/npm-shrinkwrap.json b/packages/rocketchat-autotranslate/.npm/package/npm-shrinkwrap.json new file mode 100644 index 000000000000..cea863e6cc0f --- /dev/null +++ b/packages/rocketchat-autotranslate/.npm/package/npm-shrinkwrap.json @@ -0,0 +1,87 @@ +{ + "lockfileVersion": 1, + "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==" + }, + "cld": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/cld/-/cld-2.4.8.tgz", + "integrity": "sha512-X5C0aW6KKe5JG4P6pkn6Y75mSuR8Cx3oI0ailENY3MIX9/98QL7ja1tKbPBQAW6DgTb6I1IbMDTSHSfs1ssndA==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==" + }, + "nan": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.0.tgz", + "integrity": "sha512-F4miItu2rGnV2ySkXOQoA8FKz/SR2Q2sWP0sbTxNxz/tuokeC8WxOhPMcwi0qIyGtVn/rrSeLbvVkznqCdwYnw==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dependencies": { + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==" + } + } + }, + "underscore": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", + "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } +} diff --git a/packages/rocketchat-autotranslate/client/lib/actionButton.js b/packages/rocketchat-autotranslate/client/lib/actionButton.js index e8289f61dd82..cb4edcf3bfc7 100644 --- a/packages/rocketchat-autotranslate/client/lib/actionButton.js +++ b/packages/rocketchat-autotranslate/client/lib/actionButton.js @@ -1,3 +1,4 @@ +/* globals RocketChat */ Meteor.startup(function() { Tracker.autorun(function() { if (RocketChat.settings.get('AutoTranslate_Enabled') && RocketChat.authz.hasAtLeastOnePermission(['auto-translate'])) { diff --git a/packages/rocketchat-autotranslate/client/lib/autotranslate.js b/packages/rocketchat-autotranslate/client/lib/autotranslate.js index 0be929042004..1a8eda776672 100644 --- a/packages/rocketchat-autotranslate/client/lib/autotranslate.js +++ b/packages/rocketchat-autotranslate/client/lib/autotranslate.js @@ -1,3 +1,4 @@ +/* globals RocketChat */ import _ from 'underscore'; RocketChat.AutoTranslate = { diff --git a/packages/rocketchat-autotranslate/client/views/autoTranslateFlexTab.js b/packages/rocketchat-autotranslate/client/views/autoTranslateFlexTab.js index 7778b7345634..9d1034b9cd5c 100644 --- a/packages/rocketchat-autotranslate/client/views/autoTranslateFlexTab.js +++ b/packages/rocketchat-autotranslate/client/views/autoTranslateFlexTab.js @@ -1,4 +1,4 @@ -/* globals ChatSubscription */ +/* globals ChatSubscription, RocketChat */ import _ from 'underscore'; import toastr from 'toastr'; diff --git a/packages/rocketchat-autotranslate/package.js b/packages/rocketchat-autotranslate/package.js index 5724f020580f..c2a96c35111a 100644 --- a/packages/rocketchat-autotranslate/package.js +++ b/packages/rocketchat-autotranslate/package.js @@ -25,12 +25,26 @@ Package.onUse(function(api) { api.addFiles([ 'server/settings.js', - 'server/autotranslate.js', 'server/permissions.js', + 'server/autotranslate.js', + 'server/googleTranslate.js', + 'server/deeplTranslate.js', + 'server/dbsTranslate.js', 'server/models/Messages.js', + 'server/models/Settings.js', 'server/models/Subscriptions.js', 'server/methods/saveSettings.js', 'server/methods/translateMessage.js', 'server/methods/getSupportedLanguages.js' ], 'server'); + api.mainModule('server/index.js', 'server'); }); + +/** + * Package-level dependencies + * cld - Text language detector + */ +Npm.depends({ + cld: '2.4.8' +}); + diff --git a/packages/rocketchat-autotranslate/server/autotranslate.js b/packages/rocketchat-autotranslate/server/autotranslate.js index 9e163592314c..f67fd16b71c7 100644 --- a/packages/rocketchat-autotranslate/server/autotranslate.js +++ b/packages/rocketchat-autotranslate/server/autotranslate.js @@ -1,22 +1,54 @@ -import _ from 'underscore'; -import s from 'underscore.string'; +/* globals SystemLogger, RocketChat */ -class AutoTranslate { +import s from 'underscore.string'; +import _ from 'underscore'; +import {RocketChat} from 'meteor/rocketchat:lib'; + +/** + * Generic auto translate base implementation. + * Can be used as superclass for translation providers + * @abstract + * @class + */ +export class AutoTranslate { + /** + * Encapsulate the api key and provider settings. + * @constructor + */ constructor() { + this.name = ''; this.languages = []; - this.enabled = RocketChat.settings.get('AutoTranslate_Enabled'); - this.apiKey = RocketChat.settings.get('AutoTranslate_GoogleAPIKey'); this.supportedLanguages = {}; - RocketChat.callbacks.add('afterSaveMessage', this.translateMessage.bind(this), RocketChat.callbacks.priority.MEDIUM, 'AutoTranslate'); - + // Get the service provide API key. + RocketChat.settings.get('AutoTranslate_APIKey', (key, value) => { + this.apiKey = value; + }); + // Get Service provider URL. + RocketChat.settings.get('AutoTranslate_ServiceProviderURL', (key, value) => { + this.apiEndPointUrl = value; + }); + // Get Auto Translate Active flag RocketChat.settings.get('AutoTranslate_Enabled', (key, value) => { - this.enabled = value; + this.autoTranslateEnabled = value; }); - RocketChat.settings.get('AutoTranslate_GoogleAPIKey', (key, value) => { - this.apiKey = value; + /** Register the active service provider on the 'AfterSaveMessage' callback. + * So the registered provider will be invoked when a message is saved. + * All the other inactive service provider must be deactivated. + */ + RocketChat.settings.get('AutoTranslate_ServiceProvider', (key, value) => { + if (this.name === value) { + this.registerAfterSaveMsgCallBack(this.name); + } else { + this.unRegisterAfterSaveMsgCallBack(this.name); + } }); } + /** + * Extracts non-translatable parts of a message + * @param {object} message + * @return {object} message + */ tokenize(message) { if (!message.tokens || !Array.isArray(message.tokens)) { message.tokens = []; @@ -146,43 +178,29 @@ class AutoTranslate { return message.msg; } + /** + * Triggers the translation of the prepared (tokenized) message + * and persists the result + * @public + * @param {object} message + * @param {object} room + * @param {object} targetLanguage + * @returns {object} unmodified message object. + */ translateMessage(message, room, targetLanguage) { - if (this.enabled && this.apiKey) { + if (this.autoTranslateEnabled && this.apiKey) { let targetLanguages; if (targetLanguage) { - targetLanguages = [ targetLanguage ]; + targetLanguages = [targetLanguage]; } else { targetLanguages = RocketChat.models.Subscriptions.getAutoTranslateLanguagesByRoomAndNotUser(room._id, message.u && message.u._id); } if (message.msg) { Meteor.defer(() => { - const translations = {}; let targetMessage = Object.assign({}, message); - targetMessage.html = s.escapeHTML(String(targetMessage.msg)); targetMessage = this.tokenize(targetMessage); - - let msgs = targetMessage.msg.split('\n'); - msgs = msgs.map(msg => encodeURIComponent(msg)); - const query = `q=${ msgs.join('&q=') }`; - - const supportedLanguages = this.getSupportedLanguages('en'); - targetLanguages.forEach(language => { - if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, { language })) { - language = language.substr(0, 2); - } - let result; - try { - result = HTTP.get('https://translation.googleapis.com/language/translate/v2', { params: { key: this.apiKey, target: language }, query }); - } catch (e) { - console.log('Error translating message', e); - return message; - } - if (result.statusCode === 200 && result.data && result.data.data && result.data.data.translations && Array.isArray(result.data.data.translations) && result.data.data.translations.length > 0) { - const txt = result.data.data.translations.map(translation => translation.translatedText).join('\n'); - translations[language] = this.deTokenize(Object.assign({}, targetMessage, { msg: txt })); - } - }); + const translations = this._translateMessage(targetMessage, targetLanguages); if (!_.isEmpty(translations)) { RocketChat.models.Messages.addTranslations(message._id, translations); } @@ -194,20 +212,8 @@ class AutoTranslate { for (const index in message.attachments) { if (message.attachments.hasOwnProperty(index)) { const attachment = message.attachments[index]; - const translations = {}; if (attachment.description || attachment.text) { - const query = `q=${ encodeURIComponent(attachment.description || attachment.text) }`; - const supportedLanguages = this.getSupportedLanguages('en'); - targetLanguages.forEach(language => { - if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, { language })) { - language = language.substr(0, 2); - } - const result = HTTP.get('https://translation.googleapis.com/language/translate/v2', { params: { key: this.apiKey, target: language }, query }); - if (result.statusCode === 200 && result.data && result.data.data && result.data.data.translations && Array.isArray(result.data.data.translations) && result.data.data.translations.length > 0) { - const txt = result.data.data.translations.map(translation => translation.translatedText).join('\n'); - translations[language] = txt; - } - }); + const translations = this._translateAttachmentDescriptions(attachment, targetLanguages); if (!_.isEmpty(translations)) { RocketChat.models.Messages.addAttachmentTranslations(message._id, index, translations); } @@ -220,38 +226,95 @@ class AutoTranslate { return message; } + /** + * On changing the service provider, the callback in which the translation + * is being requested needs to be switched to the new provider + * @protected + * @param {string} provider + */ + registerAfterSaveMsgCallBack(provider) { + RocketChat.callbacks.add('afterSaveMessage', this.translateMessage.bind(this), RocketChat.callbacks.priority.MEDIUM, provider); + } + + /** + * On changing the service provider, the callback in which the translation + * is being requested needs to be deactivated for the all other translation providers + * @protected + * @param {string} provider + */ + unRegisterAfterSaveMsgCallBack(provider) { + RocketChat.callbacks.remove('afterSaveMessage', provider); + } + + /** + * Returns metadata information about the service provider which is used by + * the generic implementation + * @abstract + * @protected + * @returns { name, displayName, settings } + }; + */ + _getProviderMetadata() { + SystemLogger.warn('must be implemented by subclass!', '_getProviderMetadata'); + } + + + /** + * Provides the possible languages _from_ which a message can be translated into a target language + * @abstract + * @protected + * @param {string} target - the language into which shall be translated + * @returns [{ language, name }] + */ getSupportedLanguages(target) { - if (this.enabled && this.apiKey) { - if (this.supportedLanguages[target]) { - return this.supportedLanguages[target]; - } + SystemLogger.warn('must be implemented by subclass!', 'getSupportedLanguages', target); + } - let result; - const params = { key: this.apiKey }; - if (target) { - params.target = target; - } + /** + * Performs the actual translation of a message, + * usually by sending a REST API call to the service provider. + * @abstract + * @protected + * @param {object} message + * @param {object} targetLanguages + * @return {object} + */ + _translateMessage(message, targetLanguages) { + SystemLogger.warn('must be implemented by subclass!', '_translateMessage', message, targetLanguages); + } - try { - result = HTTP.get('https://translation.googleapis.com/language/translate/v2/languages', { params }); - } catch (e) { - if (e.response && e.response.statusCode === 400 && e.response.data && e.response.data.error && e.response.data.error.status === 'INVALID_ARGUMENT') { - params.target = 'en'; - target = 'en'; - if (!this.supportedLanguages[target]) { - result = HTTP.get('https://translation.googleapis.com/language/translate/v2/languages', { params }); - } - } - } finally { - if (this.supportedLanguages[target]) { - return this.supportedLanguages[target]; - } else { - this.supportedLanguages[target || 'en'] = result && result.data && result.data.data && result.data.data.languages; - return this.supportedLanguages[target || 'en']; - } - } + /** + * Performs the actual translation of an attachment (precisely its description), + * usually by sending a REST API call to the service provider. + * @abstract + * @param {object} attachment + * @param {object} targetLanguages + * @returns {object} translated messages for each target language + */ + _translateAttachmentDescriptions(attachment, targetLanguages) { + SystemLogger.warn('must be implemented by subclass!', '_translateAttachmentDescriptions', attachment, targetLanguages); + } +} + +export class TranslationProviderRegistry { + static registerProvider(provider) { + //get provider information + const metadata = provider._getProviderMetadata(); + if (!TranslationProviderRegistry._providers) { + TranslationProviderRegistry._providers = {}; } + TranslationProviderRegistry._providers[metadata.name] = provider; + } + + static loadActiveServiceProvider() { + RocketChat.settings.get('AutoTranslate_ServiceProvider', (key, value) => { + TranslationProviderRegistry._activeProvider = value; + RocketChat.AutoTranslate = TranslationProviderRegistry._providers[TranslationProviderRegistry._activeProvider]; + }); } } -RocketChat.AutoTranslate = new AutoTranslate; +Meteor.startup(() => { + TranslationProviderRegistry.loadActiveServiceProvider(); +}); + diff --git a/packages/rocketchat-autotranslate/server/dbsTranslate.js b/packages/rocketchat-autotranslate/server/dbsTranslate.js new file mode 100644 index 000000000000..935e4bf0a59c --- /dev/null +++ b/packages/rocketchat-autotranslate/server/dbsTranslate.js @@ -0,0 +1,216 @@ +/** + * @author Vigneshwaran OdayappanDer Benutzer [Name] ([E-Mail])wurde registriert.
Bitte aktivieren Sie \"Administration ->Benutzer\", um sie zu aktivieren oder zu löschen.
", @@ -364,10 +356,16 @@ "AutoLinker_Urls_www": "AutoLinker \"www\"-URLs", "AutoLinker_UrlsRegExp": "AutoLinker RegExp für URLs", "Automatic_Translation": "Automatische Übersetzung", + "AutoTranslate_APIKey": "API Key", "AutoTranslate_Change_Language_Description": "Das Verändern der Option zur automatischen Übersetzung übersetzt keine Nachrichten aus der Vergangenheit.", + "AutoTranslate_DBS": "DBS Business Hub", + "AutoTranslate_DeepL": "DeepL", "AutoTranslate_Enabled_Description": "Die Aktivierung der automatischen Übersetzung ermöglicht es Benutzern mit der entsprechenden Berechtigung (auto-translate
, Nachrichten immer in Ihrer Sprache übersetzt zu lesen. Hierfür fallen potentiell Gebühren an (s. Google-Dokumentation.",
"AutoTranslate_Enabled": "Automatische Übersetzung",
+ "AutoTranslate_Google": "Google",
"AutoTranslate_GoogleAPIKey": "Google API-Schlüssel",
+ "AutoTranslate_ServiceProvider": "Übersetzungsdienst",
+ "AutoTranslate_ServiceProviderURL": "Übersetzungsdienst url",
"Available_agents": "Verfügbare Agenten",
"Available": "Verfügbar",
"Avatar_changed_successfully": "Das Profilbild wurde erfolgreich geändert.",
@@ -1142,7 +1140,6 @@
"File_not_allowed_direct_messages": "Dateiaustausch ist in Direktnachrichten nicht möglich.",
"File_removed_by_automatic_prune": "Datei wurde durch automatische Bereinigung entfernt",
"File_removed_by_prune": "Die Datei wurde entfernt",
- "File_removed_by_automatic_prune": "Datei wurde durch automatische Bereinigung entfernt",
"File_type_is_not_accepted": "Dateityp wir nicht akzeptiert.",
"File_uploaded": "Datei hochgeladen",
"Files_only": "Entferne nur die angehängten Dateien, behalte Nachrichten",
@@ -1503,6 +1500,17 @@
"Language_Not_set": "nicht spezifisch",
"Language_Version": "Deutsche Version",
"Language": "Sprache",
+ "Language_English": "Englisch",
+ "Language_German": "Deutsch",
+ "Language_French": "Französisch",
+ "Language_Spanish": "Spanisch",
+ "Language_Italian": "Italienisch",
+ "Language_Dutch": "Niederländisch",
+ "Language_Polish": "Polnisch",
+ "Language_Romanian": "Rumänisch",
+ "Language_Slovak": "Slowakisch",
+ "Language_Japanese": "Japanisch",
+ "Language_Chinese": "Chinesisch",
"Last_login": "Letzte Anmeldung",
"Last_Message_At": "Letzte Nachricht am",
"Last_Message": "Letzte Nachricht",
@@ -2043,8 +2051,6 @@
"Prune_Warning_between": "Dadurch werden alle% s in% s zwischen% s und% s gelöscht.",
"Pruning_messages": "Lösche Nachrichten ...",
"Pruning_files": "Lösche Dateien ...",
- "messages_pruned": "Nachrichten gelöscht",
- "files_pruned": "Dateien gelöscht",
"Public_Channel": "Öffentlicher Kanal",
"Public_Community": "Öffentliche Community",
"Public_Relations": "Public Relations",
@@ -2527,6 +2533,8 @@
"thread-welcome" : "Danke __username__, dass Du einen neuen Thread angelegt hast! Ich habe für Dich Mitglieder aus __parentChannel__ eingeladen. Tipp: mit \"@all\" kannst Du sie anstupsen, wenn sich länger niemand melden sollte ;)",
"Threading_description": "Erstelle einen Thread, um wichtigen Dingen mehr Raum zu geben. Dort kannst Du mit allen verfügbaren Mitgliedern schreiben, ohne andere zu stören. So sorgst Du für etwas mehr Ordnung in Eurem Chat.",
"Thread_from_context_menu": "Threads im Kontext-Menü",
+ "Thread_invitations_threshold": "Max. Anzahl automatisch einzuladender Benutzer",
+ "Thread_invitations_threshold_description": "Max. Anzahl der Benutzer, die automatisch zu einem öffentlichen Thread hinzugezogen werden",
"Threading_context_menu_button": "Separater Button",
"Threading_context_menu_none": "Unsichtbar",
"Thread_creation_on_home": "Threads von der Home-Seite aus anlegen",
diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json
index 46986b2d6180..43c165cec8d2 100644
--- a/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -357,9 +357,15 @@
"AutoLinker_UrlsRegExp": "AutoLinker URL Regular Expression",
"Automatic_Translation": "Automatic Translation",
"AutoTranslate_Change_Language_Description": "Changing the auto-translate language does not translate previous messages.",
+ "AutoTranslate_APIKey": "API Key",
+ "AutoTranslate_DBS": "DBS Business Hub",
+ "AutoTranslate_DeepL": "DeepL",
+ "AutoTranslate_Google": "Google",
"AutoTranslate_Enabled": "Enable Auto-Translate",
"AutoTranslate_Enabled_Description": "Enabling auto-translation will allow people with the auto-translate
permission to have all messages automatically translated into their selected language. Fees may apply, see Google's Documentation",
"AutoTranslate_GoogleAPIKey": "Google API Key",
+ "AutoTranslate_ServiceProvider": "Service Provider",
+ "AutoTranslate_ServiceProviderURL":"Service Provider url",
"Available": "Available",
"Available_agents": "Available agents",
"Avatar": "Avatar",
@@ -1486,6 +1492,17 @@
"Knowledge_Base": "Knowledge Base",
"Label": "Label",
"Language": "Language",
+ "Language_English": "English",
+ "Language_German": "German",
+ "Language_French": "French",
+ "Language_Spanish": "Spanish",
+ "Language_Italian": "Italian",
+ "Language_Dutch": "Dutch",
+ "Language_Polish": "Polish",
+ "Language_Romanian": "Romanian",
+ "Language_Slovak": "Slovakian",
+ "Language_Japanese": "Japanese",
+ "Language_Chinese": "Chinsese",
"Language_Not_set": "No specific",
"Language_Version": "English Version",
"Last_login": "Last login",
@@ -2514,6 +2531,8 @@
"thread-created" : "I started a new __channelLink__",
"thread-welcome" : "Thanks __username__ for creating a thread! I invited some members from __parentChannel__ who shall be able to help you. Hint: You can poke them with \"@all\", in case there's nothing happening for a longer time ;)",
"Thread_from_context_menu": "Threads in context-menu",
+ "Thread_invitations_threshold": "Max. users to be automatically invited",
+ "Thread_invitations_threshold_description": "Max. count of users who are automatically being invited into a public thread",
"Threading_context_menu_button": "Dedicated button",
"Threading_context_menu_none": "Invisible",
"Threading_description": "Help keeping an overview about what's going on! By creating a thread, a sub-channel of the one you selected is created and both are linked.",
diff --git a/packages/rocketchat-lib/rocketchat.info b/packages/rocketchat-lib/rocketchat.info
index 72693c8bf219..be04271a687e 100644
--- a/packages/rocketchat-lib/rocketchat.info
+++ b/packages/rocketchat-lib/rocketchat.info
@@ -1,3 +1,3 @@
{
- "version": "0.68.5-0.9.1"
+ "version": "0.68.5-0.9.2"
}
diff --git a/packages/rocketchat-lib/server/methods/sendInvitationEmail.js b/packages/rocketchat-lib/server/methods/sendInvitationEmail.js
index 72b7a8fdddef..c5f532a30643 100644
--- a/packages/rocketchat-lib/server/methods/sendInvitationEmail.js
+++ b/packages/rocketchat-lib/server/methods/sendInvitationEmail.js
@@ -8,7 +8,7 @@ Meteor.methods({
method: 'sendInvitationEmail'
});
}
- if (!RocketChat.authz.hasRole(Meteor.userId(), 'admin')) {
+ if (!RocketChat.authz.hasPermission(Meteor.userId(), 'bulk-register-user')) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
method: 'sendInvitationEmail'
});
diff --git a/packages/rocketchat-mentions/Mentions.js b/packages/rocketchat-mentions/Mentions.js
index b0a3cbc0874f..4c164e89f7de 100644
--- a/packages/rocketchat-mentions/Mentions.js
+++ b/packages/rocketchat-mentions/Mentions.js
@@ -55,7 +55,9 @@ export default class {
return match;
}
- return `${ prefix }${ `#${ name }` }`;
+ const channel = message.channels && message.channels.find(c => c.name === name);
+ const roomNameorId = channel ? channel._id : name;
+ return `${ prefix }${ `#${ name }` }`;
});
}
getUserMentions(str) {
diff --git a/packages/rocketchat-mentions/server/server.js b/packages/rocketchat-mentions/server/server.js
index 0aa5074b0779..cb407abb1fe4 100644
--- a/packages/rocketchat-mentions/server/server.js
+++ b/packages/rocketchat-mentions/server/server.js
@@ -7,7 +7,7 @@ const mention = new MentionsServer({
getUsers: (usernames) => Meteor.users.find({ username: {$in: _.unique(usernames)}}, { fields: {_id: true, username: true, name: 1 }}).fetch(),
getUser: (userId) => RocketChat.models.Users.findOneById(userId),
getTotalChannelMembers: (rid) => RocketChat.models.Subscriptions.findByRoomId(rid).count(),
- getChannels: (channels) => RocketChat.models.Rooms.find({ name: {$in: _.unique(channels)}, t: 'c' }, { fields: {_id: 1, name: 1 }}).fetch(),
+ getChannels: (channels) => RocketChat.models.Rooms.find({ name: {$in: _.unique(channels)}, t: {$in: ['c', 'p']} }, { fields: {_id: 1, name: 1 }}).fetch(),
onMaxRoomMembersExceeded({ sender, rid }) {
// Get the language of the user for the error notification.
const language = this.getUser(sender._id).language;
diff --git a/packages/rocketchat-mentions/tests/client.tests.js b/packages/rocketchat-mentions/tests/client.tests.js
index 19825eafa8d1..b925709a03c4 100644
--- a/packages/rocketchat-mentions/tests/client.tests.js
+++ b/packages/rocketchat-mentions/tests/client.tests.js
@@ -184,7 +184,7 @@ describe('Mention', function() {
});
const message = {
mentions:[{username:'rocket.cat', name: 'Rocket.Cat'}, {username:'admin', name: 'Admin'}, {username: 'me', name: 'Me'}, {username: 'specialchars', name:''}],
- channels: [{name: 'general'}, {name: 'rocket.cat'}]
+ channels: [{name: 'general', _id: '42'}, {name: 'rocket.cat', _id: '169'}]
};
describe('replace methods', function() {
describe('replaceUsers', () => {
@@ -252,16 +252,16 @@ describe('replace methods', function() {
describe('replaceChannels', () => {
it('should render for #general', () => {
const result = mention.replaceChannels('#general', message);
- assert.equal('#general', result);
+ assert.equal('#general', result);
});
const str2 = '#rocket.cat';
it(`should render for ${ str2 }`, () => {
const result = mention.replaceChannels(str2, message);
- assert.equal(result, `${ str2 }`);
+ assert.equal(result, `${ str2 }`);
});
it(`should render for "hello ${ str2 }"`, () => {
const result = mention.replaceChannels(`hello ${ str2 }`, message);
- assert.equal(result, `hello ${ str2 }`);
+ assert.equal(result, `hello ${ str2 }`);
});
it('should render for unknow/private channel "hello #unknow"', () => {
const result = mention.replaceChannels('hello #unknow', message);
@@ -273,12 +273,12 @@ describe('replace methods', function() {
it('should render for #general', () => {
message.html = '#general';
const result = mention.parse(message, 'me');
- assert.equal('#general', result.html);
+ assert.equal('#general', result.html);
});
it('should render for "#general and @rocket.cat', () => {
message.html = '#general and @rocket.cat';
const result = mention.parse(message, 'me');
- assert.equal('#general and @rocket.cat', result.html);
+ assert.equal('#general and @rocket.cat', result.html);
});
it('should render for "', () => {
message.html = '';
@@ -299,12 +299,12 @@ describe('replace methods', function() {
it('should render for #general', () => {
message.html = '#general';
const result = mention.parse(message, 'me');
- assert.equal('#general', result.html);
+ assert.equal('#general', result.html);
});
it('should render for "#general and @rocket.cat', () => {
message.html = '#general and @rocket.cat';
const result = mention.parse(message, 'me');
- assert.equal('#general and Rocket.Cat', result.html);
+ assert.equal('#general and Rocket.Cat', result.html);
});
it('should render for "', () => {
message.html = '';
diff --git a/packages/rocketchat-ui-admin/client/users/adminInviteUser.html b/packages/rocketchat-ui-admin/client/users/adminInviteUser.html
index 32b0c3bf2db8..836c7bd88614 100644
--- a/packages/rocketchat-ui-admin/client/users/adminInviteUser.html
+++ b/packages/rocketchat-ui-admin/client/users/adminInviteUser.html
@@ -1,5 +1,5 @@
- {{#if isAdmin}}
+ {{#if isAllowed}}