diff --git a/.docker/latest/Dockerfile b/.docker/Dockerfile similarity index 96% rename from .docker/latest/Dockerfile rename to .docker/Dockerfile index 77b309ea5830e8d464d598370aef0321da4d31b0..beaccef6157ceb62633a4485062fcab562e44d36 100644 --- a/.docker/latest/Dockerfile +++ b/.docker/Dockerfile @@ -1,6 +1,6 @@ FROM rocketchat/base:4 -ENV RC_VERSION latest +ENV RC_VERSION 0.57.0-develop MAINTAINER buildmaster@rocket.chat diff --git a/.docker/develop/Dockerfile b/.docker/develop/Dockerfile deleted file mode 100644 index b14fd89de4d41f1f32603511ffaefae9e80bda9f..0000000000000000000000000000000000000000 --- a/.docker/develop/Dockerfile +++ /dev/null @@ -1,38 +0,0 @@ -FROM rocketchat/base:4 - -ENV RC_VERSION develop - -MAINTAINER buildmaster@rocket.chat - -RUN set -x \ - && curl -SLf "https://rocket.chat/releases/${RC_VERSION}/download" -o rocket.chat.tgz \ - && curl -SLf "https://rocket.chat/releases/${RC_VERSION}/asc" -o rocket.chat.tgz.asc \ - && mkdir /app \ - && gpg --verify rocket.chat.tgz.asc \ - && mkdir -p /app \ - && tar -zxf rocket.chat.tgz -C /app \ - && rm rocket.chat.tgz rocket.chat.tgz.asc \ - && cd /app/bundle/programs/server \ - && npm install \ - && npm cache clear \ - && 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 \ - MONGO_OPLOG_URL=mongodb://mongo:27017/local \ - HOME=/tmp \ - PORT=3000 \ - ROOT_URL=http://localhost:3000 \ - Accounts_AvatarStorePath=/app/uploads - -EXPOSE 3000 - -CMD ["node", "main.js"] diff --git a/.docker/release-candidate/Dockerfile b/.docker/release-candidate/Dockerfile deleted file mode 100644 index 192b8e95c0194a5bdae2bdc18dc304b9ce967112..0000000000000000000000000000000000000000 --- a/.docker/release-candidate/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -FROM rocketchat/base:4 - -ENV RC_VERSION release-candidate - -MAINTAINER buildmaster@rocket.chat - -RUN set -x \ - && curl -SLf "https://rocket.chat/releases/${RC_VERSION}/download" -o rocket.chat.tgz \ - && curl -SLf "https://rocket.chat/releases/${RC_VERSION}/asc" -o rocket.chat.tgz.asc \ - && mkdir /app \ - && gpg --verify rocket.chat.tgz.asc \ - && mkdir -p /app \ - && tar -zxf rocket.chat.tgz -C /app \ - && rm rocket.chat.tgz rocket.chat.tgz.asc \ - && cd /app/bundle/programs/server \ - && npm install \ - && npm cache clear \ - && 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/.eslintrc b/.eslintrc index bbae28a07d6049da66a14d2b0a70b8b4bbe49749..9445d90e05c71cc78b5dde59059c5daca036792c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,10 @@ { "parserOptions": { "sourceType": "module", - "ecmaVersion": 2017 + "ecmaVersion": 2017, + "ecmaFeatures": { + "experimentalObjectRestSpread" : true, + } }, "env": { "browser": true, diff --git a/.sandstorm/sandstorm-pkgdef.capnp b/.sandstorm/sandstorm-pkgdef.capnp index 14ee2003b1780a2fb399406ffd9f6c1fa64bf459..c794307a7de6917a4eb18b8951ad8053743c6d8b 100644 --- a/.sandstorm/sandstorm-pkgdef.capnp +++ b/.sandstorm/sandstorm-pkgdef.capnp @@ -21,7 +21,7 @@ const pkgdef :Spk.PackageDefinition = ( appVersion = 62, # Increment this for every release. - appMarketingVersion = (defaultText = "0.56.0-develop"), + appMarketingVersion = (defaultText = "0.57.0-develop"), # Human-readable representation of appVersion. Should match the way you # identify versions of your app in documentation and marketing. diff --git a/.scripts/set-version.js b/.scripts/set-version.js new file mode 100644 index 0000000000000000000000000000000000000000..c1e1264ae468a560e2039043dd9c5dafa664c7e1 --- /dev/null +++ b/.scripts/set-version.js @@ -0,0 +1,190 @@ +/* eslint object-shorthand: 0, prefer-template: 0 */ + +const path = require('path'); +const fs = require('fs'); +const semver = require('semver'); +const inquirer = require('inquirer'); +const execSync = require('child_process').execSync; +const git = require('simple-git')(process.cwd()); + + +let pkgJson = {}; + +try { + pkgJson = require(path.resolve( + process.cwd(), + './package.json' + )); +} catch (err) { + console.error('no root package.json found'); +} + +const files = [ + './package.json', + './.sandstorm/sandstorm-pkgdef.capnp', + './.travis/snap.sh', + './.docker/Dockerfile', + './packages/rocketchat-lib/rocketchat.info' +]; + +class Actions { + static release_rc() { + function processVersion(version) { + // console.log('Updating files to version ' + version); + + files.forEach(function(file) { + const data = fs.readFileSync(file, 'utf8'); + + fs.writeFileSync(file, data.replace(pkgJson.version, version), 'utf8'); + }); + + execSync('conventional-changelog --config .github/changelog.js -i HISTORY.md -s'); + + inquirer.prompt([{ + type: 'confirm', + message: 'Commit files?', + name: 'commit' + }]).then(function(answers) { + if (!answers.commit) { + return; + } + + git.status((error, status) => { + inquirer.prompt([{ + type: 'checkbox', + message: 'Select files to commit?', + name: 'files', + choices: status.files.map(file => { return {name: `${ file.working_dir } ${ file.path }`, checked: true}; }) + }]).then(function(answers) { + if (answers.files.length) { + git.add(answers.files.map(file => file.slice(2)), () => { + git.commit(`Bump version to ${ version }`, () => { + inquirer.prompt([{ + type: 'confirm', + message: `Add tag ${ version }?`, + name: 'tag' + }]).then(function(answers) { + if (answers.tag) { + // TODO: Add annotated tag + git.addTag(version); + // TODO: Push + // Useg GitHub api to create the release with history + } + }); + }); + }); + } + }); + }); + }); + } + + + inquirer.prompt([{ + type: 'list', + message: `The current version is ${ pkgJson.version }. Update to version:`, + name: 'version', + choices: [ + semver.inc(pkgJson.version, 'prerelease', 'rc'), + // semver.inc(pkgJson.version, 'patch'), + 'custom' + ] + }]).then(function(answers) { + if (answers.version === 'custom') { + inquirer.prompt([{ + name: 'version', + message: 'Enter your custom version:' + }]).then(function(answers) { + processVersion(answers.version); + }); + } else { + processVersion(answers.version); + } + }); + } + + static release_gm() { + function processVersion(version) { + // console.log('Updating files to version ' + version); + + files.forEach(function(file) { + const data = fs.readFileSync(file, 'utf8'); + + fs.writeFileSync(file, data.replace(pkgJson.version, version), 'utf8'); + }); + + execSync('conventional-changelog --config .github/changelog.js -i HISTORY.md -s'); + // TODO improve HISTORY generation for GM + + inquirer.prompt([{ + type: 'confirm', + message: 'Commit files?', + name: 'commit' + }]).then(function(answers) { + if (!answers.commit) { + return; + } + + git.status((error, status) => { + inquirer.prompt([{ + type: 'checkbox', + message: 'Select files to commit?', + name: 'files', + choices: status.files.map(file => { return {name: `${ file.working_dir } ${ file.path }`, checked: true}; }) + }]).then(function(answers) { + if (answers.files.length) { + git.add(answers.files.map(file => file.slice(2)), () => { + git.commit(`Bump version to ${ version }`, () => { + inquirer.prompt([{ + type: 'confirm', + message: `Add tag ${ version }?`, + name: 'tag' + }]).then(function(answers) { + if (answers.tag) { + // TODO: Add annotated tag + git.addTag(version); + // TODO: Push + // Useg GitHub api to create the release with history + } + }); + }); + }); + } + }); + }); + }); + } + + + inquirer.prompt([{ + type: 'list', + message: `The current version is ${ pkgJson.version }. Update to version:`, + name: 'version', + choices: [ + semver.inc(pkgJson.version, 'patch'), + 'custom' + ] + }]).then(function(answers) { + if (answers.version === 'custom') { + inquirer.prompt([{ + name: 'version', + message: 'Enter your custom version:' + }]).then(function(answers) { + processVersion(answers.version); + }); + } else { + processVersion(answers.version); + } + }); + } +} + +git.status((err, status) => { + if (status.current === 'release-candidate') { + Actions.release_rc(); + } else if (status.current === 'master') { + Actions.release_gm(); + } else { + console.log(`No release action for branch ${ status.current }`); + } +}); diff --git a/.scripts/version.js b/.scripts/version.js index a99cf279946879f0e0af948e30b6c342d229c9d5..c4aeb68acd0f2a454578c6903dd2ffef72ae722e 100644 --- a/.scripts/version.js +++ b/.scripts/version.js @@ -1,8 +1,7 @@ /* eslint object-shorthand: 0, prefer-template: 0 */ const path = require('path'); -const fs = require('fs'); -let pkgJson = {}; +var pkgJson = {}; try { pkgJson = require(path.resolve( @@ -13,35 +12,4 @@ try { console.error('no root package.json found'); } -const readline = require('readline'); - -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout -}); - -const files = [ - './package.json', - './.sandstorm/sandstorm-pkgdef.capnp', - './.travis/snap.sh', - './packages/rocketchat-lib/rocketchat.info' -]; - -console.log('Current version:', pkgJson.version); -rl.question('New version: ', function(version) { - rl.close(); - - version = version.trim(); - - if (version === '') { - return; - } - - console.log('Updating files to version ' + version); - - files.forEach(function(file) { - const data = fs.readFileSync(file, 'utf8'); - - fs.writeFileSync(file, data.replace(pkgJson.version, version), 'utf8'); - }); -}); +console.log(pkgJson.version); diff --git a/.travis.yml b/.travis.yml index 09bfc03484ba452e68017397e6886774bbf969d8..7d8b89102247b956faa546df60cc57ebdd113723 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ services: branches: only: - develop - - release-candidate - "/^\\d+\\.\\d+\\.\\d+(-rc\\.\\d+)?$/" git: depth: 1 @@ -73,16 +72,24 @@ before_deploy: - ".travis/namefiles.sh" - echo ".travis/sandstorm.sh" deploy: - provider: s3 - access_key_id: AKIAIKIA7H7D47KUHYCA - secret_access_key: "$ACCESSKEY" - bucket: download.rocket.chat - skip_cleanup: true - upload_dir: build - local_dir: "$ROCKET_DEPLOY_DIR" - on: - condition: "$TRAVIS_PULL_REQUEST=false" - all_branches: true + - provider: s3 + access_key_id: AKIAIKIA7H7D47KUHYCA + secret_access_key: "$ACCESSKEY" + bucket: download.rocket.chat + skip_cleanup: true + upload_dir: build + local_dir: "$ROCKET_DEPLOY_DIR" + on: + condition: "$TRAVIS_PULL_REQUEST=false" + all_branches: true + # - provider: releases + # api-key: "$GITHUB_TOKEN" + # file_glob: true + # file: build/* + # skip_cleanup: true + # on: + # tags: true + after_deploy: - ".travis/docker.sh" - ".travis/update-releases.sh" diff --git a/.travis/setartname.sh b/.travis/setartname.sh index cf8f9a150c9a00d168854f2442dfec92ebe43c85..35ba8056881a895d234011429ff63a2eb397fec7 100755 --- a/.travis/setartname.sh +++ b/.travis/setartname.sh @@ -1,6 +1,6 @@ -if [[ $TRAVIS_TAG ]] +if [[ $TRAVIS_BRANCH ]] then - export ARTIFACT_NAME="$TRAVIS_TAG"; + export ARTIFACT_NAME="$(meteor npm run version --silent).$TRAVIS_BUILD_NUMBER" else - export ARTIFACT_NAME="$TRAVIS_BRANCH"; + export ARTIFACT_NAME="$(meteor npm run version --silent)" fi diff --git a/.travis/snap.sh b/.travis/snap.sh index d5ad25ba5c6805828b8d0d7f250f2be024cf7ee0..8b602fa558c03cee5e58c6c374cde8794cb63d24 100755 --- a/.travis/snap.sh +++ b/.travis/snap.sh @@ -17,7 +17,7 @@ elif [[ $TRAVIS_TAG ]]; then RC_VERSION=$TRAVIS_TAG else CHANNEL=edge - RC_VERSION=0.56.0-develop + RC_VERSION=0.57.0-develop fi echo "Preparing to trigger a snap release for $CHANNEL channel" diff --git a/HISTORY.md b/HISTORY.md index a286b0d5590e65f494efeb5d6d0d664d4125b08e..4e732d59c737c668eb5679b4047dc6e80db3df0d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,29 +1,199 @@ -<a name="0.56.0-develop"></a> -# 0.56.0-develop (2017-04-18) - +<a name="0.56.0"></a> +# 0.56.0 (2017-05-15) ### New Features +- [#6881](https://github.com/RocketChat/Rocket.Chat/pull/6881) Add a pointer cursor to message images - [#6615](https://github.com/RocketChat/Rocket.Chat/pull/6615) Add a setting to not run outgoing integrations on message edits +- [#5373](https://github.com/RocketChat/Rocket.Chat/pull/5373) Add option on Channel Settings: Hide Notifications and Hide Unread Room Status ([#2707](https://github.com/RocketChat/Rocket.Chat/issue/2707), [#2143](https://github.com/RocketChat/Rocket.Chat/issue/2143)) +- [#6807](https://github.com/RocketChat/Rocket.Chat/pull/6807) create a method 'create token' +- [#6827](https://github.com/RocketChat/Rocket.Chat/pull/6827) Make channels.info accept roomName, just like groups.info +- [#6797](https://github.com/RocketChat/Rocket.Chat/pull/6797) Option to allow to signup as anonymous +- [#6722](https://github.com/RocketChat/Rocket.Chat/pull/6722) Remove lesshat +- [#6842](https://github.com/RocketChat/Rocket.Chat/pull/6842) Snap ARM support - [#6692](https://github.com/RocketChat/Rocket.Chat/pull/6692) Use tokenSentVia parameter for clientid/secret to token endpoint +- [#6940](https://github.com/RocketChat/Rocket.Chat/pull/6940) Add SMTP settings for Protocol and Pool +- [#6938](https://github.com/RocketChat/Rocket.Chat/pull/6938) Improve CI/Docker build/release +- [#6953](https://github.com/RocketChat/Rocket.Chat/pull/6953) Show info about multiple instances at admin page ### Bug Fixes +- [#6845](https://github.com/RocketChat/Rocket.Chat/pull/6845) Added helper for testing if the current user matches the params +- [#6737](https://github.com/RocketChat/Rocket.Chat/pull/6737) Archiving Direct Messages +- [#6734](https://github.com/RocketChat/Rocket.Chat/pull/6734) Bug with incoming integration (0.55.1) +- [#6768](https://github.com/RocketChat/Rocket.Chat/pull/6768) CSV importer: require that there is some data in the zip, not ALL data - [#6709](https://github.com/RocketChat/Rocket.Chat/pull/6709) emoji picker exception +- [#6721](https://github.com/RocketChat/Rocket.Chat/pull/6721) Fix Caddy by forcing go 1.7 as needed by one of caddy's dependencies +- [#6798](https://github.com/RocketChat/Rocket.Chat/pull/6798) Fix iframe wise issues +- [#6704](https://github.com/RocketChat/Rocket.Chat/pull/6704) Fix message types +- [#6760](https://github.com/RocketChat/Rocket.Chat/pull/6760) Hides nav buttons when selecting own profile +- [#6747](https://github.com/RocketChat/Rocket.Chat/pull/6747) Incorrect error message when creating channel +- [#6800](https://github.com/RocketChat/Rocket.Chat/pull/6800) Quoted and replied messages not retaining the original message's alias +- [#6796](https://github.com/RocketChat/Rocket.Chat/pull/6796) REST API user.update throwing error due to rate limiting +- [#6767](https://github.com/RocketChat/Rocket.Chat/pull/6767) Search full name on client side +- [#6758](https://github.com/RocketChat/Rocket.Chat/pull/6758) Sort by real name if use real name setting is enabled +- [#6861](https://github.com/RocketChat/Rocket.Chat/pull/6861) start/unstar message +- [#6896](https://github.com/RocketChat/Rocket.Chat/pull/6896) Users status on main menu always offline +- [#6923](https://github.com/RocketChat/Rocket.Chat/pull/6923) Not showing unread count on electron app’s icon +- [#6939](https://github.com/RocketChat/Rocket.Chat/pull/6939) Compile CSS color variables +- [#6935](https://github.com/RocketChat/Rocket.Chat/pull/6935) Error when trying to show preview of undefined filetype +- [#6955](https://github.com/RocketChat/Rocket.Chat/pull/6955) Remove spaces from env PORT and INSTANCE_IP +- [#6968](https://github.com/RocketChat/Rocket.Chat/pull/6968) make channels.create API check for create-c + + +<details> +<summary>Others</summary> + +- [#5986](https://github.com/RocketChat/Rocket.Chat/pull/5986) Anonymous use +- [#6368](https://github.com/RocketChat/Rocket.Chat/pull/6368) Breaking long URLS to prevent overflow +- [#6671](https://github.com/RocketChat/Rocket.Chat/pull/6671) Convert Katex Package to Js +- [#6780](https://github.com/RocketChat/Rocket.Chat/pull/6780) Convert Mailer Package to Js +- [#6694](https://github.com/RocketChat/Rocket.Chat/pull/6694) Convert markdown to js +- [#6689](https://github.com/RocketChat/Rocket.Chat/pull/6689) Convert Mentions-Flextab Package to Js +- [#6781](https://github.com/RocketChat/Rocket.Chat/pull/6781) Convert Message-Star Package to js +- [#6688](https://github.com/RocketChat/Rocket.Chat/pull/6688) Convert Oembed Package to Js +- [#6672](https://github.com/RocketChat/Rocket.Chat/pull/6672) Converted rocketchat-lib 3 +- [#6654](https://github.com/RocketChat/Rocket.Chat/pull/6654) disable proxy configuration +- [#6816](https://github.com/RocketChat/Rocket.Chat/pull/6816) LingoHub based on develop +- [#6715](https://github.com/RocketChat/Rocket.Chat/pull/6715) LingoHub based on develop +- [#6703](https://github.com/RocketChat/Rocket.Chat/pull/6703) LingoHub based on develop +- [#6858](https://github.com/RocketChat/Rocket.Chat/pull/6858) Meteor update +- [#6706](https://github.com/RocketChat/Rocket.Chat/pull/6706) meteor update to 1.4.4 +- [#6804](https://github.com/RocketChat/Rocket.Chat/pull/6804) Missing useful fields in admin user list [#5110](https://github.com/RocketChat/Rocket.Chat/issue/5110) +- [#6593](https://github.com/RocketChat/Rocket.Chat/pull/6593) Rocketchat lib2 +</details> + + + +<details> +<summary>Details</summary> + +## 0.56.0-rc.7 (2017-05-15) + + +### Bug Fixes + +- [#6968](https://github.com/RocketChat/Rocket.Chat/pull/6968) make channels.create API check for create-c + + +## 0.56.0-rc.5 (2017-05-11) + + +### Bug Fixes + +- [#6935](https://github.com/RocketChat/Rocket.Chat/pull/6935) Error when trying to show preview of undefined filetype +- [#6955](https://github.com/RocketChat/Rocket.Chat/pull/6955) Remove spaces from env PORT and INSTANCE_IP + + +## 0.56.0-rc.4 (2017-05-11) + + +### New Features + +- [#6953](https://github.com/RocketChat/Rocket.Chat/pull/6953) Show info about multiple instances at admin page + + +## 0.56.0-rc.3 (2017-05-10) + + +### New Features + +- [#6940](https://github.com/RocketChat/Rocket.Chat/pull/6940) Add SMTP settings for Protocol and Pool +- [#6938](https://github.com/RocketChat/Rocket.Chat/pull/6938) Improve CI/Docker build/release + + +### Bug Fixes + +- [#6939](https://github.com/RocketChat/Rocket.Chat/pull/6939) Compile CSS color variables + + +## 0.56.0-rc.2 (2017-05-09) + + +### Bug Fixes + +- [#6923](https://github.com/RocketChat/Rocket.Chat/pull/6923) Not showing unread count on electron app’s icon + + +## 0.56.0-rc.1 (2017-05-05) + + +### Bug Fixes + +- [#6896](https://github.com/RocketChat/Rocket.Chat/pull/6896) Users status on main menu always offline + + +## 0.56.0-rc.0 (2017-05-04) + + +### New Features + +- [#6881](https://github.com/RocketChat/Rocket.Chat/pull/6881) Add a pointer cursor to message images +- [#6615](https://github.com/RocketChat/Rocket.Chat/pull/6615) Add a setting to not run outgoing integrations on message edits +- [#5373](https://github.com/RocketChat/Rocket.Chat/pull/5373) Add option on Channel Settings: Hide Notifications and Hide Unread Room Status ([#2707](https://github.com/RocketChat/Rocket.Chat/issue/2707), [#2143](https://github.com/RocketChat/Rocket.Chat/issue/2143)) +- [#6807](https://github.com/RocketChat/Rocket.Chat/pull/6807) create a method 'create token' +- [#6827](https://github.com/RocketChat/Rocket.Chat/pull/6827) Make channels.info accept roomName, just like groups.info +- [#6797](https://github.com/RocketChat/Rocket.Chat/pull/6797) Option to allow to signup as anonymous +- [#6722](https://github.com/RocketChat/Rocket.Chat/pull/6722) Remove lesshat +- [#6842](https://github.com/RocketChat/Rocket.Chat/pull/6842) Snap ARM support +- [#6692](https://github.com/RocketChat/Rocket.Chat/pull/6692) Use tokenSentVia parameter for clientid/secret to token endpoint + + +### Bug Fixes + +- [#6845](https://github.com/RocketChat/Rocket.Chat/pull/6845) Added helper for testing if the current user matches the params +- [#6737](https://github.com/RocketChat/Rocket.Chat/pull/6737) Archiving Direct Messages +- [#6734](https://github.com/RocketChat/Rocket.Chat/pull/6734) Bug with incoming integration (0.55.1) +- [#6768](https://github.com/RocketChat/Rocket.Chat/pull/6768) CSV importer: require that there is some data in the zip, not ALL data - [#6709](https://github.com/RocketChat/Rocket.Chat/pull/6709) emoji picker exception +- [#6721](https://github.com/RocketChat/Rocket.Chat/pull/6721) Fix Caddy by forcing go 1.7 as needed by one of caddy's dependencies +- [#6798](https://github.com/RocketChat/Rocket.Chat/pull/6798) Fix iframe wise issues - [#6704](https://github.com/RocketChat/Rocket.Chat/pull/6704) Fix message types +- [#6760](https://github.com/RocketChat/Rocket.Chat/pull/6760) Hides nav buttons when selecting own profile +- [#6747](https://github.com/RocketChat/Rocket.Chat/pull/6747) Incorrect error message when creating channel +- [#6800](https://github.com/RocketChat/Rocket.Chat/pull/6800) Quoted and replied messages not retaining the original message's alias +- [#6796](https://github.com/RocketChat/Rocket.Chat/pull/6796) REST API user.update throwing error due to rate limiting +- [#6767](https://github.com/RocketChat/Rocket.Chat/pull/6767) Search full name on client side +- [#6758](https://github.com/RocketChat/Rocket.Chat/pull/6758) Sort by real name if use real name setting is enabled +- [#6861](https://github.com/RocketChat/Rocket.Chat/pull/6861) start/unstar message <details> <summary>Others</summary> +- [#5986](https://github.com/RocketChat/Rocket.Chat/pull/5986) Anonymous use +- [#6368](https://github.com/RocketChat/Rocket.Chat/pull/6368) Breaking long URLS to prevent overflow +- [#6671](https://github.com/RocketChat/Rocket.Chat/pull/6671) Convert Katex Package to Js +- [#6780](https://github.com/RocketChat/Rocket.Chat/pull/6780) Convert Mailer Package to Js +- [#6694](https://github.com/RocketChat/Rocket.Chat/pull/6694) Convert markdown to js +- [#6689](https://github.com/RocketChat/Rocket.Chat/pull/6689) Convert Mentions-Flextab Package to Js +- [#6781](https://github.com/RocketChat/Rocket.Chat/pull/6781) Convert Message-Star Package to js +- [#6688](https://github.com/RocketChat/Rocket.Chat/pull/6688) Convert Oembed Package to Js +- [#6672](https://github.com/RocketChat/Rocket.Chat/pull/6672) Converted rocketchat-lib 3 +- [#6654](https://github.com/RocketChat/Rocket.Chat/pull/6654) disable proxy configuration +- [#6816](https://github.com/RocketChat/Rocket.Chat/pull/6816) LingoHub based on develop +- [#6715](https://github.com/RocketChat/Rocket.Chat/pull/6715) LingoHub based on develop - [#6703](https://github.com/RocketChat/Rocket.Chat/pull/6703) LingoHub based on develop +- [#6858](https://github.com/RocketChat/Rocket.Chat/pull/6858) Meteor update - [#6706](https://github.com/RocketChat/Rocket.Chat/pull/6706) meteor update to 1.4.4 +- [#6804](https://github.com/RocketChat/Rocket.Chat/pull/6804) Missing useful fields in admin user list [#5110](https://github.com/RocketChat/Rocket.Chat/issue/5110) +- [#6593](https://github.com/RocketChat/Rocket.Chat/pull/6593) Rocketchat lib2 +</details> + </details> +<a name="0.55.1"></a> +## 0.55.1 (2017-04-19) + + +### Bug Fixes + +- [#6734](https://github.com/RocketChat/Rocket.Chat/pull/6734) Bug with incoming integration (0.55.1) + + <a name="0.55.0"></a> # 0.55.0 (2017-04-18) diff --git a/client/methods/deleteMessage.js b/client/methods/deleteMessage.js index e7acb1c30aa31b643ce644aaf8114c5d9409c819..8a86f3affc253152d5e2592b9560dcc17e651bf3 100644 --- a/client/methods/deleteMessage.js +++ b/client/methods/deleteMessage.js @@ -11,18 +11,18 @@ Meteor.methods({ message = ChatMessage.findOne({ _id: message._id }); const hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', message.rid); + const forceDelete = RocketChat.authz.hasAtLeastOnePermission('force-delete-message', message.rid); const deleteAllowed = RocketChat.settings.get('Message_AllowDeleting'); let deleteOwn = false; + if (message && message.u && message.u._id) { deleteOwn = message.u._id === Meteor.userId(); } - - if (!(hasPermission || (deleteAllowed && deleteOwn))) { + if (!(forceDelete || hasPermission || (deleteAllowed && deleteOwn))) { return false; } - const blockDeleteInMinutes = RocketChat.settings.get('Message_AllowDeleting_BlockDeleteInMinutes'); - if (_.isNumber(blockDeleteInMinutes) && blockDeleteInMinutes !== 0) { + if (!(forceDelete) || (_.isNumber(blockDeleteInMinutes) && blockDeleteInMinutes !== 0)) { if (message.ts) { const msgTs = moment(message.ts); if (msgTs) { diff --git a/package.json b/package.json index 5238246908b99327d7547d466f0630ceb1165749..925c63540e1581b69833a393a2638c87eed186c0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "Rocket.Chat", "description": "The Ultimate Open Source WebChat Platform", - "version": "0.56.0-develop", + "version": "0.57.0-develop", "author": { "name": "Rocket.Chat", "url": "https://rocket.chat/" @@ -44,7 +44,8 @@ "chimp-test": "chimp tests/chimp-config.js", "postinstall": "cd packages/rocketchat-katex && npm i", "version": "node .scripts/version.js", - "release": "npm run version && conventional-changelog --config .github/changelog.js -i HISTORY.md -s" + "set-version": "node .scripts/set-version.js", + "release": "npm run set-version --silent" }, "license": "MIT", "repository": { @@ -56,7 +57,7 @@ "email": "support@rocket.chat" }, "devDependencies": { - "chimp": "^0.48.0", + "chimp": "^0.49.0", "eslint": "^3.19.0", "stylelint": "^7.10.1", "supertest": "^3.0.0", @@ -66,15 +67,15 @@ "babel-runtime": "^6.23.0", "bcrypt": "^1.0.2", "codemirror": "^5.25.2", - "file-type": "^4.2.0", + "file-type": "^4.3.0", "highlight.js": "^9.11.0", "jquery": "^3.2.1", - "mime-db": "^1.27.0", + "mime-db": "^1.28.0", "mime-type": "^3.0.4", "moment": "^2.18.1", "moment-timezone": "^0.5.13", "photoswipe": "^4.1.2", - "prom-client": "^8.1.1", + "prom-client": "^9.0.0", "semver": "^5.3.0", "toastr": "^2.1.2" } diff --git a/packages/meteor-accounts-saml/saml_utils.js b/packages/meteor-accounts-saml/saml_utils.js index 5d8396e88eca33163aea4136d608eed2b3ec1f3c..02660f2dcdc8657374445f65fcd176cb2efa7118 100644 --- a/packages/meteor-accounts-saml/saml_utils.js +++ b/packages/meteor-accounts-saml/saml_utils.js @@ -432,12 +432,38 @@ SAML.prototype.validateResponse = function(samlResponse, relayState, callback) { let decryptionCert; SAML.prototype.generateServiceProviderMetadata = function(callbackUrl) { - let keyDescriptor = null; - if (!decryptionCert) { decryptionCert = this.options.privateCert; } + if (!this.options.callbackUrl && !callbackUrl) { + throw new Error( + 'Unable to generate service provider metadata when callbackUrl option is not set'); + } + + const metadata = { + 'EntityDescriptor': { + '@xmlns': 'urn:oasis:names:tc:SAML:2.0:metadata', + '@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#', + '@entityID': this.options.issuer, + 'SPSSODescriptor': { + '@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol', + 'SingleLogoutService': { + '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + '@Location': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/`, + '@ResponseLocation': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/` + }, + 'NameIDFormat': this.options.identifierFormat, + 'AssertionConsumerService': { + '@index': '1', + '@isDefault': 'true', + '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + '@Location': callbackUrl + } + } + } + }; + if (this.options.privateKey) { if (!decryptionCert) { throw new Error( @@ -448,7 +474,7 @@ SAML.prototype.generateServiceProviderMetadata = function(callbackUrl) { decryptionCert = decryptionCert.replace(/-+END CERTIFICATE-+\r?\n?/, ''); decryptionCert = decryptionCert.replace(/\r\n/g, '\n'); - keyDescriptor = { + metadata['EntityDescriptor']['SPSSODescriptor']['KeyDescriptor'] = { 'ds:KeyInfo': { 'ds:X509Data': { 'ds:X509Certificate': { @@ -477,35 +503,6 @@ SAML.prototype.generateServiceProviderMetadata = function(callbackUrl) { }; } - if (!this.options.callbackUrl && !callbackUrl) { - throw new Error( - 'Unable to generate service provider metadata when callbackUrl option is not set'); - } - - const metadata = { - 'EntityDescriptor': { - '@xmlns': 'urn:oasis:names:tc:SAML:2.0:metadata', - '@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#', - '@entityID': this.options.issuer, - 'SPSSODescriptor': { - '@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol', - 'KeyDescriptor': keyDescriptor, - 'SingleLogoutService': { - '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', - '@Location': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/`, - '@ResponseLocation': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/` - }, - 'NameIDFormat': this.options.identifierFormat, - 'AssertionConsumerService': { - '@index': '1', - '@isDefault': 'true', - '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', - '@Location': callbackUrl - } - } - } - }; - return xmlbuilder.create(metadata).end({ pretty: true, indent: ' ', diff --git a/packages/meteor-autocomplete/autocomplete-client.coffee b/packages/meteor-autocomplete/autocomplete-client.coffee deleted file mode 100755 index b58875785139042817b8edf03b41f032651c88c2..0000000000000000000000000000000000000000 --- a/packages/meteor-autocomplete/autocomplete-client.coffee +++ /dev/null @@ -1,367 +0,0 @@ -AutoCompleteRecords = new Mongo.Collection("autocompleteRecords") - -isServerSearch = (rule) -> _.isString(rule.collection) - -validateRule = (rule) -> - if rule.subscription? and not Match.test(rule.collection, String) - throw new Error("Collection name must be specified as string for server-side search") - - # XXX back-compat message, to be removed - if rule.callback? - console.warn("autocomplete no longer supports callbacks; use event listeners instead.") - -isWholeField = (rule) -> - # either '' or null both count as whole field. - return !rule.token - -getRegExp = (rule) -> - unless isWholeField(rule) - # Expressions for the range from the last word break to the current cursor position - new RegExp('(^|\\b|\\s)' + rule.token + '([\\w.]*)$') - else - # Whole-field behavior - word characters or spaces - new RegExp('(^)(.*)$') - -getFindParams = (rule, filter, limit) -> - # This is a different 'filter' - the selector from the settings - # We need to extend so that we don't copy over rule.filter - selector = _.extend({}, rule.filter || {}) - options = { limit: limit } - - # Match anything, no sort, limit X - return [ selector, options ] unless filter - - if rule.sort and rule.field - sortspec = {} - # Only sort if there is a filter, for faster performance on a match of anything - sortspec[rule.field] = 1 - options.sort = sortspec - - if _.isFunction(rule.selector) - # Custom selector - _.extend(selector, rule.selector(filter)) - else - selector[rule.field] = { - $regex: if rule.matchAll then filter else "^" + filter - # default is case insensitive search - empty string is not the same as undefined! - $options: if (typeof rule.options is 'undefined') then 'i' else rule.options - } - - return [ selector, options ] - -getField = (obj, str) -> - obj = obj[key] for key in str.split(".") - return obj - -class @AutoComplete - - @KEYS: [ - 40, # DOWN - 38, # UP - 13, # ENTER - 27, # ESCAPE - 9 # TAB - ] - - constructor: (settings) -> - @limit = settings.limit || 5 - @position = settings.position || "bottom" - - @rules = settings.rules - validateRule(rule) for rule in @rules - - @expressions = (getRegExp(rule) for rule in @rules) - - @matched = -1 - @loaded = true - - # Reactive dependencies for current matching rule and filter - @ruleDep = new Deps.Dependency - @filterDep = new Deps.Dependency - @loadingDep = new Deps.Dependency - - # autosubscribe to the record set published by the server based on the filter - # This will tear down server subscriptions when they are no longer being used. - @sub = null - @comp = Deps.autorun => - # Stop any existing sub immediately, don't wait - @sub?.stop() - - return unless (rule = @matchedRule()) and (filter = @getFilter()) isnt null - - # subscribe only for server-side collections - unless isServerSearch(rule) - @setLoaded(true) # Immediately loaded - return - - [ selector, options ] = getFindParams(rule, filter, @limit) - - # console.debug 'Subscribing to <%s> in <%s>.<%s>', filter, rule.collection, rule.field - @setLoaded(false) - subName = rule.subscription || "autocomplete-recordset" - @sub = Meteor.subscribe(subName, - selector, options, rule.collection, => @setLoaded(true)) - - teardown: -> - # Stop the reactive computation we started for this autocomplete instance - @comp.stop() - - # reactive getters and setters for @filter and the currently matched rule - matchedRule: -> - @ruleDep.depend() - if @matched >= 0 then @rules[@matched] else null - - setMatchedRule: (i) -> - @matched = i - @ruleDep.changed() - - getFilter: -> - @filterDep.depend() - return @filter - - setFilter: (x) -> - @filter = x - @filterDep.changed() - return @filter - - isLoaded: -> - @loadingDep.depend() - return @loaded - - setLoaded: (val) -> - return if val is @loaded # Don't cause redraws unnecessarily - @loaded = val - @loadingDep.changed() - - onKeyUp: -> - return unless @$element # Don't try to do this while loading - startpos = @element.selectionStart - val = @getText().substring(0, startpos) - - ### - Matching on multiple expressions. - We always go from a matched state to an unmatched one - before going to a different matched one. - ### - i = 0 - breakLoop = false - while i < @expressions.length - matches = val.match(@expressions[i]) - - # matching -> not matching - if not matches and @matched is i - @setMatchedRule(-1) - breakLoop = true - - # not matching -> matching - if matches and @matched is -1 - @setMatchedRule(i) - breakLoop = true - - # Did filter change? - if matches and @filter isnt matches[2] - @setFilter(matches[2]) - breakLoop = true - - break if breakLoop - i++ - - onKeyDown: (e) -> - return if @matched is -1 or (@constructor.KEYS.indexOf(e.keyCode) < 0) - - switch e.keyCode - when 9, 13 # TAB, ENTER - if @select() # Don't jump fields or submit if select successful - e.preventDefault() - e.stopPropagation() - # preventDefault needed below to avoid moving cursor when selecting - when 40 # DOWN - e.preventDefault() - @next() - when 38 # UP - e.preventDefault() - @prev() - when 27 # ESCAPE - @$element.blur() - @hideList() - - return - - onFocus: -> - # We need to run onKeyUp after the focus resolves, - # or the caret position (selectionStart) will not be correct - Meteor.defer => @onKeyUp() - - onBlur: -> - # We need to delay this so click events work - # TODO this is a bit of a hack; see if we can't be smarter - Meteor.setTimeout => - @hideList() - , 500 - - onItemClick: (doc, e) => @processSelection(doc, @rules[@matched]) - - onItemHover: (doc, e) -> - @tmplInst.$(".-autocomplete-item").removeClass("selected") - $(e.target).closest(".-autocomplete-item").addClass("selected") - - filteredList: -> - # @ruleDep.depend() # optional as long as we use depend on filter, because list will always get re-rendered - filter = @getFilter() # Reactively depend on the filter - return null if @matched is -1 - - rule = @rules[@matched] - # Don't display list unless we have a token or a filter (or both) - # Single field: nothing displayed until something is typed - return null unless rule.token or filter - - [ selector, options ] = getFindParams(rule, filter, @limit) - - Meteor.defer => @ensureSelection() - - # if server collection, the server has already done the filtering work - return AutoCompleteRecords.find({}, options) if isServerSearch(rule) - - # Otherwise, search on client - return rule.collection.find(selector, options) - - isShowing: -> - rule = @matchedRule() - # Same rules as above - showing = rule? and (rule.token or @getFilter()) - - # Do this after the render - if showing - Meteor.defer => - @positionContainer() - @ensureSelection() - - return showing - - # Replace text with currently selected item - select: -> - node = @tmplInst.find(".-autocomplete-item.selected") - return false unless node? - doc = Blaze.getData(node) - return false unless doc # Don't select if nothing matched - - @processSelection(doc, @rules[@matched]) - return true - - processSelection: (doc, rule) -> - replacement = getField(doc, rule.field) - - unless isWholeField(rule) - @replace(replacement, rule) - @hideList() - - else - # Empty string or doesn't exist? - # Single-field replacement: replace whole field - @setText(replacement) - - # Field retains focus, but list is hidden unless another key is pressed - # Must be deferred or onKeyUp will trigger and match again - # TODO this is a hack; see above - @onBlur() - - @$element.trigger("autocompleteselect", doc) - return - - # Replace the appropriate region - replace: (replacement) -> - startpos = @element.selectionStart - fullStuff = @getText() - val = fullStuff.substring(0, startpos) - val = val.replace(@expressions[@matched], "$1" + @rules[@matched].token + replacement) - posfix = fullStuff.substring(startpos, fullStuff.length) - separator = (if posfix.match(/^\s/) then "" else " ") - finalFight = val + separator + posfix - @setText finalFight - - newPosition = val.length + 1 - @element.setSelectionRange(newPosition, newPosition) - return - - hideList: -> - @setMatchedRule(-1) - @setFilter(null) - - getText: -> - return @$element.val() || @$element.text() - - setText: (text) -> - if @$element.is("input,textarea") - @$element.val(text) - else - @$element.html(text) - - ### - Rendering functions - ### - positionContainer: -> - # First render; Pick the first item and set css whenever list gets shown - position = @$element.position() - - rule = @matchedRule() - - offset = getCaretCoordinates(@element, @element.selectionStart) - - # In whole-field positioning, we don't move the container and make it the - # full width of the field. - if rule? and isWholeField(rule) - pos = - left: position.left - width: @$element.outerWidth() # position.offsetWidth - else # Normal positioning, at token word - pos = - left: position.left + offset.left - - # Position menu from top (above) or from bottom of caret (below, default) - if @position is "top" - pos.bottom = @$element.offsetParent().height() - position.top - offset.top - else - pos.top = position.top + offset.top + parseInt(@$element.css('font-size')) - - @tmplInst.$(".-autocomplete-container").css(pos) - - ensureSelection : -> - # Re-render; make sure selected item is something in the list or none if list empty - selectedItem = @tmplInst.$(".-autocomplete-item.selected") - - unless selectedItem.length - # Select anything - @tmplInst.$(".-autocomplete-item:first-child").addClass("selected") - - # Select next item in list - next: -> - currentItem = @tmplInst.$(".-autocomplete-item.selected") - return unless currentItem.length # Don't try to iterate an empty list - currentItem.removeClass("selected") - - next = currentItem.next() - if next.length - next.addClass("selected") - else # End of list or lost selection; Go back to first item - @tmplInst.$(".-autocomplete-item:first-child").addClass("selected") - - # Select previous item in list - prev: -> - currentItem = @tmplInst.$(".-autocomplete-item.selected") - return unless currentItem.length # Don't try to iterate an empty list - currentItem.removeClass("selected") - - prev = currentItem.prev() - if prev.length - prev.addClass("selected") - else # Beginning of list or lost selection; Go to end of list - @tmplInst.$(".-autocomplete-item:last-child").addClass("selected") - - # This doesn't need to be reactive because list already changes reactively - # and will cause all of the items to re-render anyway - currentTemplate: -> @rules[@matched].template - -AutocompleteTest = - records: AutoCompleteRecords - getRegExp: getRegExp - getFindParams: getFindParams diff --git a/packages/meteor-autocomplete/autocomplete-server.coffee b/packages/meteor-autocomplete/autocomplete-server.coffee deleted file mode 100755 index 192d5479bb20a72935a8730bb583391fee7100a6..0000000000000000000000000000000000000000 --- a/packages/meteor-autocomplete/autocomplete-server.coffee +++ /dev/null @@ -1,27 +0,0 @@ -class Autocomplete - @publishCursor: (cursor, sub) -> - # This also attaches an onStop callback to sub, so we don't need to worry about that. - # https://github.com/meteor/meteor/blob/devel/packages/mongo/collection.js - Mongo.Collection._publishCursor(cursor, sub, "autocompleteRecords") - -Meteor.publish 'autocomplete-recordset', (selector, options, collName) -> - collection = global[collName] - unless collection - throw new Error(collName + ' is not defined on the global namespace of the server.') - - # This is a semi-documented Meteor feature: - # https://github.com/meteor/meteor/blob/devel/packages/mongo-livedata/collection.js - unless collection._isInsecure() - Meteor._debug(collName + ' is a secure collection, therefore no data was returned because the client could compromise security by subscribing to arbitrary server collections via the browser console. Please write your own publish function.') - return [] # We need this for the subscription to be marked ready - - # guard against client-side DOS: hard limit to 50 - options.limit = Math.min(50, Math.abs(options.limit)) if options.limit - - # Push this into our own collection on the client so they don't interfere with other publications of the named collection. - # This also stops the observer automatically when the subscription is stopped. - Autocomplete.publishCursor( collection.find(selector, options), this) - - # Mark the subscription ready after the initial addition of documents. - this.ready() - diff --git a/packages/meteor-autocomplete/client/autocomplete-client.js b/packages/meteor-autocomplete/client/autocomplete-client.js new file mode 100755 index 0000000000000000000000000000000000000000..809f2fa77043a736705412ca3b105f05abecb7e5 --- /dev/null +++ b/packages/meteor-autocomplete/client/autocomplete-client.js @@ -0,0 +1,449 @@ +/* globals Deps, getCaretCoordinates*/ +const AutoCompleteRecords = new Mongo.Collection('autocompleteRecords'); + +const isServerSearch = function(rule) { + return _.isString(rule.collection); +}; + +const validateRule = function(rule) { + if (rule.subscription != null && !Match.test(rule.collection, String)) { + throw new Error('Collection name must be specified as string for server-side search'); + } + // XXX back-compat message, to be removed + if (rule.callback) { + console.warn('autocomplete no longer supports callbacks; use event listeners instead.'); + } +}; + +const isWholeField = function(rule) { + // either '' or null both count as whole field. + return !rule.token; +}; + +const getRegExp = function(rule) { + if (!isWholeField(rule)) { + // Expressions for the range from the last word break to the current cursor position + return new RegExp(`(^|\\b|\\s)${ rule.token }([\\w.]*)$`); + } else { + // Whole-field behavior - word characters or spaces + return new RegExp('(^)(.*)$'); + } +}; + +const getFindParams = function(rule, filter, limit) { + // This is a different 'filter' - the selector from the settings + // We need to extend so that we don't copy over rule.filter + const selector = _.extend({}, rule.filter || {}); + const options = { + limit + }; + if (!filter) { + // Match anything, no sort, limit X + return [selector, options]; + } + if (rule.sort && rule.field) { + const sortspec = {}; + // Only sort if there is a filter, for faster performance on a match of anything + sortspec[rule.field] = 1; + options.sort = sortspec; + } + if (_.isFunction(rule.selector)) { + // Custom selector + _.extend(selector, rule.selector(filter)); + } else { + selector[rule.field] = { + $regex: rule.matchAll ? filter : `^${ filter }`, + // default is case insensitive search - empty string is not the same as undefined! + $options: typeof rule.options === 'undefined' ? 'i' : rule.options + }; + } + return [selector, options]; +}; + +const getField = function(obj, str) { + const string = str.split('.'); + string.forEach(key => { + obj = obj[key]; + }); + return obj; +}; + +this.AutoComplete = class { + constructor(settings) { + this.KEYS = [40, 38, 13, 27, 9]; + this.limit = settings.limit || 5; + this.position = settings.position || 'bottom'; + this.rules = settings.rules; + const rules = this.rules; + + Object.keys(rules).forEach(key => { + const rule = rules[key]; + validateRule(rule); + }); + + this.expressions = (() => { + return Object.keys(rules).map(key => { + const rule = rules[key]; + return getRegExp(rule); + }); + })(); + this.matched = -1; + this.loaded = true; + + // Reactive dependencies for current matching rule and filter + this.ruleDep = new Deps.Dependency; + this.filterDep = new Deps.Dependency; + this.loadingDep = new Deps.Dependency; + + // Autosubscribe to the record set published by the server based on the filter + // This will tear down server subscriptions when they are no longer being used. + this.sub = null; + this.comp = Deps.autorun(() => { + const rule = this.matchedRule(); + const filter = this.getFilter(); + if (this.sub) { + // Stop any existing sub immediately, don't wait + this.sub.stop(); + } + if (!(rule && filter)) { + return; + } + + // subscribe only for server-side collections + if (!isServerSearch(rule)) { + this.setLoaded(true); + return; + } + const params = getFindParams(rule, filter, this.limit); + const selector = params[0]; + const options = params[1]; + + // console.debug 'Subscribing to <%s> in <%s>.<%s>', filter, rule.collection, rule.field + this.setLoaded(false); + const subName = rule.subscription || 'autocomplete-recordset'; + this.sub = Meteor.subscribe(subName, selector, options, rule.collection, () => { + this.setLoaded(true); + }); + }); + } + + teardown() { + // Stop the reactive computation we started for this autocomplete instance + this.comp.stop(); + } + + matchedRule() { + // reactive getters and setters for @filter and the currently matched rule + this.ruleDep.depend(); + if (this.matched >= 0) { + return this.rules[this.matched]; + } else { + return null; + } + } + + setMatchedRule(i) { + this.matched = i; + this.ruleDep.changed(); + } + + getFilter() { + this.filterDep.depend(); + return this.filter; + } + + setFilter(x) { + this.filter = x; + this.filterDep.changed(); + return this.filter; + } + + isLoaded() { + this.loadingDep.depend(); + return this.loaded; + } + + setLoaded(val) { + if (val === this.loaded) { + return; //Don't cause redraws unnecessarily + } + this.loaded = val; + this.loadingDep.changed(); + } + + onKeyUp() { + if (!this.$element) { + return; //Don't try to do this while loading + } + const startpos = this.element.selectionStart; + const val = this.getText().substring(0, startpos); + + /* + Matching on multiple expressions. + We always go from a matched state to an unmatched one + before going to a different matched one. + */ + let i = 0; + let breakLoop = false; + while (i < this.expressions.length) { + const matches = val.match(this.expressions[i]); + + // matching -> not matching + if (!matches && this.matched === i) { + this.setMatchedRule(-1); + breakLoop = true; + } + + // not matching -> matching + if (matches && this.matched === -1) { + this.setMatchedRule(i); + breakLoop = true; + } + + // Did filter change? + if (matches && this.filter !== matches[2]) { + this.setFilter(matches[2]); + breakLoop = true; + } + if (breakLoop) { + break; + } + i++; + } + } + + onKeyDown(e) { + if (this.matched === -1 || (this.KEYS.indexOf(e.keyCode) < 0)) { + return; + } + switch (e.keyCode) { + case 9: //TAB + case 13: //ENTER + if (this.select()) { //Don't jump fields or submit if select successful + e.preventDefault(); + e.stopPropagation(); + } + break; + // preventDefault needed below to avoid moving cursor when selecting + case 40: //DOWN + e.preventDefault(); + this.next(); + break; + case 38: //UP + e.preventDefault(); + this.prev(); + break; + case 27: //ESCAPE + this.$element.blur(); + this.hideList(); + } + } + + onFocus() { + // We need to run onKeyUp after the focus resolves, + // or the caret position (selectionStart) will not be correct + Meteor.defer(() => this.onKeyUp()); + } + + onBlur() { + // We need to delay this so click events work + // TODO this is a bit of a hack, see if we can't be smarter + Meteor.setTimeout(() => { + this.hideList(); + }, 500); + } + + onItemClick(doc) { + this.processSelection(doc, this.rules[this.matched]); + } + + onItemHover(doc, e) { + this.tmplInst.$('.-autocomplete-item').removeClass('selected'); + $(e.target).closest('.-autocomplete-item').addClass('selected'); + } + + filteredList() { + // @ruleDep.depend() # optional as long as we use depend on filter, because list will always get re-rendered + const filter = this.getFilter(); //Reactively depend on the filter + if (this.matched === -1) { + return null; + } + const rule = this.rules[this.matched]; + + // Don't display list unless we have a token or a filter (or both) + // Single field: nothing displayed until something is typed + if (!(rule.token || filter)) { + return null; + } + const params = getFindParams(rule, filter, this.limit); + const selector = params[0]; + const options = params[1]; + Meteor.defer(() => this.ensureSelection()); + + // if server collection, the server has already done the filtering work + if (isServerSearch(rule)) { + return AutoCompleteRecords.find({}, options); + } + // Otherwise, search on client + return rule.collection.find(selector, options); + } + + isShowing() { + const rule = this.matchedRule(); + // Same rules as above + const showing = rule && (rule.token || this.getFilter()); + + // Do this after the render + if (showing) { + Meteor.defer(() => { + this.positionContainer(); + this.ensureSelection(); + }); + } + return showing; + } + + // Replace text with currently selected item + select() { + const node = this.tmplInst.find('.-autocomplete-item.selected'); + if (node == null) { + return false; + } + const doc = Blaze.getData(node); + if (!doc) { + return false; //Don't select if nothing matched + + } + this.processSelection(doc, this.rules[this.matched]); + return true; + } + + processSelection(doc, rule) { + const replacement = getField(doc, rule.field); + if (!isWholeField(rule)) { + this.replace(replacement, rule); + this.hideList(); + } else { + + // Empty string or doesn't exist? + // Single-field replacement: replace whole field + this.setText(replacement); + + // Field retains focus, but list is hidden unless another key is pressed + // Must be deferred or onKeyUp will trigger and match again + // TODO this is a hack; see above + this.onBlur(); + } + this.$element.trigger('autocompleteselect', doc); + } + + + // Replace the appropriate region + replace(replacement) { + const startpos = this.element.selectionStart; + const fullStuff = this.getText(); + let val = fullStuff.substring(0, startpos); + val = val.replace(this.expressions[this.matched], `$1${ this.rules[this.matched].token }${ replacement }`); + const posfix = fullStuff.substring(startpos, fullStuff.length); + const separator = (posfix.match(/^\s/) ? '' : ' '); + const finalFight = val + separator + posfix; + this.setText(finalFight); + const newPosition = val.length + 1; + this.element.setSelectionRange(newPosition, newPosition); + } + + hideList() { + this.setMatchedRule(-1); + this.setFilter(null); + } + + getText() { + return this.$element.val() || this.$element.text(); + } + + setText(text) { + if (this.$element.is('input,textarea')) { + this.$element.val(text); + } else { + this.$element.html(text); + } + } + + + /* + Rendering functions + */ + + positionContainer() { + // First render; Pick the first item and set css whenever list gets shown + let pos; + const position = this.$element.position(); + const rule = this.matchedRule(); + const offset = getCaretCoordinates(this.element, this.element.selectionStart); + + // In whole-field positioning, we don't move the container and make it the + // full width of the field. + if (rule && isWholeField(rule)) { + pos = { + left: position.left, + width: this.$element.outerWidth() //position.offsetWidth + }; + } else { //Normal positioning, at token word + pos = { left: position.left + offset.left }; + } + + // Position menu from top (above) or from bottom of caret (below, default) + if (this.position === 'top') { + pos.bottom = this.$element.offsetParent().height() - position.top - offset.top; + } else { + pos.top = position.top + offset.top + parseInt(this.$element.css('font-size')); + } + this.tmplInst.$('.-autocomplete-container').css(pos); + } + + ensureSelection() { + // Re-render; make sure selected item is something in the list or none if list empty + const selectedItem = this.tmplInst.$('.-autocomplete-item.selected'); + if (!selectedItem.length) { + // Select anything + this.tmplInst.$('.-autocomplete-item:first-child').addClass('selected'); + } + } + + // Select next item in list + next() { + const currentItem = this.tmplInst.$('.-autocomplete-item.selected'); + if (!currentItem.length) { + return; + } + currentItem.removeClass('selected'); + const next = currentItem.next(); + if (next.length) { + next.addClass('selected'); + } else { //End of list or lost selection; Go back to first item + this.tmplInst.$('.-autocomplete-item:first-child').addClass('selected'); + } + } + + //Select previous item in list + prev() { + const currentItem = this.tmplInst.$('.-autocomplete-item.selected'); + if (!currentItem.length) { + return; //Don't try to iterate an empty list + } + currentItem.removeClass('selected'); + const prev = currentItem.prev(); + if (prev.length) { + prev.addClass('selected'); + } else { //Beginning of list or lost selection; Go to end of list + this.tmplInst.$('.-autocomplete-item:last-child').addClass('selected'); + } + } + + // This doesn't need to be reactive because list already changes reactively + // and will cause all of the items to re-render anyway + currentTemplate() { + return this.rules[this.matched].template; + } + +}; diff --git a/packages/meteor-autocomplete/autocomplete.css b/packages/meteor-autocomplete/client/autocomplete.css similarity index 100% rename from packages/meteor-autocomplete/autocomplete.css rename to packages/meteor-autocomplete/client/autocomplete.css diff --git a/packages/meteor-autocomplete/inputs.html b/packages/meteor-autocomplete/client/inputs.html similarity index 100% rename from packages/meteor-autocomplete/inputs.html rename to packages/meteor-autocomplete/client/inputs.html diff --git a/packages/meteor-autocomplete/client/templates.js b/packages/meteor-autocomplete/client/templates.js new file mode 100755 index 0000000000000000000000000000000000000000..97b2e25971355c73ee7880e34c709a9d3746eeab --- /dev/null +++ b/packages/meteor-autocomplete/client/templates.js @@ -0,0 +1,80 @@ +/* globals AutoComplete */ +// Events on template instances, sent to the autocomplete class +const acEvents = { + 'keydown'(e, t) { + t.ac.onKeyDown(e); + }, + 'keyup'(e, t) { + t.ac.onKeyUp(e); + }, + 'focus'(e, t) { + t.ac.onFocus(e); + }, + 'blur'(e, t) { + t.ac.onBlur(e); + } +}; + +Template.inputAutocomplete.events(acEvents); + +Template.textareaAutocomplete.events(acEvents); + +const attributes = function() { + return _.omit(this, 'settings'); //Render all but the settings parameter + +}; + +const autocompleteHelpers = { + attributes, + autocompleteContainer: new Template('AutocompleteContainer', function() { + const ac = new AutoComplete(Blaze.getData().settings); + // Set the autocomplete object on the parent template instance + this.parentView.templateInstance().ac = ac; + + // Set nodes on render in the autocomplete class + this.onViewReady(function() { + ac.element = this.parentView.firstNode(); + ac.$element = $(ac.element); + }); + return Blaze.With(ac, function() { //eslint-disable-line + return Template._autocompleteContainer; + }); + }) +}; + +Template.inputAutocomplete.helpers(autocompleteHelpers); + +Template.textareaAutocomplete.helpers(autocompleteHelpers); + +Template._autocompleteContainer.rendered = function() { + this.data.tmplInst = this; +}; + +Template._autocompleteContainer.destroyed = function() { + // Meteor._debug "autocomplete destroyed" + this.data.teardown(); +}; + + +/* + List rendering helpers + */ + +Template._autocompleteContainer.events({ + // t.data is the AutoComplete instance; `this` is the data item + 'click .-autocomplete-item'(e, t) { + t.data.onItemClick(this, e); + }, + 'mouseenter .-autocomplete-item'(e, t) { + t.data.onItemHover(this, e); + } +}); + +Template._autocompleteContainer.helpers({ + empty() { + return this.filteredList().count() === 0; + }, + noMatchTemplate() { + return this.matchedRule().noMatchTemplate || Template._noMatch; + } +}); diff --git a/packages/meteor-autocomplete/package.js b/packages/meteor-autocomplete/package.js index 5a6912af07a94fb162a3a78c14c36212a2c23423..ab887e0432afc1dffa6ef851c651759c87b11481 100755 --- a/packages/meteor-autocomplete/package.js +++ b/packages/meteor-autocomplete/package.js @@ -7,21 +7,21 @@ Package.describe({ Package.onUse(function(api) { api.use(['blaze', 'templating', 'jquery'], 'client'); - api.use(['coffeescript', 'underscore', 'ecmascript']); // both + api.use(['underscore', 'ecmascript']); // both api.use(['mongo', 'ddp']); api.use('dandv:caret-position@2.1.0-3', 'client'); // Our files api.addFiles([ - 'autocomplete.css', - 'inputs.html', - 'autocomplete-client.coffee', - 'templates.coffee' + 'client/autocomplete.css', + 'client/inputs.html', + 'client/autocomplete-client.js', + 'client/templates.js' ], 'client'); api.addFiles([ - 'autocomplete-server.coffee' + 'server/autocomplete-server.js' ], 'server'); api.export('Autocomplete', 'server'); @@ -31,7 +31,6 @@ Package.onUse(function(api) { Package.onTest(function(api) { api.use('mizzao:autocomplete'); - api.use('coffeescript'); api.use('mongo'); api.use('tinytest'); diff --git a/packages/meteor-autocomplete/server/autocomplete-server.js b/packages/meteor-autocomplete/server/autocomplete-server.js new file mode 100755 index 0000000000000000000000000000000000000000..582daf40c1d1a1e6841f58e0a319a199a9b07a7a --- /dev/null +++ b/packages/meteor-autocomplete/server/autocomplete-server.js @@ -0,0 +1,31 @@ +// This also attaches an onStop callback to sub, so we don't need to worry about that. +// https://github.com/meteor/meteor/blob/devel/packages/mongo/collection.js +const Autocomplete = class { + publishCursor(cursor, sub) { + Mongo.Collection._publishCursor(cursor, sub, 'autocompleteRecords'); + } +}; + +Meteor.publish('autocomplete-recordset', function(selector, options, collName) { + const collection = global[collName]; + + // This is a semi-documented Meteor feature: + // https://github.com/meteor/meteor/blob/devel/packages/mongo-livedata/collection.js + if (!collection) { + throw new Error(`${ collName } is not defined on the global namespace of the server.`); + } + if (!collection._isInsecure()) { + Meteor._debug(`${ collName } is a secure collection, therefore no data was returned because the client could compromise security by subscribing to arbitrary server collections via the browser console. Please write your own publish function.`); + return []; // We need this for the subscription to be marked ready + } + if (options.limit) { + // guard against client-side DOS: hard limit to 50 + options.limit = Math.min(50, Math.abs(options.limit)); + } + + // Push this into our own collection on the client so they don't interfere with other publications of the named collection. + // This also stops the observer automatically when the subscription is stopped. + Autocomplete.publishCursor(collection.find(selector, options), this); + // Mark the subscription ready after the initial addition of documents. + this.ready(); +}); diff --git a/packages/meteor-autocomplete/templates.coffee b/packages/meteor-autocomplete/templates.coffee deleted file mode 100755 index cd56d8ba739846aa43fde4f8deca69d1eaf5dd63..0000000000000000000000000000000000000000 --- a/packages/meteor-autocomplete/templates.coffee +++ /dev/null @@ -1,50 +0,0 @@ -# Events on template instances, sent to the autocomplete class -acEvents = - "keydown": (e, t) -> t.ac.onKeyDown(e) - "keyup": (e, t) -> t.ac.onKeyUp(e) - "focus": (e, t) -> t.ac.onFocus(e) - "blur": (e, t) -> t.ac.onBlur(e) - -Template.inputAutocomplete.events(acEvents) -Template.textareaAutocomplete.events(acEvents) - -attributes = -> _.omit(@, 'settings') # Render all but the settings parameter - -autocompleteHelpers = { - attributes, - autocompleteContainer: new Template('AutocompleteContainer', -> - ac = new AutoComplete( Blaze.getData().settings ) - # Set the autocomplete object on the parent template instance - this.parentView.templateInstance().ac = ac - - # Set nodes on render in the autocomplete class - this.onViewReady -> - ac.element = this.parentView.firstNode() - ac.$element = $(ac.element) - - return Blaze.With(ac, -> Template._autocompleteContainer) - ) -} - -Template.inputAutocomplete.helpers(autocompleteHelpers) -Template.textareaAutocomplete.helpers(autocompleteHelpers) - -Template._autocompleteContainer.rendered = -> - @data.tmplInst = this - -Template._autocompleteContainer.destroyed = -> - # Meteor._debug "autocomplete destroyed" - @data.teardown() - -### - List rendering helpers -### - -Template._autocompleteContainer.events - # t.data is the AutoComplete instance; `this` is the data item - "click .-autocomplete-item": (e, t) -> t.data.onItemClick(this, e) - "mouseenter .-autocomplete-item": (e, t) -> t.data.onItemHover(this, e) - -Template._autocompleteContainer.helpers - empty: -> @filteredList().count() is 0 - noMatchTemplate: -> @matchedRule().noMatchTemplate || Template._noMatch diff --git a/packages/rocketchat-api/package.js b/packages/rocketchat-api/package.js index ef8aaf159327b7b036b6c590911440315fc6646e..16d34b948044b991546631d6295b75e200caac4b 100644 --- a/packages/rocketchat-api/package.js +++ b/packages/rocketchat-api/package.js @@ -17,6 +17,7 @@ Package.onUse(function(api) { api.addFiles('server/settings.js', 'server'); //Register v1 helpers + api.addFiles('server/v1/helpers/requestParams.js', 'server'); api.addFiles('server/v1/helpers/getPaginationItems.js', 'server'); api.addFiles('server/v1/helpers/getUserFromParams.js', 'server'); api.addFiles('server/v1/helpers/isUserFromParams.js', 'server'); diff --git a/packages/rocketchat-api/server/v1/channels.js b/packages/rocketchat-api/server/v1/channels.js index 1de2d82d724b238b812fdbb812f855044ddf9530..3d2e33387f9e47470aa140dd2ed0e74562c83f86 100644 --- a/packages/rocketchat-api/server/v1/channels.js +++ b/packages/rocketchat-api/server/v1/channels.js @@ -1,18 +1,18 @@ //Returns the channel IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property -function findChannelByIdOrName({ roomId, roomName, checkedArchived = true }) { - if ((!roomId || !roomId.trim()) && (!roomName || !roomName.trim())) { +function findChannelByIdOrName({ params, checkedArchived = true }) { + if ((!params.roomId || !params.roomId.trim()) && (!params.roomName || !params.roomName.trim())) { throw new Meteor.Error('error-roomid-param-not-provided', 'The parameter "roomId" or "roomName" is required'); } let room; - if (roomId) { - room = RocketChat.models.Rooms.findOneById(roomId, { fields: RocketChat.API.v1.defaultFieldsToExclude }); - } else if (roomName) { - room = RocketChat.models.Rooms.findOneByName(roomName, { fields: RocketChat.API.v1.defaultFieldsToExclude }); + if (params.roomId) { + room = RocketChat.models.Rooms.findOneById(params.roomId, { fields: RocketChat.API.v1.defaultFieldsToExclude }); + } else if (params.roomName) { + room = RocketChat.models.Rooms.findOneByName(params.roomName, { fields: RocketChat.API.v1.defaultFieldsToExclude }); } if (!room || room.t !== 'c') { - throw new Meteor.Error('error-room-not-found', `No channel found by the id of: ${ roomId }`); + throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any channel'); } if (checkedArchived && room.archived) { @@ -24,7 +24,7 @@ function findChannelByIdOrName({ roomId, roomName, checkedArchived = true }) { RocketChat.API.v1.addRoute('channels.addAll', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); Meteor.runAsUser(this.userId, () => { Meteor.call('addAllUserToRoom', findResult._id, this.bodyParams.activeUsersOnly); @@ -38,7 +38,7 @@ RocketChat.API.v1.addRoute('channels.addAll', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.addModerator', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); const user = this.getUserFromParams(); @@ -52,7 +52,7 @@ RocketChat.API.v1.addRoute('channels.addModerator', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.addOwner', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); const user = this.getUserFromParams(); @@ -66,7 +66,7 @@ RocketChat.API.v1.addRoute('channels.addOwner', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.archive', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); Meteor.runAsUser(this.userId, () => { Meteor.call('archiveRoom', findResult._id); @@ -78,7 +78,7 @@ RocketChat.API.v1.addRoute('channels.archive', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.cleanHistory', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); if (!this.bodyParams.latest) { return RocketChat.API.v1.failure('Body parameter "latest" is required.'); @@ -106,7 +106,7 @@ RocketChat.API.v1.addRoute('channels.cleanHistory', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.close', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); const sub = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(findResult._id, this.userId); @@ -128,7 +128,7 @@ RocketChat.API.v1.addRoute('channels.close', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.create', { authRequired: true }, { post() { - if (!RocketChat.authz.hasPermission(this.userId, 'create-p')) { + if (!RocketChat.authz.hasPermission(this.userId, 'create-c')) { return RocketChat.API.v1.unauthorized(); } @@ -162,7 +162,7 @@ RocketChat.API.v1.addRoute('channels.create', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.delete', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); //The find method returns either with the group or the failur @@ -182,7 +182,7 @@ RocketChat.API.v1.addRoute('channels.getIntegrations', { authRequired: true }, { return RocketChat.API.v1.unauthorized(); } - const findResult = findChannelByIdOrName({ roomId: this.queryParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); let includeAllPublicChannels = true; if (typeof this.queryParams.includeAllPublicChannels !== 'undefined') { @@ -222,7 +222,7 @@ RocketChat.API.v1.addRoute('channels.getIntegrations', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.history', { authRequired: true }, { get() { - const findResult = findChannelByIdOrName({ roomId: this.queryParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); let latestDate = new Date(); if (this.queryParams.latest) { @@ -262,7 +262,7 @@ RocketChat.API.v1.addRoute('channels.history', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.info', { authRequired: true }, { get() { - const findResult = findChannelByIdOrName({ roomId: this.queryParams.roomId, roomName: this.queryParams.roomName, checkedArchived: false }); + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); return RocketChat.API.v1.success({ channel: RocketChat.models.Rooms.findOneById(findResult._id, { fields: RocketChat.API.v1.defaultFieldsToExclude }) @@ -272,7 +272,7 @@ RocketChat.API.v1.addRoute('channels.info', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.invite', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); const user = this.getUserFromParams(); @@ -288,7 +288,7 @@ RocketChat.API.v1.addRoute('channels.invite', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.join', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); Meteor.runAsUser(this.userId, () => { Meteor.call('joinRoom', findResult._id, this.bodyParams.joinCode); @@ -302,7 +302,7 @@ RocketChat.API.v1.addRoute('channels.join', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.kick', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); const user = this.getUserFromParams(); @@ -318,7 +318,7 @@ RocketChat.API.v1.addRoute('channels.kick', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.leave', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); Meteor.runAsUser(this.userId, () => { Meteor.call('leaveRoom', findResult._id); @@ -414,7 +414,7 @@ RocketChat.API.v1.addRoute('channels.online', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.open', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); const sub = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(findResult._id, this.userId); @@ -436,7 +436,7 @@ RocketChat.API.v1.addRoute('channels.open', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.removeModerator', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); const user = this.getUserFromParams(); @@ -450,7 +450,7 @@ RocketChat.API.v1.addRoute('channels.removeModerator', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.removeOwner', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); const user = this.getUserFromParams(); @@ -468,7 +468,7 @@ RocketChat.API.v1.addRoute('channels.rename', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "name" is required'); } - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: { roomId: this.bodyParams.roomId} }); if (findResult.name === this.bodyParams.name) { return RocketChat.API.v1.failure('The channel name is the same as what it would be renamed to.'); @@ -490,7 +490,7 @@ RocketChat.API.v1.addRoute('channels.setDescription', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "description" is required'); } - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); if (findResult.description === this.bodyParams.description) { return RocketChat.API.v1.failure('The channel description is the same as what it would be changed to.'); @@ -512,7 +512,7 @@ RocketChat.API.v1.addRoute('channels.setJoinCode', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "joinCode" is required'); } - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); Meteor.runAsUser(this.userId, () => { Meteor.call('saveRoomSettings', findResult._id, 'joinCode', this.bodyParams.joinCode); @@ -530,7 +530,7 @@ RocketChat.API.v1.addRoute('channels.setPurpose', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "purpose" is required'); } - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); if (findResult.description === this.bodyParams.purpose) { return RocketChat.API.v1.failure('The channel purpose (description) is the same as what it would be changed to.'); @@ -552,7 +552,7 @@ RocketChat.API.v1.addRoute('channels.setReadOnly', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "readOnly" is required'); } - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); if (findResult.ro === this.bodyParams.readOnly) { return RocketChat.API.v1.failure('The channel read only setting is the same as what it would be changed to.'); @@ -574,7 +574,7 @@ RocketChat.API.v1.addRoute('channels.setTopic', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "topic" is required'); } - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); if (findResult.topic === this.bodyParams.topic) { return RocketChat.API.v1.failure('The channel topic is the same as what it would be changed to.'); @@ -596,7 +596,7 @@ RocketChat.API.v1.addRoute('channels.setType', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "type" is required'); } - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); if (findResult.t === this.bodyParams.type) { return RocketChat.API.v1.failure('The channel type is the same as what it would be changed to.'); @@ -614,7 +614,7 @@ RocketChat.API.v1.addRoute('channels.setType', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.unarchive', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); if (!findResult.archived) { return RocketChat.API.v1.failure(`The channel, ${ findResult.name }, is not archived`); diff --git a/packages/rocketchat-api/server/v1/groups.js b/packages/rocketchat-api/server/v1/groups.js index 10bef449b9c1a51e83f15a76d842dc6f46861ec2..d5c11136d388bd8a6171261c5be6aedf55e836b6 100644 --- a/packages/rocketchat-api/server/v1/groups.js +++ b/packages/rocketchat-api/server/v1/groups.js @@ -1,18 +1,18 @@ //Returns the private group subscription IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property -function findPrivateGroupByIdOrName({ roomId, roomName, userId, checkedArchived = true }) { - if ((!roomId || !roomId.trim()) && (!roomName || !roomName.trim())) { +function findPrivateGroupByIdOrName({ params, userId, checkedArchived = true }) { + if ((!params.roomId || !params.roomId.trim()) && (!params.roomName || !params.roomName.trim())) { throw new Meteor.Error('error-roomid-param-not-provided', 'The parameter "roomId" or "roomName" is required'); } let roomSub; - if (roomId) { - roomSub = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(roomId, userId); - } else if (roomName) { - roomSub = RocketChat.models.Subscriptions.findOneByRoomNameAndUserId(roomName, userId); + if (params.roomId) { + roomSub = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(params.roomId, userId); + } else if (params.roomName) { + roomSub = RocketChat.models.Subscriptions.findOneByRoomNameAndUserId(params.roomName, userId); } if (!roomSub || roomSub.t !== 'p') { - throw new Meteor.Error('error-room-not-found', `No private group by the id of: ${ roomId }`); + throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); } if (checkedArchived && roomSub.archived) { @@ -24,7 +24,7 @@ function findPrivateGroupByIdOrName({ roomId, roomName, userId, checkedArchived RocketChat.API.v1.addRoute('groups.addAll', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); Meteor.runAsUser(this.userId, () => { Meteor.call('addAllUserToRoom', findResult.rid, this.bodyParams.activeUsersOnly); @@ -38,7 +38,7 @@ RocketChat.API.v1.addRoute('groups.addAll', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.addModerator', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); const user = this.getUserFromParams(); @@ -52,7 +52,7 @@ RocketChat.API.v1.addRoute('groups.addModerator', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.addOwner', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); const user = this.getUserFromParams(); @@ -67,7 +67,7 @@ RocketChat.API.v1.addRoute('groups.addOwner', { authRequired: true }, { //Archives a private group only if it wasn't RocketChat.API.v1.addRoute('groups.archive', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); Meteor.runAsUser(this.userId, () => { Meteor.call('archiveRoom', findResult.rid); @@ -79,10 +79,10 @@ RocketChat.API.v1.addRoute('groups.archive', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.close', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId, checkedArchived: false }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); if (!findResult.open) { - return RocketChat.API.v1.failure(`The private group with an id "${ this.bodyParams.roomId }" is already closed to the sender`); + return RocketChat.API.v1.failure(`The private group, ${ findResult.name }, is already closed to the sender`); } Meteor.runAsUser(this.userId, () => { @@ -130,7 +130,7 @@ RocketChat.API.v1.addRoute('groups.create', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.delete', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId, checkedArchived: false }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); Meteor.runAsUser(this.userId, () => { Meteor.call('eraseRoom', findResult.rid); @@ -148,7 +148,7 @@ RocketChat.API.v1.addRoute('groups.getIntegrations', { authRequired: true }, { return RocketChat.API.v1.unauthorized(); } - const findResult = findPrivateGroupByIdOrName({ roomId: this.queryParams.roomId, userId: this.userId, checkedArchived: false }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); let includeAllPrivateGroups = true; if (typeof this.queryParams.includeAllPrivateGroups !== 'undefined') { @@ -182,7 +182,7 @@ RocketChat.API.v1.addRoute('groups.getIntegrations', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.history', { authRequired: true }, { get() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.queryParams.roomId, userId: this.userId, checkedArchived: false }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); let latestDate = new Date(); if (this.queryParams.latest) { @@ -222,7 +222,7 @@ RocketChat.API.v1.addRoute('groups.history', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.info', { authRequired: true }, { get() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.queryParams.roomId, roomName: this.queryParams.roomName, userId: this.userId, checkedArchived: false }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); return RocketChat.API.v1.success({ group: RocketChat.models.Rooms.findOneById(findResult.rid, { fields: RocketChat.API.v1.defaultFieldsToExclude }) @@ -232,7 +232,7 @@ RocketChat.API.v1.addRoute('groups.info', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.invite', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); const user = this.getUserFromParams(); @@ -248,7 +248,7 @@ RocketChat.API.v1.addRoute('groups.invite', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.kick', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); const user = this.getUserFromParams(); @@ -262,7 +262,7 @@ RocketChat.API.v1.addRoute('groups.kick', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.leave', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); Meteor.runAsUser(this.userId, () => { Meteor.call('leaveRoom', findResult.rid); @@ -331,10 +331,10 @@ RocketChat.API.v1.addRoute('groups.online', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.open', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId, checkedArchived: false }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); if (findResult.open) { - return RocketChat.API.v1.failure(`The private group, ${ this.bodyParams.name }, is already open for the sender`); + return RocketChat.API.v1.failure(`The private group, ${ findResult.name }, is already open for the sender`); } Meteor.runAsUser(this.userId, () => { @@ -347,7 +347,7 @@ RocketChat.API.v1.addRoute('groups.open', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.removeModerator', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); const user = this.getUserFromParams(); @@ -361,7 +361,7 @@ RocketChat.API.v1.addRoute('groups.removeModerator', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.removeOwner', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); const user = this.getUserFromParams(); @@ -379,7 +379,7 @@ RocketChat.API.v1.addRoute('groups.rename', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "name" is required'); } - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: { roomId: this.bodyParams.roomId}, userId: this.userId }); Meteor.runAsUser(this.userId, () => { Meteor.call('saveRoomSettings', findResult.rid, 'roomName', this.bodyParams.name); @@ -397,7 +397,7 @@ RocketChat.API.v1.addRoute('groups.setDescription', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "description" is required'); } - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); Meteor.runAsUser(this.userId, () => { Meteor.call('saveRoomSettings', findResult.rid, 'roomDescription', this.bodyParams.description); @@ -415,7 +415,7 @@ RocketChat.API.v1.addRoute('groups.setPurpose', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "purpose" is required'); } - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); Meteor.runAsUser(this.userId, () => { Meteor.call('saveRoomSettings', findResult.rid, 'roomDescription', this.bodyParams.purpose); @@ -433,7 +433,7 @@ RocketChat.API.v1.addRoute('groups.setReadOnly', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "readOnly" is required'); } - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); if (findResult.ro === this.bodyParams.readOnly) { return RocketChat.API.v1.failure('The private group read only setting is the same as what it would be changed to.'); @@ -455,7 +455,7 @@ RocketChat.API.v1.addRoute('groups.setTopic', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "topic" is required'); } - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); Meteor.runAsUser(this.userId, () => { Meteor.call('saveRoomSettings', findResult.rid, 'roomTopic', this.bodyParams.topic); @@ -473,7 +473,7 @@ RocketChat.API.v1.addRoute('groups.setType', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "type" is required'); } - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); if (findResult.t === this.bodyParams.type) { return RocketChat.API.v1.failure('The private group type is the same as what it would be changed to.'); @@ -491,7 +491,7 @@ RocketChat.API.v1.addRoute('groups.setType', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.unarchive', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId, checkedArchived: false }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); Meteor.runAsUser(this.userId, () => { Meteor.call('unarchiveRoom', findResult.rid); diff --git a/packages/rocketchat-api/server/v1/helpers/getUserFromParams.js b/packages/rocketchat-api/server/v1/helpers/getUserFromParams.js index 01c075ea0abbebb2c89e138fbad39329da52ca5c..c52296f0fb7aedafe0ed423eb84bd9778485cd7b 100644 --- a/packages/rocketchat-api/server/v1/helpers/getUserFromParams.js +++ b/packages/rocketchat-api/server/v1/helpers/getUserFromParams.js @@ -2,32 +2,19 @@ RocketChat.API.v1.helperMethods.set('getUserFromParams', function _getUserFromParams() { const doesntExist = { _doesntExist: true }; let user; + const params = this.requestParams(); - switch (this.request.method) { - case 'POST': - case 'PUT': - if (this.bodyParams.userId && this.bodyParams.userId.trim()) { - user = RocketChat.models.Users.findOneById(this.bodyParams.userId) || doesntExist; - } else if (this.bodyParams.username && this.bodyParams.username.trim()) { - user = RocketChat.models.Users.findOneByUsername(this.bodyParams.username) || doesntExist; - } else if (this.bodyParams.user && this.bodyParams.user.trim()) { - user = RocketChat.models.Users.findOneByUsername(this.bodyParams.user) || doesntExist; - } - break; - default: - if (this.queryParams.userId && this.queryParams.userId.trim()) { - user = RocketChat.models.Users.findOneById(this.queryParams.userId) || doesntExist; - } else if (this.queryParams.username && this.queryParams.username.trim()) { - user = RocketChat.models.Users.findOneByUsername(this.queryParams.username) || doesntExist; - } else if (this.queryParams.user && this.queryParams.user.trim()) { - user = RocketChat.models.Users.findOneByUsername(this.queryParams.user) || doesntExist; - } - break; + if (params.userId && params.userId.trim()) { + user = RocketChat.models.Users.findOneById(params.userId) || doesntExist; + } else if (params.username && params.username.trim()) { + user = RocketChat.models.Users.findOneByUsername(params.username) || doesntExist; + } else if (params.user && params.user.trim()) { + user = RocketChat.models.Users.findOneByUsername(params.user) || doesntExist; + } else { + throw new Meteor.Error('error-user-param-not-provided', 'The required "userId" or "username" param was not provided'); } - if (!user) { - throw new Meteor.Error('error-user-param-not-provided', 'The required "userId" or "username" param was not provided'); - } else if (user._doesntExist) { + if (user._doesntExist) { throw new Meteor.Error('error-invalid-user', 'The required "userId" or "username" param provided does not match any users'); } diff --git a/packages/rocketchat-api/server/v1/helpers/isUserFromParams.js b/packages/rocketchat-api/server/v1/helpers/isUserFromParams.js index f0b24a78096b84753d74f3d8e71c5f7db6127aae..fab907bc96da76e4d4a3ef097145da34e4efcb7e 100644 --- a/packages/rocketchat-api/server/v1/helpers/isUserFromParams.js +++ b/packages/rocketchat-api/server/v1/helpers/isUserFromParams.js @@ -1,5 +1,8 @@ RocketChat.API.v1.helperMethods.set('isUserFromParams', function _isUserFromParams() { - return (this.queryParams.userId && this.userId === this.queryParams.userId) || - (this.queryParams.username && this.user.username === this.queryParams.username) || - (this.queryParams.user && this.user.username === this.queryParams.user); + const params = this.requestParams(); + + return (!params.userId && !params.username && !params.user) || + (params.userId && this.userId === params.userId) || + (params.username && this.user.username === params.username) || + (params.user && this.user.username === params.user); }); diff --git a/packages/rocketchat-api/server/v1/helpers/requestParams.js b/packages/rocketchat-api/server/v1/helpers/requestParams.js new file mode 100644 index 0000000000000000000000000000000000000000..bc5718313913ab2839d46c77d80d9c8b1e0f4eef --- /dev/null +++ b/packages/rocketchat-api/server/v1/helpers/requestParams.js @@ -0,0 +1,3 @@ +RocketChat.API.v1.helperMethods.set('requestParams', function _requestParams() { + return ['POST', 'PUT'].includes(this.request.method) ? this.bodyParams : this.queryParams; +}); diff --git a/packages/rocketchat-authorization/server/startup.js b/packages/rocketchat-authorization/server/startup.js index 6aeb68290d7ff0bf083a4bf398314aee8e0c84c2..3fe7771125de4edc9d9be81a81f2c6d5111b1316 100644 --- a/packages/rocketchat-authorization/server/startup.js +++ b/packages/rocketchat-authorization/server/startup.js @@ -32,6 +32,7 @@ Meteor.startup(function() { { _id: 'edit-other-user-password', roles : ['admin'] }, { _id: 'edit-privileged-setting', roles : ['admin'] }, { _id: 'edit-room', roles : ['admin', 'owner', 'moderator'] }, + { _id: 'force-delete-message', roles : ['admin', 'owner'] }, { _id: 'join-without-join-code', roles : ['admin', 'bot'] }, { _id: 'manage-assets', roles : ['admin'] }, { _id: 'manage-emoji', roles : ['admin'] }, diff --git a/packages/rocketchat-file-upload/lib/FileUploadBase.js b/packages/rocketchat-file-upload/lib/FileUploadBase.js index 62263dd0c6d9b8f081726b213ca49611dcabf8eb..24a304cf28882e196c234e71c3a7aaddd236de2f 100644 --- a/packages/rocketchat-file-upload/lib/FileUploadBase.js +++ b/packages/rocketchat-file-upload/lib/FileUploadBase.js @@ -2,8 +2,8 @@ /* exported FileUploadBase */ UploadFS.config.defaultStorePermissions = new UploadFS.StorePermissions({ - insert(userId/*, doc*/) { - return userId; + insert(userId, doc) { + return userId || (doc && doc.message_id && doc.message_id.indexOf('slack-') === 0); // allow inserts from slackbridge (message_id = slack-timestamp-milli) }, update(userId, doc) { return RocketChat.authz.hasPermission(Meteor.userId(), 'delete-message', doc.rid) || (RocketChat.settings.get('Message_AllowDeleting') && userId === doc.userId); diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js index daac0ea19932a009785e5e84f570348d34f8e558..e217c963e3f5f111c13cab42dc7f32609dfd3ecf 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js +++ b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js @@ -1,5 +1,5 @@ /* globals Slingshot, FileUpload, AWS, SystemLogger */ -const crypto = Npm.require('crypto'); +import AWS4 from '../lib/AWS4.js'; let S3accessKey; let S3secretKey; @@ -9,11 +9,23 @@ const generateURL = function(file) { if (!file || !file.s3) { return; } - const resourceURL = `/${ file.s3.bucket }/${ file.s3.path }${ file._id }`; - const expires = parseInt(new Date().getTime() / 1000) + Math.max(5, S3expiryTimeSpan); - const StringToSign = `GET\n\n\n${ expires }\n${ resourceURL }`; - const signature = crypto.createHmac('sha1', S3secretKey).update(new Buffer(StringToSign, 'utf-8')).digest('base64'); - return `${ file.url }?AWSAccessKeyId=${ encodeURIComponent(S3accessKey) }&Expires=${ expires }&Signature=${ encodeURIComponent(signature) }`; + + const credential = { + accessKeyId: S3accessKey, + secretKey: S3secretKey + }; + + const req = { + bucket: file.s3.bucket, + region: file.s3.region, + path: `/${ file.s3.path }${ file._id }`, + url: file.url, + expire: Math.max(5, S3expiryTimeSpan) + }; + + const queryString = AWS4.sign(req, credential); + + return `${ file.url }?${ queryString }`; }; FileUpload.addHandler('s3', { diff --git a/packages/rocketchat-file-upload/server/lib/AWS4.js b/packages/rocketchat-file-upload/server/lib/AWS4.js new file mode 100644 index 0000000000000000000000000000000000000000..bb8980ac0d4c79e8cdcb74e1007961e3f0810a1f --- /dev/null +++ b/packages/rocketchat-file-upload/server/lib/AWS4.js @@ -0,0 +1,175 @@ +import crypto from 'crypto'; +import urllib from 'url'; +import querystring from 'querystring'; + +const Algorithm = 'AWS4-HMAC-SHA256'; +const DefaultRegion = 'us-east-1'; +const Service = 's3'; +const KeyPartsRequest = 'aws4_request'; + +class Aws4 { + constructor(req, credentials) { + const { url, method = 'GET', body = '', date, region, headers = {}, expire = 86400 } = this.req = req; + + Object.assign(this, { url, body, method: method.toUpperCase() }); + + const urlObj = urllib.parse(url); + this.region = region || DefaultRegion; + this.path = urlObj.pathname; + this.host = urlObj.host; + this.date = date || this.amzDate; + this.credentials = credentials; + this.headers = this.prepareHeaders(headers); + this.expire = expire; + } + + prepareHeaders() { + const host = this.host; + + return { + host + }; + } + + hmac(key, string, encoding) { + return crypto.createHmac('sha256', key).update(string, 'utf8').digest(encoding); + } + + hash(string, encoding = 'hex') { + return crypto.createHash('sha256').update(string, 'utf8').digest(encoding); + } + + encodeRfc3986(urlEncodedString) { + return urlEncodedString.replace(/[!'()*]/g, function(c) { + return `%${ c.charCodeAt(0).toString(16).toUpperCase() }`; + }); + } + + encodeQuery(query) { + return this.encodeRfc3986(querystring.stringify(Object.keys(query).sort().reduce((obj, key) => { + if (!key) { return obj; } + obj[key] = !Array.isArray(query[key]) ? query[key] : query[key].slice().sort(); + return obj; + }, {}))); + } + + get query() { + const query = {}; + + if (this.credentials.sessionToken) { + query['X-Amz-Security-Token'] = this.credentials.sessionToken; + } + + query['X-Amz-Expires'] = this.expire; + query['X-Amz-Date'] = this.amzDate; + query['X-Amz-Algorithm'] = Algorithm; + query['X-Amz-Credential'] = `${ this.credentials.accessKeyId }/${ this.credentialScope }`; + query['X-Amz-SignedHeaders'] = this.signedHeaders; + + return query; + } + + get amzDate() { + return (new Date()).toISOString().replace(/[:\-]|\.\d{3}/g, ''); + } + + get dateStamp() { + return this.date.slice(0, 8); + } + + get payloadHash() { + return 'UNSIGNED-PAYLOAD'; + } + + get canonicalPath() { + let pathStr = this.path; + if (pathStr === '/') { return pathStr; } + + pathStr = pathStr.replace(/\/{2,}/g, '/'); + pathStr = pathStr.split('/').reduce((path, piece) => { + if (piece === '..') { + path.pop(); + } else { + path.push(this.encodeRfc3986(querystring.escape(piece))); + } + return path; + }, []).join('/'); + + return pathStr; + } + + get canonicalQuery() { + return this.encodeQuery(this.query); + } + + get canonicalHeaders() { + const headers = Object.keys(this.headers) + .sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1) + .map(key => `${ key.toLowerCase() }:${ this.headers[key] }`); + return `${ headers.join('\n') }\n`; + } + + get signedHeaders() { + return Object.keys(this.headers) + .map(key => key.toLowerCase()) + .sort() + .join(';'); + } + + get canonicalRequest() { + return [ + this.method, + this.canonicalPath, + this.canonicalQuery, + this.canonicalHeaders, + this.signedHeaders, + this.payloadHash + ].join('\n'); + } + + get credentialScope() { + return [ + this.dateStamp, + this.region, + Service, + KeyPartsRequest + ].join('/'); + } + + get stringToSign() { + return [ + Algorithm, + this.date, + this.credentialScope, + this.hash(this.canonicalRequest) + ].join('\n'); + } + + get signingKey() { + const kDate = this.hmac(`AWS4${ this.credentials.secretKey }`, this.dateStamp); + const kRegion = this.hmac(kDate, this.region); + const kService = this.hmac(kRegion, Service); + const kSigning = this.hmac(kService, KeyPartsRequest); + + return kSigning; + } + + get signature() { + return this.hmac(this.signingKey, this.stringToSign, 'hex'); + } + + // Export + // Return signed query string + sign() { + const query = this.query; + query['X-Amz-Signature'] = this.signature; + + return this.encodeQuery(query); + } +} + +export default { + sign(request, credential) { + return (new Aws4(request, credential)).sign(); + } +}; diff --git a/packages/rocketchat-file-upload/server/methods/getS3FileUrl.js b/packages/rocketchat-file-upload/server/methods/getS3FileUrl.js index ef48ddc2163b815644a57dd9712f39e474327a69..c3cbca844eb80c638ad2f8cfcef6597f73065537 100644 --- a/packages/rocketchat-file-upload/server/methods/getS3FileUrl.js +++ b/packages/rocketchat-file-upload/server/methods/getS3FileUrl.js @@ -1,4 +1,5 @@ -const crypto = Npm.require('crypto'); +import AWS4 from '../lib/AWS4.js'; + let protectedFiles; let S3accessKey; let S3secretKey; @@ -26,12 +27,22 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendFileMessage' }); } const file = RocketChat.models.Uploads.findOneById(fileId); - const resourceURL = `/${ file.s3.bucket }/${ file.s3.path }${ file._id }`; - const expires = parseInt(new Date().getTime() / 1000) + Math.max(5, S3expiryTimeSpan); - const StringToSign = `GET\n\n\n${ expires }\n${ resourceURL }`; - const signature = crypto.createHmac('sha1', S3secretKey).update(new Buffer(StringToSign, 'utf-8')).digest('base64'); - return { - url:`${ file.url }?AWSAccessKeyId=${ encodeURIComponent(S3accessKey) }&Expires=${ expires }&Signature=${ encodeURIComponent(signature) }` + + const credential = { + accessKeyId: S3accessKey, + secretKey: S3secretKey + }; + + const req = { + bucket: file.s3.bucket, + region: file.s3.region, + path: `/${ file.s3.path }${ file._id }`, + url: file.url, + expire: Math.max(5, S3expiryTimeSpan) }; + + const queryString = AWS4.sign(req, credential); + + return `${ file.url }?${ queryString }`; } }); diff --git a/packages/rocketchat-i18n/i18n/ca.i18n.json b/packages/rocketchat-i18n/i18n/ca.i18n.json index 436bcca1e9de01d37326825effe7f30fbfa4fe04..185c793f6340d4d2a15b4a30ace06b25ef5a0bda 100644 --- a/packages/rocketchat-i18n/i18n/ca.i18n.json +++ b/packages/rocketchat-i18n/i18n/ca.i18n.json @@ -17,7 +17,8 @@ "Accessing_permissions": "L'accés als permisos", "Account_SID": "Compte SID", "Accounts": "Comptes", - "Accounts_AllowAnonymousAccess": "Permet accés anònim", + "Accounts_AllowAnonymousRead": "Permetre lectura anònima", + "Accounts_AllowAnonymousWrite": "Permetre escriptura anònima", "Accounts_AllowDeleteOwnAccount": "Permetre als usuaris eliminar el seu propi compte", "Accounts_AllowedDomainsList": "Llista de dominis permesos", "Accounts_AllowedDomainsList_Description": "Llista dels dominis permesos separada per comes ", @@ -35,6 +36,7 @@ "Accounts_BlockedUsernameList": "Llista de noms d'usuari bloquejats", "Accounts_BlockedUsernameList_Description": "Llista separada per comes de noms d'usuari bloquejats (no distingeix majúscules/minúscules)", "Accounts_CustomFields_Description": "Ha de ser un objecte JSON và lid on les claus són els noms dels camps i contenen un diccionari amb les opcions del camp. Exemple:<br/><code>{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"student\",\n  \"options\": [\"teacher\", \"student\"],\n  \"required\": true,\n  \"modifyRecordField\": {\n   \"array\": true,\n   \"field\": \"roles\"\n  }\n },\n \"twitter\": {\n  \"type\": \"text\",\n  \"required\": true,\n  \"minLength\": 2,\n  \"maxLength\": 10\n }\n}</code> ", + "Accounts_DefaultUsernamePrefixSuggestion": "Prefix suggerit per al nom d'usuari per defecte", "Accounts_denyUnverifiedEmail": "Denegar correu electrònic sense verificar", "Accounts_EmailVerification": "Verificació de correu electrònic", "Accounts_EmailVerification_Description": "Assegura't que la configuració SMTP és correcta per fer servir aquesta funcionalitat", @@ -265,6 +267,7 @@ "BotHelpers_userFields": "Camps d'usuari", "BotHelpers_userFields_Description": "CSV de camps d'usuari que poden ser accedits pels mètodes helper dels bots.", "Branch": "Branca", + "Broadcast_Connected_Instances": "Difusió de les instà ncies connectades", "Bugsnag_api_key": "Clau API Bugsnag", "busy": "ocupat", "Busy": "Ocupat", @@ -360,6 +363,7 @@ "CROWD_Reject_Unauthorized": "Rebutja no autoritzat", "CRM_Integration": "Integració CRM", "Current_Chats": "Xats actuals", + "Current_Status": "Estat actual", "Custom": "Personalitzat", "Custom_Emoji": "Emoticona personalitzada", "Custom_Emoji_Add": "Afegir nova emoticona", @@ -668,11 +672,11 @@ "Iframe_Integration_receive_enable": "Activa recepció", "Iframe_Integration_receive_enable_Description": "Permetre que la finestra pare enviï ordres a Rocket.Chat.", "Iframe_Integration_receive_origin": "Rebre orÃgens", - "Iframe_Integration_receive_origin_Description": "Només les pà gines de les quals es proporciona l'origen podran enviar ordres o `*` per a totes. Es poden utilitzar múltiples valors separats per `,`. Exemple `http://localhost,https://localhost`", + "Iframe_Integration_receive_origin_Description": "Origens amb prefix del protocol, separats per comes, dels quals es permet rebre comandes. Exemple 'http://localhost, https://localhost', o * per permetre rebre de qualsevol lloc.", "Iframe_Integration_send_enable": "Activa enviament", "Iframe_Integration_send_enable_Description": "Envia esdeveniments a la finestra pare", "Iframe_Integration_send_target_origin": "Envia l'origen a l'objectiu", - "Iframe_Integration_send_target_origin_Description": "Només les pà gines amb l'origen proporcionat podran rebre esdeveniments o `*` per a totes. Exemple `http://localhost`", + "Iframe_Integration_send_target_origin_Description": "Origens amb prefix del protocol on les comandes són enviades. Exemple 'https://localhost', o * per permetre enviar a qualsevol lloc.", "Importer_Archived": "Arxivat", "Importer_CSV_Information": "L'importador CSV requereix un format especÃfic, si us plau llegiu la documentació sobre com estructurar l'arxiu .zip:", "Importer_HipChatEnterprise_Information": "L'arxiu pujat ha de ser un tar.gz desencriptat. Si us plau, llegiu la documentació per a més informació:", @@ -704,6 +708,7 @@ "Install_FxOs_follow_instructions": "Si us plau, confirma la instal·lació de l'aplicació al teu dispositiu (polsi \"Instal·lar\" quan se us demani).", "Installation": "Instal·lació", "Installed_at": "Instal·lat a", + "Instance_Record": "Registre d'instà ncia", "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Instruccions als visitants, ompliu el formulari per enviar un missatge", "Impersonate_user": "Suplantar usuari", "Impersonate_user_description": "Quan s'activa, la integració publica com a l'usuari que ha desencadenat la integració", @@ -1124,6 +1129,7 @@ "or": "o", "Open_your_authentication_app_and_enter_the_code": "Obre l'app d'autenticació i entra el codi. També pots utilitzar un dels codis de recuperació.", "Order": "Ordre", + "Or_talk_as_anonymous": "O parla com a anònim", "OS_Arch": "Arquitectura del sistema", "OS_Cpus": "Recompte de CPU", "OS_Freemem": "Memòria RAM lliure", @@ -1228,7 +1234,6 @@ "Register": "Crea un compte nou", "Registration": "Registre", "Registration_Succeeded": "Registre reeixit", - "Register_or_login_to_send_messages": "Registra't o identifica't per enviar missatges", "Registration_via_Admin": "Registre via Admin", "Regular_Expressions": "Expressions regulars", "Release": "Llançament", @@ -1254,6 +1259,7 @@ "Reset_password": "Reinicialitza la contrasenya", "Restart": "Reinicia (restart)", "Restart_the_server": "Reinicia el servidor", + "Retry_Count": "Comptador de reintents", "Role": "Rol", "Role_Editing": "Edició de rols", "Role_removed": "Rol eliminat", @@ -1363,6 +1369,7 @@ "Showing_archived_results": "<p>Mostrant <b>%s</b> resultats arxivats</p>", "Showing_online_users": "Mostrant-ne <b>__total_showing__</b>, En lÃnia: __online__, Total: __total__ usuaris", "Showing_results": "<p>Mostrant <b>%s</b> resultats</p>", + "Sign_in_to_start_talking": "Identifica't per començar a parlar", "since_creation": "des de %s", "Site_Name": "Nom del lloc", "Site_Url": "URL del lloc", @@ -1557,6 +1564,7 @@ "Unread_Rooms": "Sales no llegides", "Unread_Rooms_Mode": "Mode de sales no llegides", "Unstar_Message": "Esborra el destacat", + "Updated_at": "Actualitzat el", "Upload_file_description": "Descripció de l'arxiu", "Upload_file_name": "Nom de l'arxiu", "Upload_file_question": "Pujar l'arxiu?", diff --git a/packages/rocketchat-i18n/i18n/cs.i18n.json b/packages/rocketchat-i18n/i18n/cs.i18n.json index ba2dab094d9aaf1329a074484b881bde67931f71..f3d89cf4c104f0353fcbbc415f1047e455fd3b2f 100644 --- a/packages/rocketchat-i18n/i18n/cs.i18n.json +++ b/packages/rocketchat-i18n/i18n/cs.i18n.json @@ -17,7 +17,8 @@ "Accessing_permissions": "PÅ™Ãstup k oprávnÄ›nÃ", "Account_SID": "SID úÄtu", "Accounts": "ÚÄty", - "Accounts_AllowAnonymousAccess": "Povolit anonymnà pÅ™Ãstup", + "Accounts_AllowAnonymousRead": "Povolit anonymům ÄÃst", + "Accounts_AllowAnonymousWrite": "Povolit anonymům zapisovat", "Accounts_AllowDeleteOwnAccount": "Povolit uživatelům odstranit vlastnà úÄet", "Accounts_AllowedDomainsList": "Seznam povolených domén", "Accounts_AllowedDomainsList_Description": "Čárkami oddÄ›lený seznam povolených domén", @@ -35,6 +36,7 @@ "Accounts_BlockedUsernameList": "Zakázaná uživatelská jména", "Accounts_BlockedUsernameList_Description": "Äárkou oddÄ›lený seznam uživatelských jmen (na velikosti pÃsmen nezáležÃ)", "Accounts_CustomFields_Description": "Validnà JSON obsahujÃcà klÃÄe polà s nastavenÃm. NapÅ™Ãklad:<br/><code>{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"student\",\n  \"options\": [\"teacher\", \"student\"],\n  \"required\": true,\n  \"modifyRecordField\": {\n   \"array\": true,\n   \"field\": \"roles\"\n  }\n },\n \"twitter\": {\n  \"type\": \"text\",\n  \"required\": true,\n  \"minLength\": 2,\n  \"maxLength\": 10\n }\n}</code>", + "Accounts_DefaultUsernamePrefixSuggestion": "Výchozà návrh prefixu uživatelského jména", "Accounts_denyUnverifiedEmail": "Zakázat neověřené e-mailové adresy", "Accounts_EmailVerification": "Ověřenà e-mailu", "Accounts_EmailVerification_Description": "Pro použità této funkce se ujistÄ›te, že máte správné nastavenà SMTP", @@ -265,6 +267,7 @@ "BotHelpers_userFields": "Uživatelská pole", "BotHelpers_userFields_Description": "CSV uživatelských polÃ, která budou pÅ™Ãstupná botům", "Branch": "VÄ›tev", + "Broadcast_Connected_Instances": "PÅ™ipojené instance", "Bugsnag_api_key": "Bugsnag API klÃÄ", "busy": "zaneprázdnÄ›ný", "Busy": "ZaneprázdnÄ›ný", @@ -360,6 +363,7 @@ "CROWD_Reject_Unauthorized": "ZamÃtnout neutorizované", "CRM_Integration": "Integrace CRM", "Current_Chats": "Aktuálnà MÃstnosti", + "Current_Status": "Aktuálnà stav", "Custom": "VlastnÃ", "Custom_Emoji": "Vlastnà emotikona", "Custom_Emoji_Add": "PÅ™idat novou emotikonu", @@ -704,6 +708,7 @@ "Install_FxOs_follow_instructions": "ProsÃm potvrÄte instalaci aplikace na VaÅ¡em pÅ™Ãstroji (stisknÄ›te tlaÄÃtko \"Install\" po výzvÄ›).", "Installation": "Instalace", "Installed_at": "instalováno v", + "Instance_Record": "ID Instance", "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Pokyny pro VaÅ¡e návÅ¡tÄ›vnÃky k vyplnÄ›nà formulář pro odeslánà zprávy", "Impersonate_user": "Vydávat se za uživatele", "Impersonate_user_description": "Pokud je povoleno, integrace posÃlá za uživatele, který ji vyvolal", @@ -1124,6 +1129,7 @@ "or": "nebo", "Open_your_authentication_app_and_enter_the_code": "OtevÅ™ete autentizaÄnà aplikaci a zadejte vygenerovaný kód. Můžete použÃt jeden ze svých záložnÃch kódů.", "Order": "Objednat", + "Or_talk_as_anonymous": "Vydávat sezaanonyma", "OS_Arch": "Architektura OS", "OS_Cpus": "PoÄet CPU OS", "OS_Freemem": "Volná paměť OS", @@ -1228,7 +1234,6 @@ "Register": "Zaregistrovat nový úÄet", "Registration": "Registrace", "Registration_Succeeded": "Registrace úspěšná", - "Register_or_login_to_send_messages": "Pro odeslánà zpráv je tÅ™eba se zaregistrovat nebo pÅ™ihlásit", "Registration_via_Admin": "Registrace pÅ™es Admin", "Regular_Expressions": "Regulárnà výrazy", "Release": "Verze", @@ -1254,6 +1259,7 @@ "Reset_password": "Obnovit heslo", "Restart": "Restartovat", "Restart_the_server": "Restartovat server", + "Retry_Count": "PoÄet opakovánÃ", "Role": "Role", "Role_Editing": "Editace Role", "Role_removed": "Role odstranÄ›na", @@ -1363,6 +1369,7 @@ "Showing_archived_results": "<p> <b>Zobrazeno %s</b> archivovaných výsledků </p>", "Showing_online_users": "Viditelných <b>__total_showing__</b> z __total__ uživatelů", "Showing_results": "<p>Zobrazeno <b>%s</b> výsledků</p>", + "Sign_in_to_start_talking": "Pro konverzaci se pÅ™ihlaÅ¡te", "since_creation": "od %s", "Site_Name": "Jméno stránky", "Site_Url": "URL stránky", @@ -1557,6 +1564,7 @@ "Unread_Rooms": "NepÅ™eÄtené mÃstnosti", "Unread_Rooms_Mode": "Mód NepÅ™eÄtených mÃstnostÃ", "Unstar_Message": "Odebrat hvÄ›zdiÄku", + "Updated_at": "Poslednà aktualizace", "Upload_file_description": "Popis souboru", "Upload_file_name": "Název souboru", "Upload_file_question": "Nahrát soubor?", diff --git a/packages/rocketchat-i18n/i18n/de-AT.i18n.json b/packages/rocketchat-i18n/i18n/de-AT.i18n.json index 95bf6f0116731ae1a8db1a1a98390008a1ecc203..f89e6d847474833a6b4cd6f284e7fe623caa73e1 100644 --- a/packages/rocketchat-i18n/i18n/de-AT.i18n.json +++ b/packages/rocketchat-i18n/i18n/de-AT.i18n.json @@ -55,6 +55,7 @@ "Accounts_OAuth_Custom_id": "ID", "Accounts_OAuth_Custom_Identity_Path": "Identitätspfad", "Accounts_OAuth_Custom_Login_Style": "Anmeldungsart", + "Accounts_OAuth_Custom_Merge_Users": "BenutzerInnen zusammenführen", "Accounts_OAuth_Custom_Scope": "Bereich", "Accounts_OAuth_Custom_Secret": "Secret", "Accounts_OAuth_Custom_Token_Path": "Pfad des Token", @@ -118,12 +119,12 @@ "Add_agent": "Agent hinzufügen", "Add_custom_oauth": "Benutzerdefiniertes OAuth-Konto hinzufügen", "Add_manager": "Manager hinzufügen", - "Add_user": "Benutzer hinzufügen", - "Add_User": "Benutzer hinzufügen", - "Add_users": "Benutzer hinzufügen", + "Add_user": "BenutzerIn hinzufügen", + "Add_User": "BenutzerIn hinzufügen", + "Add_users": "BenutzerInnen hinzufügen", "Adding_OAuth_Services": "Hinzufügen von OAuth-Services", "Adding_permission": "Berechtigung hinzufügen", - "Adding_user": "Benutzer hinzufügen", + "Adding_user": "Füge BenutzerIn hinzu", "Additional_emails": "Zusätzliche E-Mails", "Additional_Feedback": "Zusätzliches Feedback", "Administration": "Administration", @@ -205,6 +206,7 @@ "Back_to_integrations": "Zurück zu Integrationen", "Back_to_login": "Zurück zum Login", "Back_to_permissions": "Zurück zu den Berechtigungen", + "Block_User": "BenutzerIn sperren", "Body": "Body", "bold": "fett", "Branch": "Branch", @@ -328,7 +330,7 @@ "Edit_Department": "Abteilung bearbeiten", "edited": "bearbeitet", "Editing_room": "Raum bearbeiten", - "Editing_user": "Benutzer bearbeiten", + "Editing_user": "Benutzern bearbeiten", "Email": "E-Mail", "Email_address_to_send_offline_messages": "E-Mail-Adresse zum Senden von Offline-Nachrichten", "Email_already_exists": "Die E-Mail-Adresse existiert bereits.", @@ -461,6 +463,7 @@ "Force_SSL": "SSL erzwingen", "Force_SSL_Description": "*Achtung!* _Force SSL_ solte niemals mit einem Reverse-Proxy verwendet werden. Falls Sie einen Reverse-Proxy verwenden, sollten Sie die Weiterleitung DORT einrichten. Dies Option existiert für Anwendungen wie Heroku, die keine Weiterleitungskonfigurationen für Reverse-Proxy erlauben.", "Forgot_password": "Passwort vergessen?", + "Forward_to_user": "An BenutzerIn weiterleiten", "Frequently_Used": "Häufig verwendet", "From": "Absender", "From_Email": "Absender", @@ -523,6 +526,9 @@ "Integration_added": "Die Integration wurde hinzugefügt.", "Integration_Incoming_WebHook": "Eingehende WebHook-Integration", "Integration_New": "Neue Integration", + "Integrations_Outgoing_Type_RoomJoined": "BenutzerIn hat den Raum betreten", + "Integrations_Outgoing_Type_RoomLeft": "BenutzerIn hat den Raum verlassen", + "Integrations_Outgoing_Type_UserCreated": "BenutzerIn angelegt.", "Integration_Outgoing_WebHook": "Ausgehende WebHook-Integration", "Integration_Word_Trigger_Placement_Description": "Soll das auslösende Wort irgendwo im Satz stehen und nicht nur am Anfang? ", "Integration_updated": "Die Integration wurde aktualisiert.\n", @@ -548,7 +554,7 @@ "Invitation_Subject": "Einladungsbetreff", "Invitation_Subject_Default": "Sie wurden zu [Site_Name] eingeladen", "Invite_user_to_join_channel": "Benutzer in diesen Raum einladen", - "Invite_Users": "Benutzer einladen", + "Invite_Users": "BenutzerInnen einladen", "is_also_typing": "schreibt auch", "is_also_typing_female": "schreibt auch", "is_also_typing_male": "schreibt auch", diff --git a/packages/rocketchat-i18n/i18n/de.i18n.json b/packages/rocketchat-i18n/i18n/de.i18n.json index 24e7b29e401dcc777e1c19727e05da759fc595ea..8503b587ac362ad28d73b6c7bd0a3d3d56e267d9 100644 --- a/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/packages/rocketchat-i18n/i18n/de.i18n.json @@ -17,6 +17,8 @@ "Accessing_permissions": "Zugriff auf Berechtigungen", "Account_SID": "Konto-SID", "Accounts": "Konten", + "Accounts_AllowAnonymousRead": "Erlaube Anonymes lesen", + "Accounts_AllowAnonymousWrite": "Erlaube Anonymes schreiben", "Accounts_AllowDeleteOwnAccount": "Benutzern erlauben, ihr Konto zu löschen", "Accounts_AllowedDomainsList": "Liste von erlaubten Domains", "Accounts_AllowedDomainsList_Description": "Durch Kommata getrennte Liste von erlaubten Domains", @@ -103,6 +105,8 @@ "Accounts_OAuth_Wordpress_id": "WordPress-ID", "Accounts_OAuth_Wordpress_secret": "Geheimer WordPress Schlüssel", "Accounts_PasswordReset": "Passwort zurücksetzen", + "Accounts_OAuth_Proxy_host": "Proxy Host", + "Accounts_OAuth_Proxy_services": "Proxy Service", "Accounts_Registration_AuthenticationServices_Default_Roles": "Standardrolle für Authentifizierungsdienste", "Accounts_Registration_AuthenticationServices_Default_Roles_Description": "Standardrollen die Benutzern zugewiesen werden, die sich über Authentifizierungsdienste registrieren", "Accounts_Registration_AuthenticationServices_Enabled": "Anmeldung mit Authentifizierungsdiensten", @@ -130,12 +134,12 @@ "Add_custom_oauth": "Benutzerdefiniertes OAuth-Konto hinzufügen", "Add_Domain": "Domain hinzufügen", "Add_manager": "Manager hinzufügen", - "Add_user": "Benutzer hinzufügen", - "Add_User": "Benutzer hinzufügen", - "Add_users": "Benutzer hinzufügen", + "Add_user": "BenutzerIn hinzufügen", + "Add_User": "BenutzerIn hinzufügen", + "Add_users": "BenutzerInnen hinzufügen", "Adding_OAuth_Services": "Hinzufügen von OAuth-Services", "Adding_permission": "Berechtigung hinzufügen", - "Adding_user": "Benutzer hinzufügen", + "Adding_user": "Füge BenutzerIn hinzu", "Additional_emails": "Zusätzliche E-Mails", "Additional_Feedback": "Zusätzliches Feedback", "Administration": "Administration", @@ -175,6 +179,7 @@ "API_Drupal_URL_Description": "Beispiel: https://domain.de (ohne schließenden /)", "API_Embed": "Einbetten", "API_Embed_Description": "Eingebettete Link Vorschau für Links die von Benutzern gepostet wurden aktiv.", + "API_EmbedCacheExpirationDays": "Tage bis zum Ablauf den eingebetteten Caches", "API_EmbedDisabledFor": "Einbettungen für Benutzer deaktivieren", "API_EmbedDisabledFor_Description": "Durch Kommata getrennte Liste von Benutzernamen", "API_EmbedIgnoredHosts": "Ignorierte Hosts einbetten", @@ -182,9 +187,11 @@ "API_EmbedSafePorts": "Sichere Ports", "API_EmbedSafePorts_Description": "Kommagetrennte Liste der Ports, für die eine Vorschau erlaubt ist.", "API_Enable_CORS": "Aktiviere CORS", + "API_Enable_Direct_Message_History_EndPoint": "Aktiviere den Endpunkt für den Verlauf von Direkt Nachrichten", "API_GitHub_Enterprise_URL": "Server-URL", "API_GitHub_Enterprise_URL_Description": "Beispiel: http://domain.com (ohne Schrägstrich am Ende)", "API_Gitlab_URL": "GitLab-URL", + "API_Shield_Types": "Shield Typ", "API_Token": "API-Token", "API_Upper_Count_Limit": "Max. Anzahl an Einträgen", "API_Upper_Count_Limit_Description": "Max. Anzahl an Einträgen die die REST API zurückliefen soll (sofern ohne Einschränkung)? ", @@ -205,6 +212,7 @@ "Assign_admin": "Admin zuweisen", "at": "am", "Attachment_File_Uploaded": "Datei hochgeladen", + "Attribute_handling": "Behandlung von Attributen", "Auth_Token": "Auth-Token", "Author": "Autor", "Authorization_URL": "Autorisierungs-URL", @@ -237,8 +245,10 @@ "Back": "Zurück", "Back_to_applications": "Zurück zu den Anwendungen", "Back_to_integrations": "Zurück zu Integrationen", + "Back_to_integration_detail": "Zurück zu den Integrations Details", "Back_to_login": "Zurück zum Login", "Back_to_permissions": "Zurück zu den Berechtigungen", + "Block_User": "BenutzerIn sperren", "Body": "Body", "bold": "fett", "bot_request": "Bot-Anfrage", @@ -257,10 +267,13 @@ "Cancel": "Abbrechen", "Cancel_message_input": "Abbrechen", "Cannot_invite_users_to_direct_rooms": "Benutzer können nicht in private Nachrichtenräume eingeladen werden.", + "CAS_autoclose": "Login Pupp automatisch schließen", + "CAS_base_url_Description": "Haupt URL des externen Singe Sign On Services e.g: https://sso.example.undef/sso/", "CAS_button_color": "Hintergrundfarbe des Login-Buttons", "CAS_button_label_color": "Farbe des Login-Button-Texts", "CAS_button_label_text": "Text des Login-Buttons", "CAS_enabled": "Aktiviert", + "CAS_login_url_Description": "Login URL des externen Singe Sign On Services e.g: https://sso.example.undef/sso/login", "CAS_popup_height": "Höhe des Login Pop Up", "CAS_popup_width": "Breite des Login Pop Up", "CAS_Sync_User_Data_Enabled": "Benutzerdaten immer synchronisieren", @@ -291,8 +304,10 @@ "Choose_the_username_that_this_integration_will_post_as": "Wählen Sie den Benutzernamen, der die Integration veröffentlicht.", "clear": "löschen", "clear_cache_now": "Zwischenspeicher jetzt leeren", + "clear_history": "Verlauf löschen", "Clear_all_unreads_question": "Möchten Sie alle ungelesenen Nachrichten löschen?", "Click_here": "Hier klicken", + "Click_here_for_more_info": "Hier klicken für weitere Informationen", "Client_ID": "Client-ID", "Client_Secret": "Client-Schlüssel", "Clients_will_refresh_in_a_few_seconds": "Clients werden in wenigen Sekunden aktualisiert", @@ -336,15 +351,23 @@ "Custom_Fields": "Benutzerdefinierte Felder", "Custom_oauth_helper": "Bei der Einrichtung muss eine Rückruf-URL angegeben werden. Benutze dafür folgende URL: <pre>%s</pre>", "Custom_oauth_unique_name": "Name des OAuth-Kontos", + "Custom_Scripts": "Benutzerdefinierte Skripte", "Custom_Script_Logged_In": "Benutzerdefiniertes Script für angemeldete Benutzer", "Custom_Script_Logged_Out": "Benutzerdefiniertes Script für abgemeldete Benutzer", + "Custom_Sounds": "Benutzerdefinierte Töne", + "Custom_Sound_Add": "Benutzerdefinierte Töne hinzufügen", + "Custom_Sound_Delete_Warning": "Ein gelöschter Ton kann nicht wiederhergestellt werden.", + "Custom_Sound_Error_Invalid_Sound": "Fehlerhafter Ton", "Custom_Translations": "Benutzerdefinierte Übersetzungen", "Dashboard": "Dashboard", "Date": "Datum", + "Date_From": "von", + "Date_to": "bis", "days": "Tage", "DB_Migration": "Datenbankmigration", "DB_Migration_Date": "Datenbankmigrationsdatum", "Deactivate": "Deaktivieren", + "Decline": "ablehnen", "Default": "Voreinstellung", "Delete": "Löschen", "Delete_message": "Nachricht löschen", @@ -366,6 +389,8 @@ "Desktop_Notifications_Enabled": "Desktop-Benachrichtigungen sind aktiviert.", "Direct_message_someone": "Jemandem eine private Nachricht schicken", "Direct_Messages": "Private Nachrichten", + "Disable_Notifications": "Benachrichtigungen deaktivieren", + "Disable_two-factor_authentication": "2 Faktor Authentifizierung deaktivieren", "Display_offline_form": "Offline Nachricht anzeigen", "Displays_action_text": "Zeigt Aktionstext", "Do_you_want_to_change_to_s_question": "Möchten Sie dies zu <strong>%s</strong> ändern?", @@ -384,7 +409,7 @@ "Edit_Department": "Abteilung bearbeiten", "edited": "bearbeitet", "Editing_room": "Raum bearbeiten", - "Editing_user": "Benutzer bearbeiten", + "Editing_user": "BenutzerIn bearbeiten", "Email": "E-Mail", "Email_address_to_send_offline_messages": "E-Mail-Adresse zum Senden von Offline-Nachrichten", "Email_already_exists": "Die E-Mail-Adresse existiert bereits.", @@ -403,7 +428,9 @@ "Empty_title": "Es wurde kein Titel angegeben.", "Enable": "Aktivieren", "Enable_Desktop_Notifications": "Aktivieren", + "Enable_two-factor_authentication": "2-Faktor Authentifizierung aktivieren", "Enabled": "Aktiviert", + "Enable_Svg_Favicon": "SVG Favicon aktivieren", "Encrypted_message": "Verschlüsselte Nachricht", "End_OTR": "OTR beenden", "Enter_a_regex": "Regex eingeben", @@ -437,9 +464,11 @@ "error-invalid-channel-start-with-chars": "Ungültiger Kanal. Beginnen Sie mit @ oder #", "error-invalid-custom-field": "Ungültiges benutzerdefiniertes Feld", "error-invalid-custom-field-name": "Unzulässiger Name für ein benutzerdefiniertes Feld. Benutze nur Buchstaben, Nummern, Binde- und Unterstriche.", + "error-invalid-date": "Das eingegebene Datum ist fehlerhaft.", "error-invalid-description": "Ungültige Beschreibung", "error-invalid-domain": "Ungültige Domain", "error-invalid-email": "Ungültige E-Mail-Adresse: __email__", + "error-invalid-email-address": "Fehlerhafte E-Mail-Adresse", "error-invalid-file-height": "Ungültige Dateihöhe", "error-invalid-file-type": "Ungültiges Dateiformat", "error-direct-message-file-upload-not-allowed": "Dateiaustausch ist in direkten Nachrichten nicht möglich.", @@ -499,8 +528,10 @@ "File_exceeds_allowed_size_of_bytes": "Die Datei ist größer als das erlaubte Maximum von __size__ Bytes", "File_not_allowed_direct_messages": "Dateiaustausch ist in direkten Nachrichten nicht möglich.", "File_type_is_not_accepted": "Feldtyp nicht akzeptiert.", + "File_uploaded": "Datei hochladen", "FileUpload": "Dateien hochladen", "FileUpload_Enabled": "Hochladen von Dateien aktivieren", + "FileUpload_Disabled": "Datei Uploads ", "FileUpload_Enabled_Direct": "Dateiaustausch ist in direkten Nachrichten möglich.", "FileUpload_File_Empty": "Datei ist leer", "FileUpload_FileSystemPath": "Systempfad", @@ -524,17 +555,22 @@ "Follow_social_profiles": "Folge uns in sozialen Netzwerken, fork uns auf GitHub und teile deine Gedanken über die Rocket.Chat-App auf unserem Trello-Board.", "Food_and_Drink": "Essen & Trinken", "Footer": "Fußzeile", + "Fonts": "Schriften", "For_your_security_you_must_enter_your_current_password_to_continue": "Geben Sie zu Ihrer Sicherheit Ihr aktuelles Passwort ein um fortzufahren.", "Force_SSL": "SSL erzwingen", "Force_SSL_Description": "*Achtung!* _Force SSL_ solte niemals mit einem Reverse-Proxy verwendet werden. Falls Sie einen Reverse-Proxy verwenden, sollten Sie die Weiterleitung DORT einrichten. Dies Option existiert für Anwendungen wie Heroku, die keine Weiterleitungskonfigurationen für Reverse-Proxy erlauben.", "Forgot_password": "Passwort vergessen?", + "Forgot_Password_Email_Subject": "[Site_Name] - Passwort Wiederherstellung", + "Forgot_Password_Email": "<a href=\"[Forgot_Password_Url]\">Hier</a> Klicken um das Passwort zurückzusetzen.", + "Forgot_password_section": "Passwort vergessen", "Forward": "Weiterleiten", "Forward_chat": "Chat weiterleiten", "Forward_to_department": "An Abteilung weiterleiten", - "Forward_to_user": "An Benutzer weiterleiten", + "Forward_to_user": "An BenutzerIn weiterleiten", "Frequently_Used": "Häufig verwendet", + "Friday": "Freitag", "From": "Absender", - "From_Email": "Absender", + "From_Email": "E-Mail Absender", "From_email_warning": "<b>Warnung</b>: Der <b>Absender</b> ist Gegenstand deiner Mail-Server-Einstellungen.", "General": "Allgemeines", "github_no_public_email": "Sie haben keine öffentliche E-Mail-Adresse in Ihrem GitHub-Account.", @@ -545,6 +581,7 @@ "Guest_Pool": "Gästepool", "Hash": "Hash", "Header": "Kopfzeile", + "Header_and_Footer": "Kopf und Fusszeile", "Hidden": "Versteckt", "Hide_Avatars": "Avatar verstecken", "Hide_flextab": "Rechte Seitenleiste über Klick verstecken", @@ -552,6 +589,7 @@ "Hide_Private_Warning": "Sind sie sicher, das Gespräch mit \"%s\" zu verstecken?", "Hide_room": "Raum verstecken", "Hide_Room_Warning": "Sind sie sicher, den Raum \"%s\" zu verstecken?", + "Hide_roles": "Rollen verstecken", "Hide_usernames": "Benutzernamen ausblenden", "Highlights": "Hervorhebungen", "Highlights_How_To": "Um benachrichtigt zu werden, wenn ein Wort oder Ausdruck erwähnt wird, fügen Sie ihn hier hinzu. Sie können Wörter und Ausdrücke mit Kommata trennen. Die Wörter zur Hervorhebung beachten die Groß- und Kleinschreibung nicht.", @@ -598,9 +636,17 @@ "Installation": "Installation", "Installed_at": "Installationsdatum", "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Anweisungen an Ihre Besucher: Füllen Sie das Formular aus, um eine Nachricht zu senden.", + "Integration_Advanced_Settings": "Erweiterte Einstellungen", "Integration_added": "Die Integration wurde hinzugefügt.", "Integration_Incoming_WebHook": "Eingehende WebHook-Integration", "Integration_New": "Neue Integration", + "Integrations_Outgoing_Type_FileUploaded": "Hochgeladene Dateien", + "Integrations_Outgoing_Type_RoomArchived": "Archivierte Räume", + "Integrations_Outgoing_Type_RoomCreated": "Raum erstellt (öffentlich und privat)", + "Integrations_Outgoing_Type_RoomJoined": "BenutzerIn hat den Raum betreten", + "Integrations_Outgoing_Type_RoomLeft": "BenutzerIn hat den Raum verlassen", + "Integrations_Outgoing_Type_SendMessage": "Nachricht gesendet", + "Integrations_Outgoing_Type_UserCreated": "BenutzerIn angelegt.", "Integration_Outgoing_WebHook": "Ausgehende WebHook-Integration", "Integration_Word_Trigger_Placement_Description": "Soll das auslösende Wort irgendwo im Satz stehen und nicht nur am Anfang? ", "Integration_updated": "Die Integration wurde aktualisiert.\n", @@ -618,15 +664,17 @@ "Invalid_pass": "Es muss ein Passwort angegeben werden.", "Invalid_room_name": "<strong>%s</strong> ist kein zulässiger Raumname.<br/> Verwenden Sie nur Buchstaben, Zahlen oder Binde- und Unterstriche.", "Invalid_secret_URL_message": "Die angegebene URL ist ungültig.", + "Invalid_two_factor_code": "Fehlerhafter 2-Faktor Code", "invisible": "unsichtbar", "Invisible": "Unsichtbar", + "Invitation": "Einladung", "Invitation_HTML": "Einladungstext (HTML)", "Invitation_HTML_Default": "<h2> Sie wurden eingeladen zu <h1> [Site_Name] </h1></h2><p> Besuchen Sie zu [Site_URL] und probieren Sie heute die beste verfügbare Open-Source-Chat-Lösung aus! </p>", "Invitation_HTML_Description": "Sie können die folgenden Platzhalter verwenden: <br /><ul><li> [email] für den Empfänger der E-Mail. </li><li> [Site_Name] und [Site_URL] jeweils für den Anwendungsnamen und die URL. </li></ul>", "Invitation_Subject": "Einladungsbetreff", "Invitation_Subject_Default": "Sie wurden zu [Site_Name] eingeladen", - "Invite_user_to_join_channel": "Benutzer in diesen Kanal einladen", - "Invite_Users": "Benutzer einladen", + "Invite_user_to_join_channel": "BenutzerIn in diesen Kanal einladen", + "Invite_Users": "BenutzerInnen einladen", "is_also_typing": "schreibt auch", "is_also_typing_female": "schreibt auch", "is_also_typing_male": "schreibt auch", @@ -639,6 +687,7 @@ "Jitsi_Enable_Channels": "Aktivieren in Kanälen", "join": "Beitreten", "Join_audio_call": "Anruf beitreiten", + "Join_Chat": "Chat beitreten", "Join_default_channels": "Standardkanälen beitreten", "Join_the_Community": "Trete der Community bei", "Join_the_given_channel": "Diesem Kanal beitreten", @@ -718,6 +767,7 @@ "Leave_Private_Warning": "Sind sie sicher, das Gespräch mit \"%s\" zu verlassen?", "Leave_room": "Raum verlassen", "Leave_Room_Warning": "Sind sie sicher, den Raum \"%s\" zu verlassen?", + "Leave_the_current_channel": "Aktuellen Kanal verlassen", "line": "Zeilen", "List_of_Channels": "Liste der Kanäle", "List_of_Direct_Messages": "Liste der Direktnachrichten", @@ -810,7 +860,9 @@ "Message_ShowFormattingTips": "Formatierungstipps anzeigen", "Message_starring": "Markieren von Nachrichten", "Message_TimeFormat": "Zeitformat", + "Message_TimeAndDateFormat": "Zeit und Datumsformat", "Message_TimeFormat_Description": "Siehe auch: <a href=\"http://momentjs.com/docs/#/displaying/format/\" target=\"momemt\">Moment.js</a>", + "Message_TimeAndDateFormat_Description": "Siehe auch: <a href=\"http://momentjs.com/docs/#/displaying/format/\" target=\"momemt\">Moment.js</a>", "Message_too_long": "Diese Nachricht ist zu lang.", "Message_VideoRecorderEnabled": "Videoaufnahmen eingeschaltet", "Message_VideoRecorderEnabledDescription": "Videoformat auf webm beim \"Datei hochladen\" einschränken? (Video abspielen funktioniert dann in fast allen Browsern)", @@ -818,11 +870,14 @@ "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Nachrichten, die an den eingehenden Webhook gesendet werden, werden hier veröffentlicht.", "Meta": "Metadaten", "Meta_fb_app_id": "Facebook-App-ID", + "Meta_custom": "Benutzerdefinierte Meta Tags", "Meta_google-site-verification": "Google-Seiten-Verifizierung", "Meta_language": "Sprache", "Meta_msvalidate01": "MSValidate.01", "Meta_robots": "Roboter", + "Min_length_is": "Die minimale länge beträgt %s", "minutes": "Minuten", + "Mobile": "Mobil", "Monday": "Montag", "Monitor_history_for_changes_on": "Verlaufsänderungen beobachten für", "More_channels": "Mehr Kanäle", @@ -886,16 +941,19 @@ "Off_the_record_conversation_is_not_available_for_your_browser_or_device": "Off-the-record-Gespräche sind für Ihren Browser oder Ihr Gerät nicht verfügbar.", "Office_Hours": "Bürozeiten", "Office_hours_enabled": "Bürozeiten aktiviert", + "Office_hours_updated": "Bürozeiten aktualisiert", "Offline": "Offline", "Offline_DM_Email": "Sie haben eine private Nachricht von __user__ erhalten.", "Offline_form": "Offline-Formular", "Offline_form_unavailable_message": "Nachricht, dass Offline Formular ungültig", + "Offline_Link_Message": "gehe zur Nachricht", "Offline_Mention_Email": "Sie wurden von __user__ in #__room__ erwähnt.", "Offline_message": "Offline-Nachricht", "Offline_success_message": "Nachricht, dass Offline Nachricht erfolgreich", "Offline_unavailable": "offline - nicht verfügbar", "On": "Ein", "Online": "Online", + "Only_On_Desktop": "Desktop Modus (senden mit der \"Enter\" Taste nur auf dem Desktop PC)", "Only_you_can_see_this_message": "Nur Sie können diese Nachricht sehen.", "Oops!": "Hoppla", "Open": "Öffnen", @@ -941,10 +999,12 @@ "Please_enter_value_for_url": "Bitte geben Sie eine URL für Ihr Profilbild ein.", "Please_enter_your_new_password_below": "Bitte geben Sie Ihr neues Passwort ein:", "Please_enter_your_password": "Bitte Passwort eingeben", + "please_enter_valid_domain": "Bitte eine valide Domain eingeben", "Please_fill_a_label": "Bitte Bezeichnung ausfüllen", "Please_fill_a_name": "Bitte geben Sie einen Namen ein.", "Please_fill_a_username": "Bitte geben Sie einen Benutzernamen ein.", "Please_fill_name_and_email": "Bitte geben Sie einen Namen und eine E-Mail-Adresse ein.", + "Please_select_an_user": "Bitte einen Benutzer auswählen", "Please_select_enabled_yes_or_no": "Bitte wählen Sie eine Option für \"aktiviert\".", "Please_wait": "Bitte warten", "Please_wait_activation": "Bitte warten, der Vorgang kann einige Zeit in Anspruch nehmen.", @@ -988,14 +1048,23 @@ "quote": "Zitat", "Quote": "Zitieren", "Random": "Zufällig", + "React_when_read_only": "Reaktionen erlauben", "Reacted_with": "Reagierte mit", "Reactions": "Reaktionen", + "Read_only": "Nur lesend", + "Read_only_channel": "Kanal nur lesbar", + "Read_only_group": "Nur lesbar Gruppe", "Record": "Aufnehmen", "Redirect_URI": "Weiterleitungs-URL", + "Refresh_oauth_services": "oAuth Service aktualisieren", "Refresh_keys": "Schlüssel aktualisieren", "Refresh_your_page_after_install_to_enable_screen_sharing": "Aktualisieren Sie die Seite nach der Installation, um die Bildschirmübertragung zu aktivieren.", + "Regenerate_codes": "Codes neu generieren", "Register": "Neues Konto registrieren", + "Registration": "Registrierung", "Registration_Succeeded": "Ihre Registrierung war erfolgreich.", + "Registration_via_Admin": "Registrierung via Admin", + "Regular_Expressions": "Reguläre Ausdrücke", "Release": "Veröffentlichung", "Remove": "Entfernen", "Remove_Admin": "Admin entfernen", @@ -1003,8 +1072,10 @@ "Remove_as_owner": "als Besitzer entfernen", "Remove_custom_oauth": "OAuth-Konto entfernen", "Remove_from_room": "Aus dem Raum entfernen", + "Remove_last_admin": "Entferne den letzen Admin", "Remove_someone_from_room": "Jemanden aus dem Raum entfernen", "Removed": "Entfernt", + "Reply": "Antwort", "Report_Abuse": "Missbrauch melden", "Report_exclamation_mark": "Melden!", "Report_sent": "Bericht gesendet", @@ -1028,10 +1099,16 @@ "room_changed_topic": "Das Thema des Raums wurde von <em>__user_by__</em> zu <em>__room_topic__</em> geändert.", "Room_description_changed_successfully": "Raumbeschreibung erfolgreich geändert", "Room_has_been_deleted": "Der Raum wurde gelöscht.", + "Room_has_been_archived": "Der Raum wurde archiviert.", + "Room_has_been_unarchived": "Der Raum wurde dearchiviert.", "Room_Info": "Raum", + "room_is_blocked": "Der räum ist geblockt", + "room_is_read_only": "Der Raum ist nur lesbar", + "room_name": "Raum Name", "Room_name_changed": "<em>__user_by__</em> hat den Raumnamen zu <em>__room_name__</em> geändert.", "Room_name_changed_successfully": "Der Raumname wurde erfolgreich geändert.", "Room_not_found": "Der Raum konnte nicht gefunden werden.", + "Room_password_changed_successfully": "Das Raum Passwort wurde erfolgreich geändert", "Room_topic_changed_successfully": "Das Thema des Raums wurde erfolgreich geändert.", "Room_type_changed_successfully": "Der Raumtyp wurde erfolgreich geändert.", "Room_unarchived": "Der Raum wurde wiederhergestellt.", @@ -1046,6 +1123,8 @@ "SAML_Custom_Generate_Username": "Benutzernamen generieren", "SAML_Custom_Issuer": "Benutzerdefinierter Aussteller", "SAML_Custom_Provider": "Benutzerdefinierter Provider", + "SAML_Custom_Public_Cert": "Öffentliches Zertifikat", + "SAML_Custom_Private_Key": "Privater Schlüssel", "Saturday": "Samstag", "Save": "Speichern", "Save_changes": "Änderungen speichern", @@ -1099,6 +1178,7 @@ "Show_all": "Alle Nutzer zeigen", "Show_more": "Mehr Nutzer zeigen", "show_offline_users": "Zeige Benutzer an, die offline sind", + "Show_on_registration_page": "Auf der Registrierungsseite anzeigen", "Show_only_online": "Nur Online-Nutzer zeigen", "Show_preregistration_form": "Vorregistrierungsformular zeigen", "Showing_archived_results": "<b>%s</b> archivierte Räume", @@ -1117,6 +1197,7 @@ "Slash_Topic_Description": "Thema setzen", "Slash_Topic_Params": "Themennachricht", "Smarsh_Enabled": "Smarsh aktiviert", + "Smarsh_MissingEmail_Email": "Fehlende E-Mail", "Smileys_and_People": "Gesichter & Personen", "SMS_Enabled": "SMS aktiviert", "SMTP": "SMTP", @@ -1125,6 +1206,7 @@ "SMTP_Port": "SMTP-Port", "SMTP_Test_Button": "SMTP-Einstellungen testen", "SMTP_Username": "SMTP-Benutzername", + "Snippet_Added": "Erstellt am %s", "Sound": "Ton", "SSL": "SSL", "Star_Message": "Nachricht markieren", @@ -1165,7 +1247,9 @@ "Symbols": "Symbole", "Sync_success": "Die Synchronisierung war erfolgreich.", "Sync_Users": "Benutzer synchronisieren", + "System_messages": "System Nachrichten", "Tag": "Tag", + "TargetRoom": "Ziel Raum!", "Test_Connection": "Testverbindung", "Test_Desktop_Notifications": "Desktop-Benachrichtigungen testen", "Thank_you_exclamation_mark": "Vielen Dank!", @@ -1200,11 +1284,16 @@ "There_are_no_agents_added_to_this_department_yet": "Es wurden bisher keine Agenten zu dieser Abteilung hinzugefügt.", "There_are_no_integrations": "Es sind keine Integrationen vorhanden.", "There_are_no_users_in_this_role": "Es sind dieser Rolle keine Benutzer zugeordnet.", + "This_conversation_is_already_closed": "Die Unterhaltung wurde bereits beendet.", "This_email_has_already_been_used_and_has_not_been_verified__Please_change_your_password": "Diese E-Mail wurde bereits verschickt, aber noch nicht bestätigt. Bitte ändern Sie Ihr Passwort.", "This_is_a_desktop_notification": "Das ist eine Desktop-Benachrichtigung.", "This_is_a_push_test_messsage": "Dies ist eine Test-Push-Nachricht.", "This_room_has_been_archived_by__username_": "Dieser Raum wurde von __username__ archiviert", "This_room_has_been_unarchived_by__username_": "Dieser Raum wurde von __username__ unarchiviert", + "Two-factor_authentication": "2-Faktor Authentifizierung", + "Two-factor_authentication_disabled": "2-Faktor Authentifizierung deaktiviert", + "Two-factor_authentication_enabled": "2-Faktor Authentifizierung aktiviert", + "Two-factor_authentication_is_currently_disabled": "2-Faktor Authentifizierung ist momentan deaktiviert", "Thursday": "Donnerstag", "Time_in_seconds": "Zeit in Sekunden", "Title": "Titel", @@ -1216,6 +1305,8 @@ "To_users": "An die Benutzer", "Topic": "Thema", "Travel_and_Places": "Reisen & Orte", + "Translated": "übersetzt", + "Translations": "Übersetzungen", "Trigger_removed": "Auslöser entfernt", "Trigger_Words": "Trigger Words", "Triggers": "Auslöser", @@ -1228,7 +1319,9 @@ "Type_your_new_password": "Geben Sie Ihr neues Passwort ein", "UI_DisplayRoles": "Rollen anzeigen", "UI_Merge_Channels_Groups": "Führe private Gruppen und Kanäle zusammen", + "UI_Use_Real_Name": "Benutze den Realen Namen", "Unarchive": "Wiederherstellen", + "Unblock_User": "Benutzer entsperren", "Unmute_someone_in_room": "Jemanden das Chatten in einem Raum wieder erlauben", "Unmute_user": "Benutzern das Chatten erlauben ", "Unnamed": "Unbenannt", @@ -1273,6 +1366,7 @@ "User_left_male": "Der Benutzer <em>__user_left__</em> hat den Kanal verlassen.", "User_logged_out": "Der Benutzer wurde abgemeldet.", "User_management": "Benutzerverwaltung", + "User_muted": "Benutzer stummgeschaltet", "User_muted_by": "Dem Benutzer <em>__user_muted__</em> wurde das Chatten von <em>__user_by__</em> verboten.", "User_not_found": "Der Benutzer konnte nicht gefunden werden.", "User_not_found_or_incorrect_password": "Entweder konnte der Benutzer nicht gefunden werden oder Sie haben ein falsches Passwort angegeben.", @@ -1297,15 +1391,23 @@ "Username_title": "Benutzernamen festlegen", "Username_wants_to_start_otr_Do_you_want_to_accept": "__username__ möchte ein OTR-Gespräch starten. Möchten Sie annehmen?", "Users": "Benutzer", + "Users_added": "Die Benutzer wurden hinzugefügt", "Users_in_role": "Zugeordnete Nutzer", "UTF8_Names_Slugify": "UTF8-Namen-Slugify", "UTF8_Names_Validation": "UTF8-Namen-Verifizierung", "UTF8_Names_Validation_Description": "Erlauben Sie keine Sonderzeichen und Leerzeichen. Sie können - _ und . verwenden, aber nicht am Ende eines Namens.", + "Validate_email_address": "E-Mail-Adresse bestätigen", + "Verification": "Überprüfung ", "Verification_email_sent": "Bestätigungsmail gesendet", + "Verification_Email_Subject": "[Site_Name] - Bestätige dein Benutzerkonto", + "Verification_Email": "Klicke <a href=\"[Verification_Url]\">hier</a> um dein Benutzerkonto zu bestätigen.", "Verified": "Verifiziert", + "Verify": "überprüfen", "Version": "Version", "Video_Chat_Window": "Video-Chat", + "Video_Conference": "Video-Konferenz", "Videocall_declined": "Videoanruf abgelehnt.", + "Videocall_enabled": "Videoanruf aktiviert", "View_All": "Alle ansehen", "View_Logs": "Logs anzeigen", "View_mode": "Ansichts-Modus", @@ -1319,6 +1421,7 @@ "Visitor_page_URL": "URL der Besucherseite", "Visitor_time_on_site": "Besucherzeit auf der Seite", "Wait_activation_warning": "Bevor Sie sich anmelden können, muss das Konto von einem Administrator manuell aktiviert werden.", + "Warnings": "Warnungen", "We_are_offline_Sorry_for_the_inconvenience": "Wir sind offline. Entschuldigen Sie die Unannehmlichkeiten.", "We_have_sent_password_email": "Wir haben Ihnen eine Anleitung zum Zurücksetzen des Passworts an Ihre E-Mail-Adresse gesendet. Wenn Sie keine E-Mail erhalten haben, versuchen Sie es bitte noch einmal.", "We_have_sent_registration_email": "Wir haben Ihnen eine Bestätigungsmail gesendet. Wenn Sie keine E-Mail erhalten haben, versuchen Sie es bitte noch einmal.", diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 1d18d31ef92382a2ac30d5dd485734ed7c9739f6..0be9c641d9cff4742e049ab1dceed31fc3ec5c9f 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -267,6 +267,7 @@ "BotHelpers_userFields": "User Fields", "BotHelpers_userFields_Description": "CSV of user fields that can be accessed by bots helper methods.", "Branch": "Branch", + "Broadcast_Connected_Instances": "Broadcast Connected Instances", "Bugsnag_api_key": "Bugsnag API Key", "busy": "busy", "Busy": "Busy", @@ -362,6 +363,7 @@ "CROWD_Reject_Unauthorized": "Reject Unauthorized", "CRM_Integration": "CRM Integration", "Current_Chats": "Current Chats", + "Current_Status": "Current Status", "Custom": "Custom", "Custom_Emoji": "Custom Emoji", "Custom_Emoji_Add": "Add New Emoji", @@ -706,6 +708,7 @@ "Install_FxOs_follow_instructions": "Please confirm the app installation on your device (press \"Install\" when prompted).", "Installation": "Installation", "Installed_at": "Installed at", + "Instance_Record": "Instance Record", "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Instructions to your visitor fill the form to send a message", "Impersonate_user": "Impersonate User", "Impersonate_user_description": "When enabled, integration posts as the user that triggered integration", @@ -1256,6 +1259,7 @@ "Reset_password": "Reset password", "Restart": "Restart", "Restart_the_server": "Restart the server", + "Retry_Count": "Retry Count", "Role": "Role", "Role_Editing": "Role Editing", "Role_removed": "Role removed", @@ -1560,6 +1564,7 @@ "Unread_Rooms": "Unread Rooms", "Unread_Rooms_Mode": "Unread Rooms Mode", "Unstar_Message": "Remove Star", + "Updated_at": "Updated at", "Upload_file_description": "File description", "Upload_file_name": "File name", "Upload_file_question": "Upload file?", diff --git a/packages/rocketchat-i18n/i18n/fr.i18n.json b/packages/rocketchat-i18n/i18n/fr.i18n.json index 7d9b43e0c9fcb5533db0a5d4bffa9cd8a4ecb787..96b29850cf853a08c020201eca7c637cd2d21fce 100644 --- a/packages/rocketchat-i18n/i18n/fr.i18n.json +++ b/packages/rocketchat-i18n/i18n/fr.i18n.json @@ -10,13 +10,15 @@ "__username__is_no_longer__role__defined_by__user_by_": "__user_by__ a retiré le rôle __role__ à __username__", "__username__was_set__role__by__user_by_": "__user_by__ a donné le rôle __role__ à __username__", "Accept": "Accepter", - "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Accepter les demande de chat en direct même si il n'y a pas d'assistant en ligne", - "Accept_with_no_online_agents": "Accepter sans assistant enligne", + "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Accepter les demandes de chat en ligne même si il n'y a pas d'agent en ligne", + "Accept_with_no_online_agents": "Accepter sans agent en ligne", "Access_not_authorized": "Accès non autorisé", "Access_Token_URL": "URL du jeton d'accès", "Accessing_permissions": "Accès aux permissions", "Account_SID": "SID du compte", "Accounts": "Comptes", + "Accounts_AllowAnonymousRead": "Autoriser la lecture anonyme", + "Accounts_AllowAnonymousWrite": "Autoriser l'écriture anonyme", "Accounts_AllowDeleteOwnAccount": "Autoriser les utilisateurs à supprimer leur propre compte", "Accounts_AllowedDomainsList": "Liste des domaines autorisés", "Accounts_AllowedDomainsList_Description": "Liste des domaines autorisés, séparés par des virgules", @@ -34,6 +36,7 @@ "Accounts_BlockedUsernameList": "Liste des noms d'utilisateurs bloqués", "Accounts_BlockedUsernameList_Description": "Liste de noms d'utilisateurs bloqués (insensible à la casse), séparés par des virgules", "Accounts_CustomFields_Description": "Devrait être un JSON valide où les clés sont les noms des champs contenant un dictionnaire de champs de paramétrage. Exemple :<br/>\n<code>{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"eleve\",\n  \"options\": [\"enseignant\", \"eleve\"],\n  \"required\": true,\n  \"modifyRecordField\": {\n   \"array\": true,\n   \"field\": \"roles\"\n  }\n },\n \"twitter\": {\n  \"type\": \"text\",\n  \"required\": true,\n  \"minLength\": 2,\n  \"maxLength\": 10\n }\n}</code> ", + "Accounts_DefaultUsernamePrefixSuggestion": "Suggestion par défaut du préfixe du nom d'utilisateur", "Accounts_denyUnverifiedEmail": "Refuser les e-mails non vérifiés", "Accounts_EmailVerification": "Vérification de l'adresse e-mail", "Accounts_EmailVerification_Description": "Vous devez avoir des paramètres SMTP corrects pour utiliser cette fonctionnalité", @@ -137,8 +140,8 @@ "Additional_Feedback": "Commentaires supplémentaires", "Administration": "Administration", "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "Après l'authentification par OAuth2, les utilisateurs seront redirigés vers cette URL", - "Agent": "Assistant", - "Agent_added": "Assistant ajouté", + "Agent": "Agent", + "Agent_added": "Agent ajouté", "Agent_removed": "Assistant supprimé", "Alias": "Alias", "Alias_Format": "Format d'alias", @@ -377,8 +380,8 @@ "Delete_Room_Warning": "Supprimer un salon supprimera également tous les messages postés dans le salon. Cette action est irréversible.", "Delete_User_Warning": "Supprimer un utilisateur va également supprimer tous les messages de celui-ci. Cette action est irréversible.", "Deleted": "Supprimé !", - "Department": "Département", - "Department_removed": "Département supprimé", + "Department": "Service", + "Department_removed": "Service supprimé", "Departments": "Départements", "Deployment_ID": "ID de déploiement", "Description": "Description", @@ -409,7 +412,7 @@ "Duration": "Durée", "Edit": "Modifier", "Edit_Custom_Field": "Modifier le champ personnalisé", - "Edit_Department": "Éditer le département", + "Edit_Department": "Éditer le service", "Edit_Trigger": "Éditer le déclencheur", "edited": "modifié", "Editing_room": "Modification du salon", @@ -459,7 +462,7 @@ "error-could-not-change-name": "Impossible de modifier le nom", "error-could-not-change-username": "Impossible de modifier le nom d'utilisateur", "error-delete-protected-role": "Impossible de supprimer un rôle protégé", - "error-department-not-found": "Département introuvable", + "error-department-not-found": "Service introuvable", "error-duplicate-channel-name": "Un canal avec le nom '__channel_name__' existe déjà ", "error-email-domain-blacklisted": "Le domaine de l'adresse e-mail est sur liste noire", "error-email-send-failed": "Erreur lors de la tentative d'envoi d'e-mail : __message__", @@ -581,7 +584,7 @@ "Forgot_password_section": "Mot de passe oublié", "Forward": "Transmettre", "Forward_chat": "Transmettre la conversation", - "Forward_to_department": "Transmettre au département", + "Forward_to_department": "Transmettre au service", "Forward_to_user": "Transmettre à l'utilisateur", "Frequently_Used": "Fréquemment utilisé", "Friday": "Vendredi", @@ -614,10 +617,10 @@ "Host": "Hôte", "hours": "heures", "Hours": "Heures", - "How_friendly_was_the_chat_agent": "L'assistant du chat était-il amical ?", - "How_knowledgeable_was_the_chat_agent": "L'assistant du chat était-il clair ?", + "How_friendly_was_the_chat_agent": "Votre interlocuteur était-il amical ?", + "How_knowledgeable_was_the_chat_agent": "Votre interlocuteur était-il clair ?", "How_long_to_wait_after_agent_goes_offline": "Délai d'attente après que l'agent soit hors ligne", - "How_responsive_was_the_chat_agent": "L'assistant du chat avait-il des réponses adaptées ?", + "How_responsive_was_the_chat_agent": "Votre interlocuteur avait-il des réponses adaptées ?", "How_satisfied_were_you_with_this_chat": "Étiez-vous satisfait de ce chat?", "How_to_handle_open_sessions_when_agent_goes_offline": "Comment gérer les sessions ouvertes lorsque l'asistant passe hors ligne", "If_this_email_is_registered": "Si cet e-mail est enregistré, les instructions pour réinitialiser votre mot de passe vous serons envoyées. Si vous ne recevez pas d'email rapidement, merci de revenir et d'essayer à nouveau.", @@ -953,12 +956,12 @@ "N_new_messages": "%s nouveaux messages", "Name": "Nom", "Name_cant_be_empty": "Le nom ne peut pas être vide", - "Name_of_agent": "Nom de l'assistant", + "Name_of_agent": "Nom de l'agent", "Name_optional": "Nom (optionnel)", "Navigation_History": "Historique de navigation", "New_Application": "Nouvelle application", "New_Custom_Field": "Nouveau champ personnalisé", - "New_Department": "Nouveau département", + "New_Department": "Nouveau service", "New_integration": "Nouvelle intégration", "New_logs": "Nouveaux journaux", "New_Message_Notification": "Notification de nouveau message", @@ -968,7 +971,7 @@ "New_Room_Notification": "Notification de nouveau salon", "New_videocall_request": "Nouvelle demande d'appel vidéo", "New_Trigger": "Nouveau déclencheur", - "No_available_agents_to_transfer": "Aucun assistant disponible pour le transfert", + "No_available_agents_to_transfer": "Aucun agent disponible pour le transfert", "No_channel_with_name_%s_was_found": "Aucun canal nommé <strong>\"%s\"</strong> n'a été trouvé !", "No_channels_yet": "Vous ne faites partie d’aucun canal pour le moment.", "No_direct_messages_yet": "Vous n'avez pris part à aucune discussion pour le moment.", @@ -1029,6 +1032,7 @@ "optional": "facultatif", "or": "ou", "Order": "Ordre", + "Or_talk_as_anonymous": "Ou discutez de manière anonyme", "OS_Arch": "Architecture", "OS_Cpus": "Nombre de CPU", "OS_Freemem": "Mémoire disponible", @@ -1216,7 +1220,7 @@ "seconds": "secondes", "Secret_token": "Jeton secret", "Security": "Sécurité", - "Select_a_department": "Sélectionner un département", + "Select_a_department": "Sélectionner un service", "Select_a_user": "Sélectionner un utilisateur", "Select_an_avatar": "Choisissez un avatar", "Select_file": "Sélectionnez le fichier", @@ -1261,6 +1265,7 @@ "Showing_archived_results": "<p>Affichage de <b>%s</b> résultats archivés</p>", "Showing_online_users": "<b>__total_showing__</b> utilisateur(s) affichés sur un total de __total__", "Showing_results": "<p><b>%s</b> résultat(s)</p>", + "Sign_in_to_start_talking": "Connectez vous pour commencer à discuter", "since_creation": "depuis %s", "Site_Name": "Nom du site", "Site_Url": "URL du site", @@ -1550,7 +1555,7 @@ "Yes_leave_it": "Oui, je veux partir !", "Yes_mute_user": "Oui, rend muet l'utilisateur !", "Yes_remove_user": "Oui, éjecte l'utilisateur !", - "You": "Toi", + "You": "Vous", "you_are_in_preview_mode_of": "Aperçu du salon #<strong>__room_name__</strong> ", "You_are_logged_in_as": "Vous êtes connecté en tant que", "You_are_not_authorized_to_view_this_page": "Vous n'avez pas l'autorisation de voir cette page.", diff --git a/packages/rocketchat-i18n/i18n/ko.i18n.json b/packages/rocketchat-i18n/i18n/ko.i18n.json index 02492e8aa80cd41ca749842fdf2d6630aa591094..c02eee6cb01bfbc2afe97bc3623e8a4dce8561ad 100644 --- a/packages/rocketchat-i18n/i18n/ko.i18n.json +++ b/packages/rocketchat-i18n/i18n/ko.i18n.json @@ -6,17 +6,17 @@ "403": "금지ë¨", "500": "ë‚´ë¶€ 서버 오류", "@username": "@사용ìžëª…", - "@username_message": "@username <message>", - "__username__is_no_longer__role__defined_by__user_by_": "__ ì‚¬ìš©ìž ì´ë¦„ __는 __user_by__ì— ì˜í•´, __role__ ë” ì´ìƒ 없다", - "__username__was_set__role__by__user_by_": "__ ì‚¬ìš©ìž ì´ë¦„ __ì€ __user_by__ì— ì˜í•´ __role__ ì„¤ì •í–ˆë‹¤", + "@username_message": "@사용ìžì´ë¦„ <message>", + "__username__is_no_longer__role__defined_by__user_by_": "__사용ìžëŠ” ë”ì´ìƒ __ê°€ì„¤ì •í•œ __ì—í• ì´ ì—†ìŠµë‹ˆë‹¤", + "__username__was_set__role__by__user_by_": "__ 사용ìžì—게 __사용ìžê°€ __ì—í• ì„__ ì„¤ì •í•˜ì˜€ìŠµë‹ˆë‹¤", "Accept": "수ë½", - "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "온ë¼ì¸ ìƒë‹´ì›ì´ì—†ëŠ” 경우ì—ë„ ë“¤ì–´ì˜¤ëŠ” 실시간 채팅 ìš”ì² ìˆ˜ë½", - "Access_not_authorized": "액세스 ê¶Œí•œì´ ì—†ìŠµë‹ˆë‹¤", + "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "ìƒë‹´ì›ì´ 온ë¼ì¸ ìƒíƒœê°€ 아닌 경우ì—ë„ ë¼ì´ë¸Œì±—ì„ ìˆ˜ë½í•©ë‹ˆë‹¤.", + "Access_not_authorized": "엑세스 ê¶Œí•œì´ ì—†ìŠµë‹ˆë‹¤", "Access_Token_URL": "액세스 í† í° URL", - "Accessing_permissions": "권한 액세스", + "Accessing_permissions": "ì ‘ì†ê¶Œí•œ", "Account_SID": "ê³„ì • SID", "Accounts": "ê³„ì •", - "Accounts_AllowDeleteOwnAccount": "사용ìžê°€ ìžì‹ ì˜ ê³„ì •ì„ ì‚ì œí• ìˆ˜ 있습니다.", + "Accounts_AllowDeleteOwnAccount": "사용ìžê°€ ìžì‹ ì˜ ê³„ì •ì„ ì‚ì œí• ìˆ˜ 있습니다", "Accounts_AllowedDomainsList": "í—ˆìš©ëœ ë„ë©”ì¸ ëª©ë¡", "Accounts_AllowedDomainsList_Description": "í—ˆìš©ëœ ë„ë©”ì¸ì„ 쉼표(,)로 구분하기", "Accounts_AllowEmailChange": "ì´ë©”ì¼ ë³€ê²½ì„ í—ˆìš©í•©ë‹ˆë‹¤", @@ -29,10 +29,10 @@ "Accounts_AvatarStorePath": "아바타 ì €ìž¥ 경로", "Accounts_AvatarStoreType": "아바파 ì €ìž¥ 타입", "Accounts_BlockedDomainsList": "ì°¨ë‹¨ëœ ë„ë©”ì¸ ëª©ë¡", - "Accounts_BlockedDomainsList_Description": "차단 ëœ ë„ë©”ì¸ì˜ 쉼표로 구분 ëœ ëª©ë¡", - "Accounts_BlockedUsernameList": "차단 ëœ ì‚¬ìš©ìž ì´ë¦„ 목ë¡", - "Accounts_BlockedUsernameList_Description": "차단 ëœ ì‚¬ìš©ìž ì´ë¦„ì˜ ì‰¼í‘œë¡œ 구분 ëœ ëª©ë¡ (대소 ë¬¸ìž êµ¬ë¶„)", - "Accounts_CustomFields_Description": "키는 필드 ì„¸íŒ…ì˜ ë”•ì…”ë„ˆë¦¬(dictionary) 를 í¬í•¨í•˜ëŠ” 필드 ì´ë¦„들ì´ì–´ì•¼ 합니다.\n\n예:<br/><code>{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"student\",\n  \"options\": [\"teacher\", \"student\"],\n  \"required\": true,\n  \"modifyRecordField\": {\n   \"array\": true,\n   \"field\": \"roles\"\n  }\n },\n \"twitter\": {\n  \"type\": \"text\",\n  \"required\": true,\n  \"minLength\": 2,\n  \"maxLength\": 10\n }\n}</code> ", + "Accounts_BlockedDomainsList_Description": "쉼표로 êµ¬ë¬¸ëœ ì°¨ë‹¨ ë„ë©”ì¸ ë¦¬ìŠ¤íŠ¸", + "Accounts_BlockedUsernameList": "ì°¨ë‹¨ëœ ì‚¬ìš©ìž ë¦¬ìŠ¤íŠ¸", + "Accounts_BlockedUsernameList_Description": "쉼표로 êµ¬ë¬¸ëœ ì°¨ë‹¨ ì‚¬ìš©ìž ë¦¬ìŠ¤íŠ¸ (대소 ë¬¸ìž êµ¬ë¶„)", + "Accounts_CustomFields_Description": "필드 ì„¤ì •ì— í¬í•¨ëœ í•„ë“œëª…ì„ ì‚¬ìš©í•œ 올바른 JSON ì´ì—¬ì•¼ 합니다.\n\n예:<br/><code>{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"student\",\n  \"options\": [\"teacher\", \"student\"],\n  \"required\": true,\n  \"modifyRecordField\": {\n   \"array\": true,\n   \"field\": \"roles\"\n  }\n },\n \"twitter\": {\n  \"type\": \"text\",\n  \"required\": true,\n  \"minLength\": 2,\n  \"maxLength\": 10\n }\n}</code> ", "Accounts_denyUnverifiedEmail": "확ì¸ë˜ì§€ ì•Šì€ ì´ë©”ì¼ ê±°ë¶€", "Accounts_EmailVerification": "ì´ë©”ì¼ í™•ì¸", "Accounts_EmailVerification_Description": "ì´ ê¸°ëŠ¥ì„ ì‚¬ìš©í•˜ë ¤ë©´ SMTPì„¤ì •ì´ ì˜¬ë°”ë¥´ê²Œ ë˜ì–´ìžˆëŠ”ì§€ 확ì¸í•´ì£¼ì‹ì‹œì˜¤.", @@ -40,7 +40,7 @@ "Accounts_Enrollment_Email_Default": "<h2> ì— ì˜¤ì‹ ê²ƒì„ í™˜ì˜í•©ë‹ˆë‹¤ <h1> [Site_Name] </h1></h2><p> [Site_URL]로 ì´ë™í•˜ì—¬ ì˜¤ëŠ˜ë‚ ìµœê³ ì˜ ì˜¤í”ˆ 소스 채팅 솔루션ì„ë³´ì‹ì‹œì˜¤! </p>", "Accounts_Enrollment_Email_Description": "ë‹¹ì‹ ì€ ê°ê° 사용ìžì˜ ì „ì²´ ì´ë¦„, ì´ë¦„ ë˜ëŠ” ì„±ì„ ìœ„í•´ [lname], [name], [fname]ì„ ì‚¬ìš©í• ìˆ˜ 있습니다. <br /> ë‹¹ì‹ ì€ ì‚¬ìš©ìžì˜ ì´ë©”ì¼ì„ [email]ì„ ì‚¬ìš©í• ìˆ˜ 있습니다.", "Accounts_Enrollment_Email_Subject_Default": "[Site_Name] ì— ì˜¤ì‹ ê²ƒì„ í™˜ì˜í•©ë‹ˆë‹¤ ", - "Accounts_ForgetUserSessionOnWindowClose": "윈ë„루를 ë‹«ì„ ë•Œì— ì‚¬ìš©ìž ì„¤ì •ì„ ì‚ì œ 합니다.", + "Accounts_ForgetUserSessionOnWindowClose": "ì°½ì„ ë‹«ì„때 ì‚¬ìš©ìž ì„¸ì…˜ì„ ì‚ì œí•©ë‹ˆë‹¤", "Accounts_Iframe_api_method": "API 메소드", "Accounts_Iframe_api_url": "API URL", "Accounts_iframe_enabled": "사용", @@ -61,8 +61,11 @@ "Accounts_OAuth_Custom_Token_Path": "Token 경로", "Accounts_OAuth_Custom_Token_Sent_Via": "í† í° ë³´ë‚¸ 비아", "Accounts_OAuth_Custom_Username_Field": "ì‚¬ìš©ìž ì´ë¦„ 필드", - "Accounts_OAuth_Drupal": "Drupal Login ì´ í™œì„±í™” ë¨", - "Accounts_OAuth_Facebook": "Facebook 로그ì¸", + "Accounts_OAuth_Drupal": "듀팔 ë¡œê·¸ì¸ ì´ í™œì„±í™” ë˜ì—ˆìŠµë‹ˆë‹¤.", + "Accounts_OAuth_Drupal_callback_url": "듀팔 oAuth2 리다ì´ë ‰íЏ URI", + "Accounts_OAuth_Drupal_id": "듀팔 oAuth2 í´ë¼ì´ì–¸íЏ ID", + "Accounts_OAuth_Drupal_secret": "듀팔 oAuth2 í´ë¼ì´ì–¸íЏ 비밀번호", + "Accounts_OAuth_Facebook": "페ì´ìŠ¤ë¶ ë¡œê·¸ì¸", "Accounts_OAuth_Facebook_callback_url": "페ì´ìФ ë¶ ì½œë°± URL", "Accounts_OAuth_Facebook_id": "Facebook 앱 ID", "Accounts_OAuth_Facebook_secret": "Facebook 암호", @@ -101,6 +104,7 @@ "Accounts_PasswordReset": "암호 ìž¬ì„¤ì •", "Accounts_OAuth_Proxy_host": "프ë¡ì‹œ 서버", "Accounts_OAuth_Proxy_services": "프ë¡ì‹œ 서비스", + "Accounts_Registration_AuthenticationServices_Default_Roles": "ì¸ì¦ì„œë¹„스용 기본 ì—í• ", "Accounts_Registration_AuthenticationServices_Enabled": "ì¸ì¦ ì„œë¹„ìŠ¤ì— ë“±ë¡", "Accounts_RegistrationForm": "ë“±ë¡ ì–‘ì‹", "Accounts_RegistrationForm_Disabled": "비활성화", @@ -111,43 +115,43 @@ "Accounts_RegistrationForm_SecretURL_Description": "ë‹¹ì‹ ì€ ë‹¹ì‹ ì˜ ë“±ë¡ URLì— ì¶”ê°€ë©ë‹ˆë‹¤ ìž„ì˜ì˜ 문ìžì—´ì„ ì œê³µí•´ì•¼í•©ë‹ˆë‹¤. 예 : https://demo.rocket.chat/register/[secret_hash]", "Accounts_RequireNameForSignUp": "íšŒì› ê°€ìž…ì€ ì´ë¦„ í•„ìš”", "Accounts_SetDefaultAvatar": "기본 아바타 ì„¤ì •", - "Accounts_ShowFormLogin": "보기 ì–‘ì‹ ê¸°ë°˜ 로그ì¸", - "Accounts_UseDefaultBlockedDomainsList": "기본값 사용 차단 ëœ ë„ë©”ì¸ ëª©ë¡", - "Accounts_UseDNSDomainCheck": "DNS ë„ë©”ì¸ í™•ì¸ì„ 사용하여", + "Accounts_ShowFormLogin": "í¼ë°©ì‹ ë¡œê·¸ì¸ ë³´ê¸°", + "Accounts_UseDefaultBlockedDomainsList": "기본 차단 ë„ë©”ì¸ë¦¬ìŠ¤íŠ¸ 사용", + "Accounts_UseDNSDomainCheck": "DNS ë„ë©”ì¸ í™•ì¸ ì‚¬ìš©", "Accounts_UserAddedEmail_Default": "<h2> ì— ì˜¤ì‹ ê²ƒì„ í™˜ì˜í•©ë‹ˆë‹¤ <h1> [Site_Name] </h1></h2><p> [Site_URL]로 ì´ë™í•˜ì—¬ ì˜¤ëŠ˜ë‚ ìµœê³ ì˜ ì˜¤í”ˆ 소스 채팅 솔루션ì„ë³´ì‹ì‹œì˜¤! </p><p> [email]ê³¼ 비밀번호 : [password] ë‹¹ì‹ ì€ ë‹¹ì‹ ì˜ ì´ë©”ì¼ì„ 사용하여 ë¡œê·¸ì¸ í• ìˆ˜ 있습니다. ë‹¹ì‹ ì€ ì²˜ìŒ ë¡œê·¸ì¸ í›„ 변경해야 í• ìˆ˜ 있습니다.", "Accounts_UserAddedEmail_Description": "다ìŒê³¼ ê°™ì€ ìžë¦¬ë¥¼ ì‚¬ìš©í• ìˆ˜ 있습니다 : <br /><ul><li> ê°ê° 사용ìžì˜ ì „ì²´ ì´ë¦„, ì´ë¦„ ë˜ëŠ” 성ì„위한 [name], [fname], [lname]. </li><li> 사용ìžì˜ ì´ë©”ì¼ [email]. </li><li> 사용ìžì˜ 비밀번호 [password]. </li><li> [Site_Name]와 [Site_URL] ê°ê° ì‘ìš© 프로그램 ì´ë¦„ ë° URL합니다. </li></ul>", - "Accounts_UserAddedEmailSubject_Default": "ë‹¹ì‹ ì´ ì¶”ê°€ë˜ì—ˆìŠµë‹ˆë‹¤ [Site_Name]", + "Accounts_UserAddedEmailSubject_Default": "ë‹¹ì‹ ì´ì€ [Site_Name] ì— ì¶”ê°€ë˜ì—ˆìŠµë‹ˆë‹¤", "Activate": "활성화", "Activity": "활ë™", "Add": "추가", - "Add_agent": "ì—ì´ì „트를 추가", + "Add_agent": "ìƒë‹´ì‚¬ 추가", "Add_custom_oauth": "ì‚¬ìš©ìž ì •ì˜ OAuth 추가", "Add_Domain": "ë„ë©”ì¸ ì¶”ê°€", "Add_manager": "ê´€ë¦¬ìž ì¶”ê°€", "Add_user": "ì‚¬ìš©ìž ì¶”ê°€", "Add_User": "ì‚¬ìš©ìž ì¶”ê°€", "Add_users": "ì‚¬ìš©ìž ì¶”ê°€", - "Adding_OAuth_Services": "OAuth는 서비스 추가", - "Adding_permission": "ê¶Œí•œì„ ì¶”ê°€", - "Adding_user": "추가 사용ìž", - "Additional_emails": "추가 E - ë©”ì¼", + "Adding_OAuth_Services": "OAuth 서비스 추가", + "Adding_permission": "권한 추가", + "Adding_user": "ì‚¬ìš©ìž ì¶”ê°€", + "Additional_emails": "추가 ì´ë©”ì¼", "Additional_Feedback": "추가 ì˜ê²¬", "Administration": "관리", - "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "OAuth2를 ì¸ì¦ 한 후, 사용ìžëŠ”ì´ URL로 ë¦¬ë””ë ‰ì…˜ë©ë‹ˆë‹¤", - "Agent": "ì—ì´ì „트", - "Agent_added": "ì—ì´ì „트는 추가", - "Agent_removed": "ì—ì´ì „트 ì œê±°", + "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "Oauth2 ì¸ì¦í›„ 사용ìžëŠ” ì´ URL로 ì´ë™ë©ë‹ˆë‹¤", + "Agent": "ìƒë‹´ì‚¬", + "Agent_added": "ìƒë‹´ì‚¬ê°€ 추가ë˜ì—ˆìŠµë‹ˆë‹¤", + "Agent_removed": "ìƒë‹´ì‚¬ê°€ ì‚ì œë˜ì—ˆìŠµë‹ˆë‹¤", "Alias": "별명", - "Alias_Format": "ë³„ì¹ í˜•ì‹", + "Alias_Format": "별명 형ì‹", "Alias_Set": "ë³„ì¹ ì„¤ì •", "All": "ëª¨ë“ ", "All_channels": "ëª¨ë“ ì±„ë„", "All_logs": "ëª¨ë“ ë¡œê·¸", "All_messages": "ëª¨ë“ ë©”ì‹œì§€", - "Allow_Invalid_SelfSigned_Certs": "ìž˜ëª»ëœ Self-Signed Certs 허용", - "Allow_Invalid_SelfSigned_Certs_Description": "ë§í¬ í™•ì¸ ë° ë¯¸ë¦¬ë³´ê¸° 무효 ë° ìžì²´ 서명 ëœ SSL ì¸ì¦ì„œì˜ 허용.", - "Allow_switching_departments": "방문ìžê°€ 부서를 ë³€ê²½í• ìˆ˜ 있ë„ë¡ í—ˆìš©í•¨", - "Analytics_features_enabled": "기능 활성화", + "Allow_Invalid_SelfSigned_Certs": "ìž˜ëª»ëœ ìžì²´ì„œëª… Certs 를 허용합니다", + "Allow_Invalid_SelfSigned_Certs_Description": "ë§í¬í™•ì¸ ê³¼ í”„ë¦¬ë·°ì— ìž˜ëª»ëœ ìžì²´ì„œëª… Certs 를 허용합니다.", + "Allow_switching_departments": "방문ìžê°€ 부서를 ë³€ê²½í• ìˆ˜ 있ë„ë¡ í—ˆìš©í•©ë‹ˆë‹¤", + "Analytics_features_enabled": "ê¸°ëŠ¥ì´ í™œì„±í™” ë˜ì—ˆìŠµë‹ˆë‹¤", "Analytics_features_messages_Description": "사용ìžê°€ ë©”ì‹œì§€ì— ëŒ€í•´ 수행 í–‰ë™ê³¼ ê´€ë ¨ëœ ì‚¬ìš©ìž ì •ì˜ ì´ë²¤íŠ¸ë¥¼ ì¶”ì 합니다.", "Analytics_features_rooms_Description": "ì±„ë„ ë˜ëŠ” 그룹 (ì‚ì œë‘ê³ ìž‘ì„±)ì— ëŒ€í•œ ìž‘ì—…ì— ê´€ë ¨ëœ ì‚¬ìš©ìž ì •ì˜ ì´ë²¤íŠ¸ë¥¼ ì¶”ì 합니다.", "Analytics_features_users_Description": "ì‚¬ìš©ìž (암호 ìž¬ì„¤ì • 시간, 프로필 사진 변경 등)ì— ê´€ë ¨ ìž‘ì—…ì— ê´€ë ¨ëœ ì‚¬ìš©ìž ì •ì˜ ì´ë²¤íŠ¸ë¥¼ ì¶”ì 합니다.", diff --git a/packages/rocketchat-i18n/i18n/no.i18n.json b/packages/rocketchat-i18n/i18n/no.i18n.json index d1f6bfb211eccddde2a17c9c3008d8043a9e8833..e71892775f9f6456fcbe31f9c0484a76d624eaa7 100644 --- a/packages/rocketchat-i18n/i18n/no.i18n.json +++ b/packages/rocketchat-i18n/i18n/no.i18n.json @@ -1,3 +1,68 @@ { - "0_Errors_Only": "0 - Kun Feil" + "0_Errors_Only": "0 - Kun Feil", + "1_Errors_and_Information": "1 - Feil og informasjon", + "2_Erros_Information_and_Debug": "2 - Feil, Informasjon og Feilsøking", + "403": "Forbudt", + "500": "Intern server feil", + "Accept": "Aksepter", + "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Aksepter innkommende livechat selv om det ikke er noen online", + "Accept_with_no_online_agents": "Aksepter uten online agenter", + "Access_not_authorized": "Aksepter uautoriserte", + "Accounts_Enrollment_Email_Subject_Default": "Velkommen til [Site_Name]", + "Accounts_PasswordReset": "Reset passord", + "Accounts_RegistrationForm_Disabled": "Deaktivert", + "Accounts_RegistrationForm_Public": "Offentlig", + "Activate": "Aktiver", + "Activity": "Aktivitet", + "Add": "Legg til", + "Add_agent": "Legg til agent", + "Add_Domain": "Legg til domene", + "Add_manager": "Legg til leder", + "Add_user": "Legg til bruker", + "Add_User": "Legg til bruker", + "Add_users": "Legg til brukere", + "Adding_permission": "Legger til rettigheter", + "Adding_user": "Legger til bruker", + "Additional_emails": "Ekstra e-postadresser", + "Additional_Feedback": "Ekstra tilbakemelding", + "Administration": "Administrasjon", + "Agent": "Agent", + "Agent_added": "Lagt til agent", + "Agent_removed": "Fjernet agent", + "All": "Alle", + "All_channels": "Alle kanaler", + "All_logs": "Aller logger", + "All_messages": "Alle meldinger", + "Archive": "Arkiv", + "are_also_typing": "skriver ogsÃ¥", + "are_typing": "skriver", + "Are_you_sure": "Er du sikker?", + "Author": "Forfatter", + "Available": "Tilgjengelig", + "Available_agents": "Tilgjengelige agenter", + "away": "borte", + "Away": "Borte", + "away_female": "borte", + "Away_female": "Borte", + "away_male": "borte", + "Away_male": "Borte", + "Back": "Tilbake", + "Back_to_applications": "Tilbake til programmer", + "Back_to_integrations": "Tilbake til integrasjoner", + "Back_to_login": "Tilbake til login", + "Back_to_permissions": "Tilbake til rettigheter", + "Block_User": "Blokker bruker", + "bold": "fet", + "busy": "opptatt", + "Busy": "Opptatt", + "busy_female": "opptatt", + "Busy_female": "Opptatt", + "busy_male": "opptatt", + "Busy_male": "Opptatt", + "by": "av", + "Content": "Innhold", + "Cancel": "Avbryt", + "Cancel_message_input": "Avbryt", + "channel": "kanal", + "Channel": "Kanal" } \ No newline at end of file diff --git a/packages/rocketchat-importer-slack/main.coffee b/packages/rocketchat-importer-slack/main.coffee deleted file mode 100644 index da0518e538746aec7087d8489e95646a4ad79a8a..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer-slack/main.coffee +++ /dev/null @@ -1,3 +0,0 @@ -Importer.addImporter 'slack', Importer.Slack, - name: 'Slack' - mimeType: 'application/zip' diff --git a/packages/rocketchat-importer-slack/main.js b/packages/rocketchat-importer-slack/main.js new file mode 100644 index 0000000000000000000000000000000000000000..d7298a2700e171abca5073c04849820a2731345c --- /dev/null +++ b/packages/rocketchat-importer-slack/main.js @@ -0,0 +1,5 @@ +/* globals Importer */ +Importer.addImporter('slack', Importer.Slack, { + name: 'Slack', + mimeType: 'application/zip' +}); diff --git a/packages/rocketchat-importer-slack/package.js b/packages/rocketchat-importer-slack/package.js index e10bc401cca15d69565ad74e099498bc66ad307f..04eb0571ac4a183dd21f6cdbcef7e9be020caa1c 100644 --- a/packages/rocketchat-importer-slack/package.js +++ b/packages/rocketchat-importer-slack/package.js @@ -8,11 +8,10 @@ Package.describe({ Package.onUse(function(api) { api.use([ 'ecmascript', - 'coffeescript', 'rocketchat:lib', 'rocketchat:importer' ]); api.use('rocketchat:logger', 'server'); - api.addFiles('server.coffee', 'server'); - api.addFiles('main.coffee', ['client', 'server']); + api.addFiles('server.js', 'server'); + api.addFiles('main.js', ['client', 'server']); }); diff --git a/packages/rocketchat-importer-slack/server.coffee b/packages/rocketchat-importer-slack/server.coffee deleted file mode 100644 index bbfa2a0504748d762e3d59f9955806d320fc49ad..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer-slack/server.coffee +++ /dev/null @@ -1,373 +0,0 @@ -Importer.Slack = class Importer.Slack extends Importer.Base - constructor: (name, descriptionI18N, mimeType) -> - super(name, descriptionI18N, mimeType) - @userTags = [] - @bots = {} - @logger.debug('Constructed a new Slack Importer.') - - prepare: (dataURI, sentContentType, fileName) => - super(dataURI, sentContentType, fileName) - - {image, contentType} = RocketChatFile.dataURIParse dataURI - zip = new @AdmZip(new Buffer(image, 'base64')) - zipEntries = zip.getEntries() - - tempChannels = [] - tempUsers = [] - tempMessages = {} - for entry in zipEntries - do (entry) => - if entry.entryName.indexOf('__MACOSX') > -1 - #ignore all of the files inside of __MACOSX - @logger.debug("Ignoring the file: #{entry.entryName}") - else if entry.entryName == 'channels.json' - @updateProgress Importer.ProgressStep.PREPARING_CHANNELS - tempChannels = JSON.parse entry.getData().toString() - tempChannels = tempChannels.filter (channel) -> channel.creator? - else if entry.entryName == 'users.json' - @updateProgress Importer.ProgressStep.PREPARING_USERS - tempUsers = JSON.parse entry.getData().toString() - - for user in tempUsers when user.is_bot - @bots[user.profile.bot_id] = user - - else if not entry.isDirectory and entry.entryName.indexOf('/') > -1 - item = entry.entryName.split('/') #random/2015-10-04.json - channelName = item[0] #random - msgGroupData = item[1].split('.')[0] #2015-10-04 - if not tempMessages[channelName] - tempMessages[channelName] = {} - # Catch files which aren't valid JSON files, ignore them - try - tempMessages[channelName][msgGroupData] = JSON.parse entry.getData().toString() - catch - @logger.warn "#{entry.entryName} is not a valid JSON file! Unable to import it." - - # Insert the users record, eventually this might have to be split into several ones as well - # if someone tries to import a several thousands users instance - usersId = @collection.insert { 'import': @importRecord._id, 'importer': @name, 'type': 'users', 'users': tempUsers } - @users = @collection.findOne usersId - @updateRecord { 'count.users': tempUsers.length } - @addCountToTotal tempUsers.length - - # Insert the channels records. - channelsId = @collection.insert { 'import': @importRecord._id, 'importer': @name, 'type': 'channels', 'channels': tempChannels } - @channels = @collection.findOne channelsId - @updateRecord { 'count.channels': tempChannels.length } - @addCountToTotal tempChannels.length - - # Insert the messages records - @updateProgress Importer.ProgressStep.PREPARING_MESSAGES - messagesCount = 0 - for channel, messagesObj of tempMessages - do (channel, messagesObj) => - if not @messages[channel] - @messages[channel] = {} - for date, msgs of messagesObj - messagesCount += msgs.length - @updateRecord { 'messagesstatus': "#{channel}/#{date}" } - - if Importer.Base.getBSONSize(msgs) > Importer.Base.MaxBSONSize - for splitMsg, i in Importer.Base.getBSONSafeArraysFromAnArray(msgs) - messagesId = @collection.insert { 'import': @importRecord._id, 'importer': @name, 'type': 'messages', 'name': "#{channel}/#{date}.#{i}", 'messages': splitMsg } - @messages[channel]["#{date}.#{i}"] = @collection.findOne messagesId - else - messagesId = @collection.insert { 'import': @importRecord._id, 'importer': @name, 'type': 'messages', 'name': "#{channel}/#{date}", 'messages': msgs } - @messages[channel][date] = @collection.findOne messagesId - - @updateRecord { 'count.messages': messagesCount, 'messagesstatus': null } - @addCountToTotal messagesCount - - if tempUsers.length is 0 or tempChannels.length is 0 or messagesCount is 0 - @logger.warn "The loaded users count #{tempUsers.length}, the loaded channels #{tempChannels.length}, and the loaded messages #{messagesCount}" - @updateProgress Importer.ProgressStep.ERROR - return @getProgress() - - selectionUsers = tempUsers.map (user) -> - return new Importer.SelectionUser user.id, user.name, user.profile.email, user.deleted, user.is_bot, !user.is_bot - selectionChannels = tempChannels.map (channel) -> - return new Importer.SelectionChannel channel.id, channel.name, channel.is_archived, true, false - - @updateProgress Importer.ProgressStep.USER_SELECTION - return new Importer.Selection @name, selectionUsers, selectionChannels - - startImport: (importSelection) => - super(importSelection) - start = Date.now() - - for user in importSelection.users - for u in @users.users when u.id is user.user_id - u.do_import = user.do_import - @collection.update { _id: @users._id }, { $set: { 'users': @users.users }} - - for channel in importSelection.channels - for c in @channels.channels when c.id is channel.channel_id - c.do_import = channel.do_import - @collection.update { _id: @channels._id }, { $set: { 'channels': @channels.channels }} - - startedByUserId = Meteor.userId() - Meteor.defer => - @updateProgress Importer.ProgressStep.IMPORTING_USERS - for user in @users.users when user.do_import - do (user) => - Meteor.runAsUser startedByUserId, () => - existantUser = RocketChat.models.Users.findOneByEmailAddress user.profile.email - if not existantUser - existantUser = RocketChat.models.Users.findOneByUsername user.name - - if existantUser - user.rocketId = existantUser._id - RocketChat.models.Users.update { _id: user.rocketId }, { $addToSet: { importIds: user.id } } - @userTags.push - slack: "<@#{user.id}>" - slackLong: "<@#{user.id}|#{user.name}>" - rocket: "@#{existantUser.username}" - else - if user.profile.email - userId = Accounts.createUser { email: user.profile.email, password: Date.now() + user.name + user.profile.email.toUpperCase() } - else - userId = Accounts.createUser { username: user.name, password: Date.now() + user.name, joinDefaultChannelsSilenced: true } - Meteor.runAsUser userId, () => - Meteor.call 'setUsername', user.name, {joinDefaultChannelsSilenced: true} - url = null - if user.profile.image_original - url = user.profile.image_original - else if user.profile.image_512 - url = user.profile.image_512 - - try - Meteor.call 'setAvatarFromService', url, undefined, 'url' - catch error - this.logger.warn "Failed to set #{user.name}'s avatar from url #{url}" - - # Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600 - if user.tz_offset - Meteor.call 'userSetUtcOffset', user.tz_offset / 3600 - - RocketChat.models.Users.update { _id: userId }, { $addToSet: { importIds: user.id } } - - if user.profile.real_name - RocketChat.models.Users.setName userId, user.profile.real_name - #Deleted users are 'inactive' users in Rocket.Chat - if user.deleted - Meteor.call 'setUserActiveStatus', userId, false - #TODO: Maybe send emails? - user.rocketId = userId - @userTags.push - slack: "<@#{user.id}>" - slackLong: "<@#{user.id}|#{user.name}>" - rocket: "@#{user.name}" - @addCountCompleted 1 - @collection.update { _id: @users._id }, { $set: { 'users': @users.users }} - - @updateProgress Importer.ProgressStep.IMPORTING_CHANNELS - for channel in @channels.channels when channel.do_import - do (channel) => - Meteor.runAsUser startedByUserId, () => - existantRoom = RocketChat.models.Rooms.findOneByName channel.name - if existantRoom or channel.is_general - if channel.is_general and channel.name isnt existantRoom?.name - Meteor.call 'saveRoomSettings', 'GENERAL', 'roomName', channel.name - channel.rocketId = if channel.is_general then 'GENERAL' else existantRoom._id - RocketChat.models.Rooms.update { _id: channel.rocketId }, { $addToSet: { importIds: channel.id } } - else - users = [] - for member in channel.members when member isnt channel.creator - user = @getRocketUser member - if user? - users.push user.username - - userId = startedByUserId - for user in @users.users when user.id is channel.creator and user.do_import - userId = user.rocketId - - Meteor.runAsUser userId, () => - returned = Meteor.call 'createChannel', channel.name, users - channel.rocketId = returned.rid - - # @TODO implement model specific function - roomUpdate = - ts: new Date(channel.created * 1000) - - if not _.isEmpty channel.topic?.value - roomUpdate.topic = channel.topic.value - - if not _.isEmpty(channel.purpose?.value) - roomUpdate.description = channel.purpose.value - - RocketChat.models.Rooms.update { _id: channel.rocketId }, { $set: roomUpdate, $addToSet: { importIds: channel.id } } - - @addCountCompleted 1 - @collection.update { _id: @channels._id }, { $set: { 'channels': @channels.channels }} - - missedTypes = {} - ignoreTypes = { 'bot_add': true, 'file_comment': true, 'file_mention': true } - @updateProgress Importer.ProgressStep.IMPORTING_MESSAGES - for channel, messagesObj of @messages - do (channel, messagesObj) => - Meteor.runAsUser startedByUserId, () => - slackChannel = @getSlackChannelFromName channel - if slackChannel?.do_import - room = RocketChat.models.Rooms.findOneById slackChannel.rocketId, { fields: { usernames: 1, t: 1, name: 1 } } - for date, msgs of messagesObj - @updateRecord { 'messagesstatus': "#{channel}/#{date}.#{msgs.messages.length}" } - for message in msgs.messages - msgDataDefaults = - _id: "slack-#{slackChannel.id}-#{message.ts.replace(/\./g, '-')}" - ts: new Date(parseInt(message.ts.split('.')[0]) * 1000) - - if message.type is 'message' - if message.subtype? - if message.subtype is 'channel_join' - if @getRocketUser(message.user)? - RocketChat.models.Messages.createUserJoinWithRoomIdAndUser room._id, @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'channel_leave' - if @getRocketUser(message.user)? - RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser room._id, @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'me_message' - msgObj = - msg: "_#{@convertSlackMessageToRocketChat(message.text)}_" - _.extend msgObj, msgDataDefaults - RocketChat.sendMessage @getRocketUser(message.user), msgObj, room, true - else if message.subtype is 'bot_message' or message.subtype is 'slackbot_response' - botUser = RocketChat.models.Users.findOneById 'rocket.cat', { fields: { username: 1 }} - botUsername = if @bots[message.bot_id] then @bots[message.bot_id]?.name else message.username - msgObj = - msg: @convertSlackMessageToRocketChat(message.text) - rid: room._id - bot: true - attachments: message.attachments - username: if botUsername then botUsername else undefined - - _.extend msgObj, msgDataDefaults - - if message.edited? - msgObj.editedAt = new Date(parseInt(message.edited.ts.split('.')[0]) * 1000) - editedBy = @getRocketUser(message.edited.user) - if editedBy? - msgObj.editedBy = - _id: editedBy._id - username: editedBy.username - - if message.icons? - msgObj.emoji = message.icons.emoji - - RocketChat.sendMessage botUser, msgObj, room, true - else if message.subtype is 'channel_purpose' - if @getRocketUser(message.user)? - RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser 'room_changed_description', room._id, message.purpose, @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'channel_topic' - if @getRocketUser(message.user)? - RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser 'room_changed_topic', room._id, message.topic, @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'channel_name' - if @getRocketUser(message.user)? - RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser room._id, message.name, @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'pinned_item' - if message.attachments - msgObj = - attachments: [ - "text" : @convertSlackMessageToRocketChat message.attachments[0].text - "author_name" : message.attachments[0].author_subname - "author_icon" : getAvatarUrlFromUsername(message.attachments[0].author_subname) - ] - _.extend msgObj, msgDataDefaults - RocketChat.models.Messages.createWithTypeRoomIdMessageAndUser 'message_pinned', room._id, '', @getRocketUser(message.user), msgObj - else - #TODO: make this better - @logger.debug('Pinned item with no attachment, needs work.'); - #RocketChat.models.Messages.createWithTypeRoomIdMessageAndUser 'message_pinned', room._id, '', @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'file_share' - if message.file?.url_private_download isnt undefined - details = - message_id: "slack-#{message.ts.replace(/\./g, '-')}" - name: message.file.name - size: message.file.size - type: message.file.mimetype - rid: room._id - @uploadFile details, message.file.url_private_download, @getRocketUser(message.user), room, new Date(parseInt(message.ts.split('.')[0]) * 1000) - else - if not missedTypes[message.subtype] and not ignoreTypes[message.subtype] - missedTypes[message.subtype] = message - else - user = @getRocketUser(message.user) - if user? - msgObj = - msg: @convertSlackMessageToRocketChat message.text - rid: room._id - u: - _id: user._id - username: user.username - - _.extend msgObj, msgDataDefaults - - if message.edited? - msgObj.editedAt = new Date(parseInt(message.edited.ts.split('.')[0]) * 1000) - editedBy = @getRocketUser(message.edited.user) - if editedBy? - msgObj.editedBy = - _id: editedBy._id - username: editedBy.username - - RocketChat.sendMessage @getRocketUser(message.user), msgObj, room, true - - # Process the reactions - if RocketChat.models.Messages.findOneById(msgDataDefaults._id)? and message.reactions?.length > 0 - for reaction in message.reactions - for u in reaction.users - rcUser = @getRocketUser(u) - if rcUser? - Meteor.runAsUser rcUser._id, () => - Meteor.call 'setReaction', ":#{reaction.name}:", msgDataDefaults._id - - @addCountCompleted 1 - - if not _.isEmpty missedTypes - console.log 'Missed import types:', missedTypes - - @updateProgress Importer.ProgressStep.FINISHING - for channel in @channels.channels when channel.do_import and channel.is_archived - do (channel) => - Meteor.runAsUser startedByUserId, () => - Meteor.call 'archiveRoom', channel.rocketId - - @updateProgress Importer.ProgressStep.DONE - timeTook = Date.now() - start - @logger.log "Import took #{timeTook} milliseconds." - - return @getProgress() - - getSlackChannelFromName: (channelName) => - for channel in @channels.channels when channel.name is channelName - return channel - - getRocketUser: (slackId) => - for user in @users.users when user.id is slackId - return RocketChat.models.Users.findOneById user.rocketId, { fields: { username: 1, name: 1 }} - - convertSlackMessageToRocketChat: (message) => - if message? - message = message.replace /<!everyone>/g, '@all' - message = message.replace /<!channel>/g, '@all' - message = message.replace /<!here>/g, '@here' - message = message.replace />/g, '>' - message = message.replace /</g, '<' - message = message.replace /&/g, '&' - message = message.replace /:simple_smile:/g, ':smile:' - message = message.replace /:memo:/g, ':pencil:' - message = message.replace /:piggy:/g, ':pig:' - message = message.replace /:uk:/g, ':gb:' - message = message.replace /<(http[s]?:[^>]*)>/g, '$1' - for userReplace in @userTags - message = message.replace userReplace.slack, userReplace.rocket - message = message.replace userReplace.slackLong, userReplace.rocket - else - message = '' - return message - - getSelection: () => - selectionUsers = @users.users.map (user) -> - return new Importer.SelectionUser user.id, user.name, user.profile.email, user.deleted, user.is_bot, !user.is_bot - selectionChannels = @channels.channels.map (channel) -> - return new Importer.SelectionChannel channel.id, channel.name, channel.is_archived, true, false - - return new Importer.Selection @name, selectionUsers, selectionChannels diff --git a/packages/rocketchat-importer-slack/server.js b/packages/rocketchat-importer-slack/server.js new file mode 100644 index 0000000000000000000000000000000000000000..ad52655f58a0c6d7b640d5b5bebf0c9a9eaa6dce --- /dev/null +++ b/packages/rocketchat-importer-slack/server.js @@ -0,0 +1,434 @@ +/* globals Importer */ +Importer.Slack = class extends Importer.Base { + constructor(name, descriptionI18N, mimeType) { + super(name, descriptionI18N, mimeType); + this.userTags = []; + this.bots = {}; + this.logger.debug('Constructed a new Slack Importer.'); + } + prepare(dataURI, sentContentType, fileName) { + super.prepare(dataURI, sentContentType, fileName); + const {image/*, contentType*/} = RocketChatFile.dataURIParse(dataURI); + const zip = new this.AdmZip(new Buffer(image, 'base64')); + const zipEntries = zip.getEntries(); + let tempChannels = []; + let tempUsers = []; + const tempMessages = {}; + zipEntries.forEach(entry => { + if (entry.entryName.indexOf('__MACOSX') > -1) { + return this.logger.debug(`Ignoring the file: ${ entry.entryName }`); + } + if (entry.entryName === 'channels.json') { + this.updateProgress(Importer.ProgressStep.PREPARING_CHANNELS); + tempChannels = JSON.parse(entry.getData().toString()).filter(channel => channel.creator != null); + return; + } + if (entry.entryName === 'users.json') { + this.updateProgress(Importer.ProgressStep.PREPARING_USERS); + tempUsers = JSON.parse(entry.getData().toString()); + return tempUsers.forEach(user => { + if (user.is_bot) { + this.bots[user.profile.bot_id] = user; + } + }); + } + if (!entry.isDirectory && entry.entryName.indexOf('/') > -1) { + const item = entry.entryName.split('/'); + const channelName = item[0]; + const msgGroupData = item[1].split('.')[0]; + tempMessages[channelName] = tempMessages[channelName] || {}; + try { + tempMessages[channelName][msgGroupData] = JSON.parse(entry.getData().toString()); + } catch (error) { + this.logger.warn(`${ entry.entryName } is not a valid JSON file! Unable to import it.`); + } + } + }); + + // Insert the users record, eventually this might have to be split into several ones as well + // if someone tries to import a several thousands users instance + const usersId = this.collection.insert({ 'import': this.importRecord._id, 'importer': this.name, 'type': 'users', 'users': tempUsers }); + this.users = this.collection.findOne(usersId); + this.updateRecord({ 'count.users': tempUsers.length }); + this.addCountToTotal(tempUsers.length); + + // Insert the channels records. + const channelsId = this.collection.insert({ 'import': this.importRecord._id, 'importer': this.name, 'type': 'channels', 'channels': tempChannels }); + this.channels = this.collection.findOne(channelsId); + this.updateRecord({ 'count.channels': tempChannels.length }); + this.addCountToTotal(tempChannels.length); + + // Insert the messages records + this.updateProgress(Importer.ProgressStep.PREPARING_MESSAGES); + + let messagesCount = 0; + Object.keys(tempMessages).forEach(channel => { + const messagesObj = tempMessages[channel]; + this.messages[channel] = this.messages[channel] || {}; + Object.keys(messagesObj).forEach(date => { + const msgs = messagesObj[date]; + messagesCount += msgs.length; + this.updateRecord({ 'messagesstatus': '#{channel}/#{date}' }); + if (Importer.Base.getBSONSize(msgs) > Importer.Base.MaxBSONSize) { + const tmp = Importer.Base.getBSONSafeArraysFromAnArray(msgs); + Object.keys(tmp).forEach(i => { + const splitMsg = tmp[i]; + const messagesId = this.collection.insert({ 'import': this.importRecord._id, 'importer': this.name, 'type': 'messages', 'name': `${ channel }/${ date }.${ i }`, 'messages': splitMsg }); + this.messages[channel][`${ date }.${ i }`] = this.collection.findOne(messagesId); + }); + } else { + const messagesId = this.collection.insert({ 'import': this.importRecord._id, 'importer': this.name, 'type': 'messages', 'name': `${ channel }/${ date }`, 'messages': msgs }); + this.messages[channel][date] = this.collection.findOne(messagesId); + } + }); + }); + this.updateRecord({ 'count.messages': messagesCount, 'messagesstatus': null }); + this.addCountToTotal(messagesCount); + if ([tempUsers.length, tempChannels.length, messagesCount].some(e => e === 0)) { + this.logger.warn(`The loaded users count ${ tempUsers.length }, the loaded channels ${ tempChannels.length }, and the loaded messages ${ messagesCount }`); + console.log(`The loaded users count ${ tempUsers.length }, the loaded channels ${ tempChannels.length }, and the loaded messages ${ messagesCount }`); + this.updateProgress(Importer.ProgressStep.ERROR); + return this.getProgress(); + } + const selectionUsers = tempUsers.map(user => new Importer.SelectionUser(user.id, user.name, user.profile.email, user.deleted, user.is_bot, !user.is_bot)); + const selectionChannels = tempChannels.map(channel => new Importer.SelectionChannel(channel.id, channel.name, channel.is_archived, true, false)); + this.updateProgress(Importer.ProgressStep.USER_SELECTION); + return new Importer.Selection(this.name, selectionUsers, selectionChannels); + } + startImport(importSelection) { + super.startImport(importSelection); + const start = Date.now(); + Object.keys(importSelection.users).forEach(key => { + const user = importSelection.users[key]; + Object.keys(this.users.users).forEach(k => { + const u = this.users.users[k]; + if (u.id === user.user_id) { + u.do_import = user.do_import; + } + }); + }); + this.collection.update({ _id: this.users._id }, { $set: { 'users': this.users.users }}); + Object.keys(importSelection.channels).forEach(key => { + const channel = importSelection.channels[key]; + Object.keys(this.channels.channels).forEach(k => { + const c = this.channels.channels[k]; + if (c.id === channel.channel_id) { + c.do_import = channel.do_import; + } + }); + }); + this.collection.update({ _id: this.channels._id }, { $set: { 'channels': this.channels.channels }}); + const startedByUserId = Meteor.userId(); + Meteor.defer(() => { + this.updateProgress(Importer.ProgressStep.IMPORTING_USERS); + this.users.users.forEach(user => { + if (!user.do_import) { + return; + } + Meteor.runAsUser(startedByUserId, () => { + const existantUser = RocketChat.models.Users.findOneByEmailAddress(user.profile.email) || RocketChat.models.Users.findOneByUsername(user.name); + if (existantUser) { + user.rocketId = existantUser._id; + RocketChat.models.Users.update({ _id: user.rocketId }, { $addToSet: { importIds: user.id } }); + this.userTags.push({ + slack: `<@${ user.id }>`, + slackLong: `<@${ user.id }|${ user.name }>`, + rocket: `@${ existantUser.username }` + }); + } else { + const userId = user.profile.email ? Accounts.createUser({ email: user.profile.email, password: Date.now() + user.name + user.profile.email.toUpperCase() }) : Accounts.createUser({ username: user.name, password: Date.now() + user.name, joinDefaultChannelsSilenced: true }); + Meteor.runAsUser(userId, () => { + Meteor.call('setUsername', user.name, {joinDefaultChannelsSilenced: true}); + const url = user.profile.image_original || user.profile.image_512; + try { + Meteor.call('setAvatarFromService', url, undefined, 'url'); + } catch (error) { + this.logger.warn(`Failed to set ${ user.name }'s avatar from url ${ url }`); + console.log(`Failed to set ${ user.name }'s avatar from url ${ url }`); + } + // Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600 + if (user.tz_offset) { + Meteor.call('userSetUtcOffset', user.tz_offset / 3600); + } + }); + + RocketChat.models.Users.update({ _id: userId }, { $addToSet: { importIds: user.id } }); + + if (user.profile.real_name) { + RocketChat.models.Users.setName(userId, user.profile.real_name); + } + //Deleted users are 'inactive' users in Rocket.Chat + if (user.deleted) { + Meteor.call('setUserActiveStatus', userId, false); + } + //TODO: Maybe send emails? + user.rocketId = userId; + this.userTags.push({ + slack: `<@${ user.id }>`, + slackLong: `<@${ user.id }|${ user.name }>`, + rocket: `@${ user.name }` + }); + + } + this.addCountCompleted(1); + }); + }); + this.collection.update({ _id: this.users._id }, { $set: { 'users': this.users.users }}); + this.updateProgress(Importer.ProgressStep.IMPORTING_CHANNELS); + this.channels.channels.forEach(channel => { + if (!channel.do_import) { + return; + } + Meteor.runAsUser (startedByUserId, () => { + const existantRoom = RocketChat.models.Rooms.findOneByName(channel.name); + if (existantRoom || channel.is_general) { + if (channel.is_general && existantRoom && channel.name !== existantRoom.name) { + Meteor.call('saveRoomSettings', 'GENERAL', 'roomName', channel.name); + } + channel.rocketId = channel.is_general ? 'GENERAL' : existantRoom._id; + RocketChat.models.Rooms.update({ _id: channel.rocketId }, { $addToSet: { importIds: channel.id } }); + } else { + const users = channel.members + .reduce((ret, member) => { + if (member !== channel.creator) { + const user = this.getRocketUser(member); + if (user && user.username) { + ret.push(user.username); + } + } + return ret; + }, []); + let userId = startedByUserId; + this.users.users.forEach(user => { + if (user.id === channel.creator && user.do_import) { + userId = user.rocketId; + } + }); + Meteor.runAsUser(userId, () => { + const returned = Meteor.call('createChannel', channel.name, users); + channel.rocketId = returned.rid; + }); + + // @TODO implement model specific function + const roomUpdate = { + ts: new Date(channel.created * 1000) + }; + if (!_.isEmpty(channel.topic && channel.topic.value)) { + roomUpdate.topic = channel.topic.value; + } + if (!_.isEmpty(channel.purpose && channel.purpose.value)) { + roomUpdate.description = channel.purpose.value; + } + RocketChat.models.Rooms.update({ _id: channel.rocketId }, { $set: roomUpdate, $addToSet: { importIds: channel.id } }); + } + this.addCountCompleted(1); + }); + }); + this.collection.update({ _id: this.channels._id }, { $set: { 'channels': this.channels.channels }}); + const missedTypes = {}; + const ignoreTypes = { 'bot_add': true, 'file_comment': true, 'file_mention': true }; + this.updateProgress(Importer.ProgressStep.IMPORTING_MESSAGES); + Object.keys(this.messages).forEach(channel => { + const messagesObj = this.messages[channel]; + + Meteor.runAsUser(startedByUserId, () =>{ + const slackChannel = this.getSlackChannelFromName(channel); + if (!slackChannel || !slackChannel.do_import) { return; } + const room = RocketChat.models.Rooms.findOneById(slackChannel.rocketId, { fields: { usernames: 1, t: 1, name: 1 } }); + Object.keys(messagesObj).forEach(date => { + const msgs = messagesObj[date]; + msgs.messages.forEach(message => { + this.updateRecord({ 'messagesstatus': '#{channel}/#{date}.#{msgs.messages.length}' }); + const msgDataDefaults ={ + _id: `slack-${ slackChannel.id }-${ message.ts.replace(/\./g, '-') }`, + ts: new Date(parseInt(message.ts.split('.')[0]) * 1000) + }; + if (message.type === 'message') { + if (message.subtype) { + if (message.subtype === 'channel_join') { + if (this.getRocketUser(message.user)) { + RocketChat.models.Messages.createUserJoinWithRoomIdAndUser(room._id, this.getRocketUser(message.user), msgDataDefaults); + } + } else if (message.subtype === 'channel_leave') { + if (this.getRocketUser(message.user)) { RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser(room._id, this.getRocketUser(message.user), msgDataDefaults); } + } else if (message.subtype === 'me_message') { + const msgObj = { + ...msgDataDefaults, + msg: `_${ this.convertSlackMessageToRocketChat(message.text) }_` + }; + RocketChat.sendMessage(this.getRocketUser(message.user), msgObj, room, true); + } else if (message.subtype === 'bot_message' || message.subtype === 'slackbot_response') { + const botUser = RocketChat.models.Users.findOneById('rocket.cat', { fields: { username: 1 }}); + const botUsername = this.bots[message.bot_id] ? this.bots[message.bot_id].name : message.username; + const msgObj = { + ...msgDataDefaults, + msg: this.convertSlackMessageToRocketChat(message.text), + rid: room._id, + bot: true, + attachments: message.attachments, + username: botUsername || undefined + }; + + if (message.edited) { + msgObj.editedAt = new Date(parseInt(message.edited.ts.split('.')[0]) * 1000); + const editedBy = this.getRocketUser(message.edited.user); + if (editedBy) { + msgObj.editedBy = { + _id: editedBy._id, + username: editedBy.username + }; + } + } + + if (message.icons) { + msgObj.emoji = message.icons.emoji; + } + RocketChat.sendMessage(botUser, msgObj, room, true); + } else if (message.subtype === 'channel_purpose') { + if (this.getRocketUser(message.user)) { + RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_description', room._id, message.purpose, this.getRocketUser(message.user), msgDataDefaults); + } + } else if (message.subtype === 'channel_topic') { + if (this.getRocketUser(message.user)) { + RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', room._id, message.topic, this.getRocketUser(message.user), msgDataDefaults); + } + } else if (message.subtype === 'channel_name') { + if (this.getRocketUser(message.user)) { + RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser(room._id, message.name, this.getRocketUser(message.user), msgDataDefaults); + } + } else if (message.subtype === 'pinned_item') { + if (message.attachments) { + const msgObj = { + ...msgDataDefaults, + attachments: [{ + 'text': this.convertSlackMessageToRocketChat(message.attachments[0].text), + 'author_name' : message.attachments[0].author_subname, + 'author_icon' : getAvatarUrlFromUsername(message.attachments[0].author_subname) + }] + }; + RocketChat.models.Messages.createWithTypeRoomIdMessageAndUser('message_pinned', room._id, '', this.getRocketUser(message.user), msgObj); + } else { + //TODO: make this better + this.logger.debug('Pinned item with no attachment, needs work.'); + //RocketChat.models.Messages.createWithTypeRoomIdMessageAndUser 'message_pinned', room._id, '', @getRocketUser(message.user), msgDataDefaults + } + } else if (message.subtype === 'file_share') { + if (message.file && message.file.url_private_download !== undefined) { + const details = { + message_id: `slack-${ message.ts.replace(/\./g, '-') }`, + name: message.file.name, + size: message.file.size, + type: message.file.mimetype, + rid: room._id + }; + this.uploadFile(details, message.file.url_private_download, this.getRocketUser(message.user), room, new Date(parseInt(message.ts.split('.')[0]) * 1000)); + } + } else if (!missedTypes[message.subtype] && !ignoreTypes[message.subtype]) { + missedTypes[message.subtype] = message; + } + } else { + const user = this.getRocketUser(message.user); + if (user) { + const msgObj = { + ...msgDataDefaults, + msg: this.convertSlackMessageToRocketChat(message.text), + rid: room._id, + u: { + _id: user._id, + username: user.username + } + }; + + if (message.edited) { + msgObj.editedAt = new Date(parseInt(message.edited.ts.split('.')[0]) * 1000); + const editedBy = this.getRocketUser(message.edited.user); + if (editedBy) { + msgObj.editedBy = { + _id: editedBy._id, + username: editedBy.username + }; + } + } + + RocketChat.sendMessage(this.getRocketUser(message.user), msgObj, room, true); + } + } + } + + + // Process the reactions + if (RocketChat.models.Messages.findOneById(msgDataDefaults._id) && message.reactions && message.reactions.length > 0) { + message.reactions.forEach(reaction => { + reaction.users.forEach(u => { + const rcUser = this.getRocketUser(u); + if (!rcUser) { return; } + Meteor.runAsUser(rcUser._id, () => Meteor.call('setReaction', `:${ reaction.name }:`, msgDataDefaults._id)); + }); + }); + } + this.addCountCompleted(1); + }); + }); + }); + }); + + + if (!_.isEmpty(missedTypes)) { + console.log('Missed import types:', missedTypes); + } + + this.updateProgress(Importer.ProgressStep.FINISHING); + + this.channels.channels.forEach(channel => { + if (channel.do_import && channel.is_archived) { + Meteor.runAsUser(startedByUserId, function() { + Meteor.call('archiveRoom', channel.rocketId); + }); + } + }); + this.updateProgress(Importer.ProgressStep.DONE); + + const timeTook = Date.now() - start; + + this.logger.log(`Import took ${ timeTook } milliseconds.`); + + }); + return this.getProgress(); + } + getSlackChannelFromName(channelName) { + return this.channels.channels.find(channel => channel.name === channelName); + } + getRocketUser(slackId) { + const user = this.users.users.find(user => user.id === slackId); + if (user) { + return RocketChat.models.Users.findOneById(user.rocketId, { fields: { username: 1, name: 1 }}); + } + } + convertSlackMessageToRocketChat(message) { + if (message != null) { + message = message.replace(/<!everyone>/g, '@all'); + message = message.replace(/<!channel>/g, '@all'); + message = message.replace(/<!here>/g, '@here'); + message = message.replace(/>/g, '>'); + message = message.replace(/</g, '<'); + message = message.replace(/&/g, '&'); + message = message.replace(/:simple_smile:/g, ':smile:'); + message = message.replace(/:memo:/g, ':pencil:'); + message = message.replace(/:piggy:/g, ':pig:'); + message = message.replace(/:uk:/g, ':gb:'); + message = message.replace(/<(http[s]?:[^>]*)>/g, '$1'); + for (const userReplace of Array.from(this.userTags)) { + message = message.replace(userReplace.slack, userReplace.rocket); + message = message.replace(userReplace.slackLong, userReplace.rocket); + } + } else { + message = ''; + } + return message; + } + getSelection() { + const selectionUsers = this.users.users.map(user => new Importer.SelectionUser(user.id, user.name, user.profile.email, user.deleted, user.is_bot, !user.is_bot)); + const selectionChannels = this.channels.channels.map(channel => new Importer.SelectionChannel(channel.id, channel.name, channel.is_archived, true, false)); + return new Importer.Selection(this.name, selectionUsers, selectionChannels); + } +}; diff --git a/packages/rocketchat-integrations/client/views/integrationsIncoming.html b/packages/rocketchat-integrations/client/views/integrationsIncoming.html index 68955e653b5d4bb836e8350e1762d611f79de1eb..1bc5f6e0025ac930c9b5a17adca454eb39fe05d2 100644 --- a/packages/rocketchat-integrations/client/views/integrationsIncoming.html +++ b/packages/rocketchat-integrations/client/views/integrationsIncoming.html @@ -30,11 +30,7 @@ <div class="input-line double-col"> <label>{{_ "Post_as"}}</label> <div> - {{#if data.token}} - <input type="text" name="username" value="{{data.username}}" readonly="readonly" /> - {{else}} - <input type="text" name="username" value="{{data.username}}" /> - {{/if}} + <input type="text" name="username" value="{{data.username}}" /> <div class="settings-description secondary-font-color">{{_ "Choose_the_username_that_this_integration_will_post_as"}}</div> <div class="settings-description secondary-font-color">{{_ "Should_exists_a_user_with_this_username"}}</div> </div> diff --git a/packages/rocketchat-integrations/client/views/integrationsIncoming.js b/packages/rocketchat-integrations/client/views/integrationsIncoming.js index f255ea62ac2c2a4df0b0845a774b8a93252c5fe1..38e4ebfcc7a3a180e6606c12f175a797206ab90b 100644 --- a/packages/rocketchat-integrations/client/views/integrationsIncoming.js +++ b/packages/rocketchat-integrations/client/views/integrationsIncoming.js @@ -222,6 +222,7 @@ Template.integrationsIncoming.events({ const integration = { enabled: enabled === '1', channel, + username, alias: alias !== '' ? alias : undefined, emoji: emoji !== '' ? emoji : undefined, avatar: avatar !== '' ? avatar : undefined, @@ -240,8 +241,6 @@ Template.integrationsIncoming.events({ toastr.success(TAPi18n.__('Integration_updated')); }); } else { - integration.username = username; - Meteor.call('addIncomingIntegration', integration, (err, data) => { if (err) { return handleError(err); diff --git a/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.js b/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.js index 9a4c4a878e4a56bb169f32b92accb08686f75535..a888484d05c29e38f0e3319ce9688f47e58bfc25 100644 --- a/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.js +++ b/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.js @@ -76,6 +76,11 @@ Meteor.methods({ } const user = RocketChat.models.Users.findOne({ username: currentIntegration.username }); + + if (!user || !user._id) { + throw new Meteor.Error('error-invalid-post-as-user', 'Invalid Post As User', { method: 'updateIncomingIntegration' }); + } + RocketChat.models.Roles.addUserRoles(user._id, 'bot'); RocketChat.models.Integrations.update(integrationId, { diff --git a/packages/rocketchat-ldap/server/sync.js b/packages/rocketchat-ldap/server/sync.js index df3ec4bf828bd9b084fafb3d6b1b90224370880f..06763c0a97e6526263fb7e24728b3618bb9af608 100644 --- a/packages/rocketchat-ldap/server/sync.js +++ b/packages/rocketchat-ldap/server/sync.js @@ -65,15 +65,15 @@ getDataToSyncUserData = function getDataToSyncUserData(ldapUser, user) { if (syncUserData && syncUserDataFieldMap) { const fieldMap = JSON.parse(syncUserDataFieldMap); const userData = {}; - const emailList = []; _.map(fieldMap, function(userField, ldapField) { - if (!ldapUser.object.hasOwnProperty(ldapField)) { - return; - } - switch (userField) { case 'email': + if (!ldapUser.object.hasOwnProperty(ldapField)) { + logger.debug(`user does not have attribute: ${ ldapField }`); + return; + } + if (_.isObject(ldapUser.object[ldapField])) { _.map(ldapUser.object[ldapField], function(item) { emailList.push({ address: item, verified: true }); @@ -84,8 +84,37 @@ getDataToSyncUserData = function getDataToSyncUserData(ldapUser, user) { break; case 'name': - if (user.name !== ldapUser.object[ldapField]) { - userData.name = ldapUser.object[ldapField]; + const templateRegex = /#{(\w+)}/gi; + let match = templateRegex.exec(ldapField); + let tmpLdapField = ldapField; + + if (match == null) { + if (!ldapUser.object.hasOwnProperty(ldapField)) { + logger.debug(`user does not have attribute: ${ ldapField }`); + return; + } + tmpLdapField = ldapUser.object[ldapField]; + } else { + logger.debug('template found. replacing values'); + while (match != null) { + const tmplVar = match[0]; + const tmplAttrName = match[1]; + + if (!ldapUser.object.hasOwnProperty(tmplAttrName)) { + logger.debug(`user does not have attribute: ${ tmplAttrName }`); + return; + } + + const attrVal = ldapUser.object[tmplAttrName]; + logger.debug(`replacing template var: ${ tmplVar } with value from ldap: ${ attrVal }`); + tmpLdapField = tmpLdapField.replace(tmplVar, attrVal); + match = templateRegex.exec(ldapField); + } + } + + if (user.name !== tmpLdapField) { + userData.name = tmpLdapField; + logger.debug(`user.name changed to: ${ tmpLdapField }`); } break; } diff --git a/packages/rocketchat-lib/client/MessageAction.js b/packages/rocketchat-lib/client/MessageAction.js index db2ee342a4941ddc2b4b482876e1ba760aab2049..51ad67e3fa5783e18d1109aa7cad96c3ba8d9aef 100644 --- a/packages/rocketchat-lib/client/MessageAction.js +++ b/packages/rocketchat-lib/client/MessageAction.js @@ -180,14 +180,15 @@ Meteor.startup(function() { if (RocketChat.models.Subscriptions.findOne({rid: message.rid}) == null) { return false; } + const forceDelete = RocketChat.authz.hasAtLeastOnePermission('force-delete-message', message.rid); const hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', message.rid); const isDeleteAllowed = RocketChat.settings.get('Message_AllowDeleting'); const deleteOwn = message.u && message.u._id === Meteor.userId(); - if (!(hasPermission || (isDeleteAllowed && deleteOwn))) { + if (!(hasPermission || (isDeleteAllowed && deleteOwn) || forceDelete)) { return; } const blockDeleteInMinutes = RocketChat.settings.get('Message_AllowDeleting_BlockDeleteInMinutes'); - if ((blockDeleteInMinutes != null) && blockDeleteInMinutes !== 0) { + if ((blockDeleteInMinutes != null) && blockDeleteInMinutes !== 0 && !(forceDelete)) { let msgTs; if (message.ts != null) { msgTs = moment(message.ts); diff --git a/packages/rocketchat-lib/client/lib/openRoom.coffee b/packages/rocketchat-lib/client/lib/openRoom.coffee deleted file mode 100644 index 3130cd82a3e0f41ead8b19fd979e576125d94623..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/client/lib/openRoom.coffee +++ /dev/null @@ -1,80 +0,0 @@ -currentTracker = undefined - -@openRoom = (type, name) -> - Session.set 'openedRoom', null - - Meteor.defer -> - currentTracker = Tracker.autorun (c) -> - user = Meteor.user() - if (user? and not user.username?) or (not user? and RocketChat.settings.get('Accounts_AllowAnonymousRead') is false) - BlazeLayout.render 'main' - return - - if RoomManager.open(type + name).ready() isnt true - BlazeLayout.render 'main', { modal: RocketChat.Layout.isEmbedded(), center: 'loading' } - return - - currentTracker = undefined - c.stop() - - room = RocketChat.roomTypes.findRoom(type, name, user) - if not room? - if type is 'd' - Meteor.call 'createDirectMessage', name, (err) -> - if !err - RoomManager.close(type + name) - openRoom('d', name) - else - Session.set 'roomNotFound', {type: type, name: name} - BlazeLayout.render 'main', {center: 'roomNotFound'} - return - else - Meteor.call 'getRoomByTypeAndName', type, name, (err, record) -> - if err? - Session.set 'roomNotFound', {type: type, name: name} - BlazeLayout.render 'main', {center: 'roomNotFound'} - else - delete record.$loki - RocketChat.models.Rooms.upsert({ _id: record._id }, _.omit(record, '_id')) - RoomManager.close(type + name) - openRoom(type, name) - - return - - mainNode = document.querySelector('.main-content') - if mainNode? - for child in mainNode.children - mainNode.removeChild child if child? - roomDom = RoomManager.getDomOfRoom(type + name, room._id) - mainNode.appendChild roomDom - if roomDom.classList.contains('room-container') - roomDom.querySelector('.messages-box > .wrapper').scrollTop = roomDom.oldScrollTop - - Session.set 'openedRoom', room._id - - fireGlobalEvent 'room-opened', _.omit room, 'usernames' - - Session.set 'editRoomTitle', false - RoomManager.updateMentionsMarksOfRoom type + name - Meteor.setTimeout -> - readMessage.readNow() - , 2000 - # KonchatNotification.removeRoomNotification(params._id) - - if Meteor.Device.isDesktop() and window.chatMessages?[room._id]? - setTimeout -> - $('.message-form .input-message').focus() - , 100 - - # update user's room subscription - sub = ChatSubscription.findOne({rid: room._id}) - if sub?.open is false - Meteor.call 'openRoom', room._id, (err) -> - if err - return handleError(err) - - if FlowRouter.getQueryParam('msg') - msg = { _id: FlowRouter.getQueryParam('msg'), rid: room._id } - RoomHistoryManager.getSurroundingMessages(msg); - - RocketChat.callbacks.run 'enter-room', sub diff --git a/packages/rocketchat-lib/client/lib/roomTypes.js b/packages/rocketchat-lib/client/lib/roomTypes.js index 9cfc41585961c5713d53ed8b22ba0e8d99c184e9..fdc926c063da070bddc26e0ed9bd95dbcfb640b8 100644 --- a/packages/rocketchat-lib/client/lib/roomTypes.js +++ b/packages/rocketchat-lib/client/lib/roomTypes.js @@ -22,7 +22,7 @@ RocketChat.roomTypes = new class extends roomTypesCommon { return _.map(list, (t) => t.identifier); } getUserStatus(roomType, roomId) { - this.roomTypes[roomType] && typeof this.roomTypes[roomType].getUserStatus === 'function' && this.roomTypes[roomType].getUserStatus(roomId); + return this.roomTypes[roomType] && typeof this.roomTypes[roomType].getUserStatus === 'function' && this.roomTypes[roomType].getUserStatus(roomId); } findRoom(roomType, identifier, user) { return this.roomTypes[roomType] && this.roomTypes[roomType].findRoom(identifier, user); diff --git a/packages/rocketchat-lib/lib/settings.js b/packages/rocketchat-lib/lib/settings.js index 9c14943dac63ee4a0d30ae4b36efb2f98dd2d49d..ecdd2c87c4b3e663c5d6129c4f09352c38c2f161 100644 --- a/packages/rocketchat-lib/lib/settings.js +++ b/packages/rocketchat-lib/lib/settings.js @@ -63,9 +63,9 @@ RocketChat.settings = { return _(actions).reduceRight(_.wrap, (err, success) => callback(err, success))(); }, load(key, value, initialLoad) { - ['*', key].forEach(key => { - if (RocketChat.settings.callbacks[key]) { - RocketChat.settings.callbacks[key].forEach(callback => callback(key, value, initialLoad)); + ['*', key].forEach(item => { + if (RocketChat.settings.callbacks[item]) { + RocketChat.settings.callbacks[item].forEach(callback => callback(key, value, initialLoad)); } }); Object.keys(RocketChat.settings.regexCallbacks).forEach(cbKey => { diff --git a/packages/rocketchat-lib/package.js b/packages/rocketchat-lib/package.js index 3d7f8402927a4b2bbaa46da191ba8a3d2d5bcb2f..a6360892d67a0046cd19b33a87e7eb02de1ac6a1 100644 --- a/packages/rocketchat-lib/package.js +++ b/packages/rocketchat-lib/package.js @@ -21,7 +21,6 @@ Package.onUse(function(api) { api.use('reactive-var'); api.use('reactive-dict'); api.use('accounts-base'); - api.use('coffeescript'); api.use('ecmascript'); api.use('random'); api.use('check'); @@ -72,7 +71,7 @@ Package.onUse(function(api) { api.addFiles('server/functions/addUserToDefaultChannels.js', 'server'); api.addFiles('server/functions/addUserToRoom.js', 'server'); api.addFiles('server/functions/archiveRoom.js', 'server'); - api.addFiles('server/functions/checkUsernameAvailability.coffee', 'server'); + api.addFiles('server/functions/checkUsernameAvailability.js', 'server'); api.addFiles('server/functions/checkEmailAvailability.js', 'server'); api.addFiles('server/functions/createRoom.js', 'server'); api.addFiles('server/functions/deleteMessage.js', 'server'); @@ -82,15 +81,15 @@ Package.onUse(function(api) { api.addFiles('server/functions/removeUserFromRoom.js', 'server'); api.addFiles('server/functions/saveUser.js', 'server'); api.addFiles('server/functions/saveCustomFields.js', 'server'); - api.addFiles('server/functions/sendMessage.coffee', 'server'); - api.addFiles('server/functions/settings.coffee', 'server'); + api.addFiles('server/functions/sendMessage.js', 'server'); + api.addFiles('server/functions/settings.js', 'server'); api.addFiles('server/functions/setUserAvatar.js', 'server'); - api.addFiles('server/functions/setUsername.coffee', 'server'); + api.addFiles('server/functions/setUsername.js', 'server'); api.addFiles('server/functions/setRealName.js', 'server'); api.addFiles('server/functions/setEmail.js', 'server'); api.addFiles('server/functions/unarchiveRoom.js', 'server'); api.addFiles('server/functions/updateMessage.js', 'server'); - api.addFiles('server/functions/Notifications.coffee', 'server'); + api.addFiles('server/functions/Notifications.js', 'server'); // SERVER LIB api.addFiles('server/lib/configLogger.js', 'server'); @@ -104,13 +103,13 @@ Package.onUse(function(api) { // SERVER MODELS api.addFiles('server/models/_Base.js', 'server'); - api.addFiles('server/models/Messages.coffee', 'server'); + api.addFiles('server/models/Messages.js', 'server'); api.addFiles('server/models/Reports.js', 'server'); - api.addFiles('server/models/Rooms.coffee', 'server'); - api.addFiles('server/models/Settings.coffee', 'server'); - api.addFiles('server/models/Subscriptions.coffee', 'server'); - api.addFiles('server/models/Uploads.coffee', 'server'); - api.addFiles('server/models/Users.coffee', 'server'); + api.addFiles('server/models/Rooms.js', 'server'); + api.addFiles('server/models/Settings.js', 'server'); + api.addFiles('server/models/Subscriptions.js', 'server'); + api.addFiles('server/models/Uploads.js', 'server'); + api.addFiles('server/models/Users.js', 'server'); api.addFiles('server/oauth/oauth.js', 'server'); api.addFiles('server/oauth/google.js', 'server'); @@ -154,7 +153,7 @@ Package.onUse(function(api) { api.addFiles('server/methods/robotMethods.js', 'server'); api.addFiles('server/methods/saveSetting.js', 'server'); api.addFiles('server/methods/sendInvitationEmail.js', 'server'); - api.addFiles('server/methods/sendMessage.coffee', 'server'); + api.addFiles('server/methods/sendMessage.js', 'server'); api.addFiles('server/methods/sendSMTPTestEmail.js', 'server'); api.addFiles('server/methods/setAdminStatus.js', 'server'); api.addFiles('server/methods/setRealName.js', 'server'); diff --git a/packages/rocketchat-lib/rocketchat.info b/packages/rocketchat-lib/rocketchat.info index ce43333a4eb3d13b5c1194a345a71480e9311583..fbad42938bafe8b016313731862d336e9f2bad8d 100644 --- a/packages/rocketchat-lib/rocketchat.info +++ b/packages/rocketchat-lib/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "0.56.0-develop" + "version": "0.57.0-develop" } diff --git a/packages/rocketchat-lib/server/functions/Notifications.coffee b/packages/rocketchat-lib/server/functions/Notifications.coffee deleted file mode 100644 index 9e08663db6cd6ef6311c11554072217486bd1a67..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/functions/Notifications.coffee +++ /dev/null @@ -1,119 +0,0 @@ -RocketChat.Notifications = new class - constructor: -> - self = @ - - @debug = false - - @streamAll = new Meteor.Streamer 'notify-all' - @streamLogged = new Meteor.Streamer 'notify-logged' - @streamRoom = new Meteor.Streamer 'notify-room' - @streamRoomUsers = new Meteor.Streamer 'notify-room-users' - @streamUser = new Meteor.Streamer 'notify-user' - - - @streamAll.allowWrite('none') - @streamLogged.allowWrite('none') - @streamRoom.allowWrite('none') - @streamRoomUsers.allowWrite (eventName, args...) -> - [roomId, e] = eventName.split('/') - - user = Meteor.users.findOne @userId, {fields: {username: 1}} - if RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(roomId, @userId)? - subscriptions = RocketChat.models.Subscriptions.findByRoomIdAndNotUserId(roomId, @userId).fetch() - for subscription in subscriptions - RocketChat.Notifications.notifyUser(subscription.u._id, e, args...) - - return false - - @streamUser.allowWrite('logged') - - @streamAll.allowRead('all') - - @streamLogged.allowRead('logged') - - @streamRoom.allowRead (eventName) -> - if not @userId? then return false - - roomId = eventName.split('/')[0] - - user = Meteor.users.findOne @userId, {fields: {username: 1}} - room = RocketChat.models.Rooms.findOneById(roomId) - if room.t is 'l' and room.v._id is user._id - return true - - return room.usernames.indexOf(user.username) > -1 - - @streamRoomUsers.allowRead('none'); - - @streamUser.allowRead (eventName) -> - userId = eventName.split('/')[0] - return @userId? and @userId is userId - - - notifyAll: (eventName, args...) -> - console.log 'notifyAll', arguments if @debug is true - - args.unshift eventName - @streamAll.emit.apply @streamAll, args - - notifyLogged: (eventName, args...) -> - console.log 'notifyLogged', arguments if @debug is true - - args.unshift eventName - @streamLogged.emit.apply @streamLogged, args - - notifyRoom: (room, eventName, args...) -> - console.log 'notifyRoom', arguments if @debug is true - - args.unshift "#{room}/#{eventName}" - @streamRoom.emit.apply @streamRoom, args - - notifyUser: (userId, eventName, args...) -> - console.log 'notifyUser', arguments if @debug is true - - args.unshift "#{userId}/#{eventName}" - @streamUser.emit.apply @streamUser, args - - - notifyAllInThisInstance: (eventName, args...) -> - console.log 'notifyAll', arguments if @debug is true - - args.unshift eventName - @streamAll.emitWithoutBroadcast.apply @streamAll, args - - notifyLoggedInThisInstance: (eventName, args...) -> - console.log 'notifyLogged', arguments if @debug is true - - args.unshift eventName - @streamLogged.emitWithoutBroadcast.apply @streamLogged, args - - notifyRoomInThisInstance: (room, eventName, args...) -> - console.log 'notifyRoomAndBroadcast', arguments if @debug is true - - args.unshift "#{room}/#{eventName}" - @streamRoom.emitWithoutBroadcast.apply @streamRoom, args - - notifyUserInThisInstance: (userId, eventName, args...) -> - console.log 'notifyUserAndBroadcast', arguments if @debug is true - - args.unshift "#{userId}/#{eventName}" - @streamUser.emitWithoutBroadcast.apply @streamUser, args - - -## Permissions for client - -# Enable emit for event typing for rooms and add username to event data -func = (eventName, username) -> - [room, e] = eventName.split('/') - - if e is 'webrtc' - return true - - if e is 'typing' - user = Meteor.users.findOne(@userId, {fields: {username: 1}}) - if user?.username is username - return true - - return false - -RocketChat.Notifications.streamRoom.allowWrite func diff --git a/packages/rocketchat-lib/server/functions/Notifications.js b/packages/rocketchat-lib/server/functions/Notifications.js new file mode 100644 index 0000000000000000000000000000000000000000..89fec97a27998423a5ab0492c50b6bec0e6e3d98 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/Notifications.js @@ -0,0 +1,132 @@ +RocketChat.Notifications = new class { + constructor() { + this.debug = false; + this.streamAll = new Meteor.Streamer('notify-all'); + this.streamLogged = new Meteor.Streamer('notify-logged'); + this.streamRoom = new Meteor.Streamer('notify-room'); + this.streamRoomUsers = new Meteor.Streamer('notify-room-users'); + this.streamUser = new Meteor.Streamer('notify-user'); + this.streamAll.allowWrite('none'); + this.streamLogged.allowWrite('none'); + this.streamRoom.allowWrite('none'); + this.streamRoomUsers.allowWrite(function(eventName, ...args) { + const [roomId, e] = eventName.split('/'); + // const user = Meteor.users.findOne(this.userId, { + // fields: { + // username: 1 + // } + // }); + if (RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId) != null) { + const subscriptions = RocketChat.models.Subscriptions.findByRoomIdAndNotUserId(roomId, this.userId).fetch(); + subscriptions.forEach(subscription => RocketChat.Notifications.notifyUser(subscription.u._id, e, ...args)); + } + return false; + }); + this.streamUser.allowWrite('logged'); + this.streamAll.allowRead('all'); + this.streamLogged.allowRead('logged'); + this.streamRoom.allowRead(function(eventName) { + if (this.userId == null) { + return false; + } + const [roomId] = eventName.split('/'); + const user = Meteor.users.findOne(this.userId, { + fields: { + username: 1 + } + }); + const room = RocketChat.models.Rooms.findOneById(roomId); + if (room.t === 'l' && room.v._id === user._id) { + return true; + } + return room.usernames.indexOf(user.username) > -1; + }); + this.streamRoomUsers.allowRead('none'); + this.streamUser.allowRead(function(eventName) { + const [userId] = eventName.split('/'); + return (this.userId != null) && this.userId === userId; + }); + } + + notifyAll(eventName, ...args) { + if (this.debug === true) { + console.log('notifyAll', arguments); + } + args.unshift(eventName); + return this.streamAll.emit.apply(this.streamAll, args); + } + + notifyLogged(eventName, ...args) { + if (this.debug === true) { + console.log('notifyLogged', arguments); + } + args.unshift(eventName); + return this.streamLogged.emit.apply(this.streamLogged, args); + } + + notifyRoom(room, eventName, ...args) { + if (this.debug === true) { + console.log('notifyRoom', arguments); + } + args.unshift(`${ room }/${ eventName }`); + return this.streamRoom.emit.apply(this.streamRoom, args); + } + + notifyUser(userId, eventName, ...args) { + if (this.debug === true) { + console.log('notifyUser', arguments); + } + args.unshift(`${ userId }/${ eventName }`); + return this.streamUser.emit.apply(this.streamUser, args); + } + + notifyAllInThisInstance(eventName, ...args) { + if (this.debug === true) { + console.log('notifyAll', arguments); + } + args.unshift(eventName); + return this.streamAll.emitWithoutBroadcast.apply(this.streamAll, args); + } + + notifyLoggedInThisInstance(eventName, ...args) { + if (this.debug === true) { + console.log('notifyLogged', arguments); + } + args.unshift(eventName); + return this.streamLogged.emitWithoutBroadcast.apply(this.streamLogged, args); + } + + notifyRoomInThisInstance(room, eventName, ...args) { + if (this.debug === true) { + console.log('notifyRoomAndBroadcast', arguments); + } + args.unshift(`${ room }/${ eventName }`); + return this.streamRoom.emitWithoutBroadcast.apply(this.streamRoom, args); + } + + notifyUserInThisInstance(userId, eventName, ...args) { + if (this.debug === true) { + console.log('notifyUserAndBroadcast', arguments); + } + args.unshift(`${ userId }/${ eventName }`); + return this.streamUser.emitWithoutBroadcast.apply(this.streamUser, args); + } +}; + +RocketChat.Notifications.streamRoom.allowWrite(function(eventName, username) { + const [, e] = eventName.split('/'); + if (e === 'webrtc') { + return true; + } + if (e === 'typing') { + const user = Meteor.users.findOne(this.userId, { + fields: { + username: 1 + } + }); + if (user != null && user.username === username) { + return true; + } + } + return false; +}); diff --git a/packages/rocketchat-lib/server/functions/checkUsernameAvailability.coffee b/packages/rocketchat-lib/server/functions/checkUsernameAvailability.coffee deleted file mode 100644 index 987b8e2af2b2ffa06c191e2672b815831a58980e..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/functions/checkUsernameAvailability.coffee +++ /dev/null @@ -1,10 +0,0 @@ -RocketChat.checkUsernameAvailability = (username) -> - usernameBlackList = [] - RocketChat.settings.get('Accounts_BlockedUsernameList', (key, value) => - usernameBlackList = _.map(value.split(','), (username) => username.trim()) - if usernameBlackList.length isnt 0 - for restrictedUsername in usernameBlackList - regex = new RegExp('^' + s.escapeRegExp(restrictedUsername) + '$', 'i') - return false if regex.test(s.trim(s.escapeRegExp(username))) - return not Meteor.users.findOne({ username: { $regex : new RegExp("^" + s.trim(s.escapeRegExp(username)) + "$", "i") } }); - ) diff --git a/packages/rocketchat-lib/server/functions/checkUsernameAvailability.js b/packages/rocketchat-lib/server/functions/checkUsernameAvailability.js new file mode 100644 index 0000000000000000000000000000000000000000..01e8f8c1b1069264fa686c963ea5e59fb19e355f --- /dev/null +++ b/packages/rocketchat-lib/server/functions/checkUsernameAvailability.js @@ -0,0 +1,20 @@ +RocketChat.checkUsernameAvailability = function(username) { + return RocketChat.settings.get('Accounts_BlockedUsernameList', function(key, value) { + const usernameBlackList = _.map(value.split(','), function(username) { + return username.trim(); + }); + if (usernameBlackList.length !== 0) { + if (usernameBlackList.every(restrictedUsername => { + const regex = new RegExp(`^${ s.escapeRegExp(restrictedUsername) }$`, 'i'); + return !regex.test(s.trim(s.escapeRegExp(username))); + })) { + return !Meteor.users.findOne({ + username: { + $regex: new RegExp(`^${ s.trim(s.escapeRegExp(username)) }$`, 'i') + } + }); + } + return false; + } + }); +}; diff --git a/packages/rocketchat-lib/server/functions/sendMessage.coffee b/packages/rocketchat-lib/server/functions/sendMessage.coffee deleted file mode 100644 index bdce87faba4d40f69610db48e1f17fe269834568..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/functions/sendMessage.coffee +++ /dev/null @@ -1,50 +0,0 @@ -RocketChat.sendMessage = (user, message, room, upsert = false) -> - if not user or not message or not room._id - return false - - unless message.ts? - message.ts = new Date() - - message.u = _.pick user, ['_id','username'] - - if not Match.test(message.msg, String) - message.msg = '' - - message.rid = room._id - - if not room.usernames? || room.usernames.length is 0 - updated_room = RocketChat.models.Rooms.findOneById(room._id) - if updated_room? - room = updated_room - else - room.usernames = [] - - if message.parseUrls isnt false - if urls = message.msg.match /([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g - message.urls = urls.map (url) -> url: url - - message = RocketChat.callbacks.run 'beforeSaveMessage', message - - # Avoid saving sandstormSessionId to the database - sandstormSessionId = null - if message.sandstormSessionId - sandstormSessionId = message.sandstormSessionId - delete message.sandstormSessionId - - if message._id? and upsert - _id = message._id - delete message._id - RocketChat.models.Messages.upsert {_id: _id, 'u._id': message.u._id}, message - message._id = _id - else - message._id = RocketChat.models.Messages.insert message - - ### - Defer other updates as their return is not interesting to the user - ### - Meteor.defer -> - # Execute all callbacks - message.sandstormSessionId = sandstormSessionId - RocketChat.callbacks.run 'afterSaveMessage', message, room - - return message diff --git a/packages/rocketchat-lib/server/functions/sendMessage.js b/packages/rocketchat-lib/server/functions/sendMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..d0e4181e733c345bdd5c09ac41072e780e9b8cf1 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/sendMessage.js @@ -0,0 +1,59 @@ +RocketChat.sendMessage = function(user, message, room, upsert = false) { + if (!user || !message || !room._id) { + return false; + } + if (message.ts == null) { + message.ts = new Date(); + } + message.u = _.pick(user, ['_id', 'username', 'name']); + if (!Match.test(message.msg, String)) { + message.msg = ''; + } + message.rid = room._id; + if (room.usernames || room.usernames.length === 0) { + const updated_room = RocketChat.models.Rooms.findOneById(room._id); + if (updated_room != null) { + room = updated_room; + } else { + room.usernames = []; + } + } + if (message.parseUrls !== false) { + const urls = message.msg.match(/([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g); + if (urls) { + message.urls = urls.map(function(url) { + return { + url + }; + }); + } + } + message = RocketChat.callbacks.run('beforeSaveMessage', message); + // Avoid saving sandstormSessionId to the database + let sandstormSessionId = null; + if (message.sandstormSessionId) { + sandstormSessionId = message.sandstormSessionId; + delete message.sandstormSessionId; + } + if (message._id && upsert) { + const _id = message._id; + delete message._id; + RocketChat.models.Messages.upsert({ + _id, + 'u._id': message.u._id + }, message); + message._id = _id; + } else { + message._id = RocketChat.models.Messages.insert(message); + } + + /* + Defer other updates as their return is not interesting to the user + */ + Meteor.defer(() => { + // Execute all callbacks + message.sandstormSessionId = sandstormSessionId; + return RocketChat.callbacks.run('afterSaveMessage', message, room); + }); + return message; +}; diff --git a/packages/rocketchat-lib/server/functions/setUserAvatar.js b/packages/rocketchat-lib/server/functions/setUserAvatar.js index e80a8c7cdd70f83523908f8219c23148b9aaf9d8..5b005313bb6c3d941a6308f8452e8cd4df298b96 100644 --- a/packages/rocketchat-lib/server/functions/setUserAvatar.js +++ b/packages/rocketchat-lib/server/functions/setUserAvatar.js @@ -10,7 +10,7 @@ RocketChat.setUserAvatar = function(user, dataURI, contentType, service) { try { result = HTTP.get(dataURI, { npmRequestOptions: {encoding: 'binary'} }); } catch (error) { - if (error.response.statusCode !== 404) { + if (!error.response || error.response.statusCode !== 404) { console.log(`Error while handling the setting of the avatar from a url (${ dataURI }) for ${ user.username }:`, error); throw new Meteor.Error('error-avatar-url-handling', `Error while handling avatar setting from a URL (${ dataURI }) for ${ user.username }`, { function: 'RocketChat.setUserAvatar', url: dataURI, username: user.username }); } diff --git a/packages/rocketchat-lib/server/functions/setUsername.coffee b/packages/rocketchat-lib/server/functions/setUsername.coffee deleted file mode 100644 index a3afa4a49e9e0d89febac37cfee8ce018396d98d..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/functions/setUsername.coffee +++ /dev/null @@ -1,77 +0,0 @@ -RocketChat._setUsername = (userId, username) -> - username = s.trim username - if not userId or not username - return false - - try - nameValidation = new RegExp '^' + RocketChat.settings.get('UTF8_Names_Validation') + '$' - catch - nameValidation = new RegExp '^[0-9a-zA-Z-_.]+$' - - if not nameValidation.test username - return false - - user = RocketChat.models.Users.findOneById userId - - # User already has desired username, return - if user.username is username - return user - - previousUsername = user.username - - # Check username availability or if the user already owns a different casing of the name - if ( !previousUsername or !(username.toLowerCase() == previousUsername.toLowerCase())) - unless RocketChat.checkUsernameAvailability username - return false - - # If first time setting username, send Enrollment Email - try - if not previousUsername and user.emails?.length > 0 and RocketChat.settings.get 'Accounts_Enrollment_Email' - Accounts.sendEnrollmentEmail(user._id) - catch error - - user.username = username - - # If first time setting username, check if should set default avatar - if not previousUsername and RocketChat.settings.get('Accounts_SetDefaultAvatar') is true - avatarSuggestions = getAvatarSuggestionForUser user - for service, avatarData of avatarSuggestions - if service isnt 'gravatar' - RocketChat.setUserAvatar(user, avatarData.blob, avatarData.contentType, service) - gravatar = null - break - else - gravatar = avatarData - if gravatar? - RocketChat.setUserAvatar(user, gravatar.blob, gravatar.contentType, 'gravatar') - - # Username is available; if coming from old username, update all references - if previousUsername - RocketChat.models.Messages.updateAllUsernamesByUserId user._id, username - RocketChat.models.Messages.updateUsernameOfEditByUserId user._id, username - - RocketChat.models.Messages.findByMention(previousUsername).forEach (msg) -> - updatedMsg = msg.msg.replace(new RegExp("@#{previousUsername}", "ig"), "@#{username}") - RocketChat.models.Messages.updateUsernameAndMessageOfMentionByIdAndOldUsername msg._id, previousUsername, username, updatedMsg - - RocketChat.models.Rooms.replaceUsername previousUsername, username - RocketChat.models.Rooms.replaceMutedUsername previousUsername, username - RocketChat.models.Rooms.replaceUsernameOfUserByUserId user._id, username - - RocketChat.models.Subscriptions.setUserUsernameByUserId user._id, username - RocketChat.models.Subscriptions.setNameForDirectRoomsWithOldName previousUsername, username - - rs = RocketChatFileAvatarInstance.getFileWithReadStream(encodeURIComponent("#{previousUsername}.jpg")) - if rs? - RocketChatFileAvatarInstance.deleteFile encodeURIComponent("#{username}.jpg") - ws = RocketChatFileAvatarInstance.createWriteStream encodeURIComponent("#{username}.jpg"), rs.contentType - ws.on 'end', Meteor.bindEnvironment -> - RocketChatFileAvatarInstance.deleteFile encodeURIComponent("#{previousUsername}.jpg") - rs.readStream.pipe(ws) - - # Set new username - RocketChat.models.Users.setUsername user._id, username - return user - -RocketChat.setUsername = RocketChat.RateLimiter.limitFunction RocketChat._setUsername, 1, 60000, - 0: () -> return not Meteor.userId() or not RocketChat.authz.hasPermission(Meteor.userId(), 'edit-other-user-info') # Administrators have permission to change others usernames, so don't limit those diff --git a/packages/rocketchat-lib/server/functions/setUsername.js b/packages/rocketchat-lib/server/functions/setUsername.js new file mode 100644 index 0000000000000000000000000000000000000000..455e6cfbc67cc5a2d83c3d3d00c536374f9802aa --- /dev/null +++ b/packages/rocketchat-lib/server/functions/setUsername.js @@ -0,0 +1,85 @@ + +RocketChat._setUsername = function(userId, u) { + const username = s.trim(u); + if (!userId || !username) { + return false; + } + let nameValidation; + try { + nameValidation = new RegExp(`^${ RocketChat.settings.get('UTF8_Names_Validation') }$`); + } catch (error) { + nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); + } + if (!nameValidation.test(username)) { + return false; + } + const user = RocketChat.models.Users.findOneById(userId); + // User already has desired username, return + if (user.username === username) { + return user; + } + const previousUsername = user.username; + // Check username availability or if the user already owns a different casing of the name + if (!previousUsername || !(username.toLowerCase() === previousUsername.toLowerCase())) { + if (!RocketChat.checkUsernameAvailability(username)) { + return false; + } + } + //If first time setting username, send Enrollment Email + try { + if (!previousUsername && user.emails && user.emails.length > 0 && RocketChat.settings.get('Accounts_Enrollment_Email')) { + Accounts.sendEnrollmentEmail(user._id); + } + } catch (e) { + console.error(e); + } + /* globals getAvatarSuggestionForUser */ + user.username = username; + if (!previousUsername && RocketChat.settings.get('Accounts_SetDefaultAvatar') === true) { + const avatarSuggestions = getAvatarSuggestionForUser(user); + let gravatar; + Object.keys(avatarSuggestions).some(service => { + const avatarData = avatarSuggestions[service]; + if (service !== 'gravatar') { + RocketChat.setUserAvatar(user, avatarData.blob, avatarData.contentType, service); + gravatar = null; + return true; + } else { + gravatar = avatarData; + } + }); + if (gravatar != null) { + RocketChat.setUserAvatar(user, gravatar.blob, gravatar.contentType, 'gravatar'); + } + } + // Username is available; if coming from old username, update all references + if (previousUsername) { + RocketChat.models.Messages.updateAllUsernamesByUserId(user._id, username); + RocketChat.models.Messages.updateUsernameOfEditByUserId(user._id, username); + RocketChat.models.Messages.findByMention(previousUsername).forEach(function(msg) { + const updatedMsg = msg.msg.replace(new RegExp(`@${ previousUsername }`, 'ig'), `@${ username }`); + return RocketChat.models.Messages.updateUsernameAndMessageOfMentionByIdAndOldUsername(msg._id, previousUsername, username, updatedMsg); + }); + RocketChat.models.Rooms.replaceUsername(previousUsername, username); + RocketChat.models.Rooms.replaceMutedUsername(previousUsername, username); + RocketChat.models.Rooms.replaceUsernameOfUserByUserId(user._id, username); + RocketChat.models.Subscriptions.setUserUsernameByUserId(user._id, username); + RocketChat.models.Subscriptions.setNameForDirectRoomsWithOldName(previousUsername, username); + const rs = RocketChatFileAvatarInstance.getFileWithReadStream(encodeURIComponent(`${ previousUsername }.jpg`)); + if (rs != null) { + RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${ username }.jpg`)); + const ws = RocketChatFileAvatarInstance.createWriteStream(encodeURIComponent(`${ username }.jpg`), rs.contentType); + ws.on('end', Meteor.bindEnvironment(() => RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${ previousUsername }.jpg`)))); + rs.readStream.pipe(ws); + } + } + // Set new username* + RocketChat.models.Users.setUsername(user._id, username); + return user; +}; + +RocketChat.setUsername = RocketChat.RateLimiter.limitFunction(RocketChat._setUsername, 1, 60000, { + [0](userId) { + return !userId || !RocketChat.authz.hasPermission(userId, 'edit-other-user-info'); + } +}); diff --git a/packages/rocketchat-lib/server/functions/settings.coffee b/packages/rocketchat-lib/server/functions/settings.coffee deleted file mode 100644 index 1098cd7941da3552922513f053de3f46d9c7ae8b..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/functions/settings.coffee +++ /dev/null @@ -1,248 +0,0 @@ -blockedSettings = {} -process.env.SETTINGS_BLOCKED?.split(',').forEach (settingId) -> - blockedSettings[settingId] = 1 - -hiddenSettings = {} -process.env.SETTINGS_HIDDEN?.split(',').forEach (settingId) -> - hiddenSettings[settingId] = 1 - -RocketChat.settings._sorter = {} - -### -# Add a setting -# @param {String} _id -# @param {Mixed} value -# @param {Object} setting -### -RocketChat.settings.add = (_id, value, options = {}) -> - # console.log '[functions] RocketChat.settings.add -> '.green, 'arguments:', arguments - - if not _id or - not value? and not process?.env?['OVERWRITE_SETTING_' + _id]? - return false - - RocketChat.settings._sorter[options.group] ?= 0 - - options.packageValue = value - options.valueSource = 'packageValue' - options.hidden = false - options.blocked = options.blocked || false - options.sorter ?= RocketChat.settings._sorter[options.group]++ - - if options.enableQuery? - options.enableQuery = JSON.stringify options.enableQuery - - if options.i18nDefaultQuery? - options.i18nDefaultQuery = JSON.stringify options.i18nDefaultQuery - - if process?.env?[_id]? - value = process.env[_id] - if value.toLowerCase() is "true" - value = true - else if value.toLowerCase() is "false" - value = false - options.processEnvValue = value - options.valueSource = 'processEnvValue' - - else if Meteor.settings?[_id]? - value = Meteor.settings[_id] - options.meteorSettingsValue = value - options.valueSource = 'meteorSettingsValue' - - if not options.i18nLabel? - options.i18nLabel = _id - - # Default description i18n key will be the setting name + "_Description" (eg: LDAP_Enable -> LDAP_Enable_Description) - if not options.i18nDescription? - options.i18nDescription = "#{_id}_Description" - - if blockedSettings[_id]? - options.blocked = true - - if hiddenSettings[_id]? - options.hidden = true - - if process?.env?['OVERWRITE_SETTING_' + _id]? - value = process.env['OVERWRITE_SETTING_' + _id] - if value.toLowerCase() is "true" - value = true - else if value.toLowerCase() is "false" - value = false - options.value = value - options.processEnvValue = value - options.valueSource = 'processEnvValue' - - updateOperations = - $set: options - $setOnInsert: - createdAt: new Date - - if options.editor? - updateOperations.$setOnInsert.editor = options.editor - delete options.editor - - if not options.value? - if options.force is true - updateOperations.$set.value = options.packageValue - else - updateOperations.$setOnInsert.value = value - - query = _.extend { _id: _id }, updateOperations.$set - - if not options.section? - updateOperations.$unset = { section: 1 } - query.section = { $exists: false } - - existantSetting = RocketChat.models.Settings.db.findOne(query) - - if existantSetting? - if not existantSetting.editor? and updateOperations.$setOnInsert.editor? - updateOperations.$set.editor = updateOperations.$setOnInsert.editor - delete updateOperations.$setOnInsert.editor - else - updateOperations.$set.ts = new Date - - return RocketChat.models.Settings.upsert { _id: _id }, updateOperations - - - -### -# Add a setting group -# @param {String} _id -### -RocketChat.settings.addGroup = (_id, options = {}, cb) -> - # console.log '[functions] RocketChat.settings.addGroup -> '.green, 'arguments:', arguments - - if not _id - return false - - if _.isFunction(options) - cb = options - options = {} - - if not options.i18nLabel? - options.i18nLabel = _id - - if not options.i18nDescription? - options.i18nDescription = "#{_id}_Description" - - options.ts = new Date - options.blocked = false - options.hidden = false - - if blockedSettings[_id]? - options.blocked = true - - if hiddenSettings[_id]? - options.hidden = true - - RocketChat.models.Settings.upsert { _id: _id }, - $set: options - $setOnInsert: - type: 'group' - createdAt: new Date - - if cb? - cb.call - add: (id, value, options = {}) -> - options.group = _id - RocketChat.settings.add id, value, options - - section: (section, cb) -> - cb.call - add: (id, value, options = {}) -> - options.group = _id - options.section = section - RocketChat.settings.add id, value, options - - return - - -### -# Remove a setting by id -# @param {String} _id -### -RocketChat.settings.removeById = (_id) -> - # console.log '[functions] RocketChat.settings.add -> '.green, 'arguments:', arguments - - if not _id - return false - - return RocketChat.models.Settings.removeById _id - - -### -# Update a setting by id -# @param {String} _id -### -RocketChat.settings.updateById = (_id, value, editor) -> - # console.log '[functions] RocketChat.settings.updateById -> '.green, 'arguments:', arguments - - if not _id or not value? - return false - - if editor? - return RocketChat.models.Settings.updateValueAndEditorById _id, value, editor - - return RocketChat.models.Settings.updateValueById _id, value - - -### -# Update options of a setting by id -# @param {String} _id -### -RocketChat.settings.updateOptionsById = (_id, options) -> - # console.log '[functions] RocketChat.settings.updateOptionsById -> '.green, 'arguments:', arguments - - if not _id or not options? - return false - - return RocketChat.models.Settings.updateOptionsById _id, options - - -### -# Update a setting by id -# @param {String} _id -### -RocketChat.settings.clearById = (_id) -> - # console.log '[functions] RocketChat.settings.clearById -> '.green, 'arguments:', arguments - - if not _id? - return false - - return RocketChat.models.Settings.updateValueById _id, undefined - - -### -# Update a setting by id -### -RocketChat.settings.init = -> - RocketChat.settings.initialLoad = true - RocketChat.models.Settings.find().observe - added: (record) -> - Meteor.settings[record._id] = record.value - if record.env is true - process.env[record._id] = record.value - RocketChat.settings.load record._id, record.value, RocketChat.settings.initialLoad - changed: (record) -> - Meteor.settings[record._id] = record.value - if record.env is true - process.env[record._id] = record.value - RocketChat.settings.load record._id, record.value, RocketChat.settings.initialLoad - removed: (record) -> - delete Meteor.settings[record._id] - if record.env is true - delete process.env[record._id] - RocketChat.settings.load record._id, undefined, RocketChat.settings.initialLoad - RocketChat.settings.initialLoad = false - - for fn in RocketChat.settings.afterInitialLoad - fn(Meteor.settings) - - -RocketChat.settings.afterInitialLoad = [] - -RocketChat.settings.onAfterInitialLoad = (fn) -> - RocketChat.settings.afterInitialLoad.push(fn) - if RocketChat.settings.initialLoad is false - fn(Meteor.settings) diff --git a/packages/rocketchat-lib/server/functions/settings.js b/packages/rocketchat-lib/server/functions/settings.js new file mode 100644 index 0000000000000000000000000000000000000000..4fa9a3f2b40af858700b26cae989fed25493cb51 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/settings.js @@ -0,0 +1,283 @@ +const blockedSettings = {}; + +if (process.env.SETTINGS_BLOCKED) { + process.env.SETTINGS_BLOCKED.split(',').forEach((settingId) => blockedSettings[settingId] = 1); +} + +const hiddenSettings = {}; +if (process.env.SETTINGS_HIDDEN) { + process.env.SETTINGS_HIDDEN.split(',').forEach((settingId) => hiddenSettings[settingId] = 1); +} + +RocketChat.settings._sorter = {}; + + +/* +* Add a setting +* @param {String} _id +* @param {Mixed} value +* @param {Object} setting +*/ + +RocketChat.settings.add = function(_id, value, options = {}) { + if (options == null) { + options = {}; + } + if (!_id || value == null) { + return false; + } + if (RocketChat.settings._sorter[options.group] == null) { + RocketChat.settings._sorter[options.group] = 0; + } + options.packageValue = value; + options.valueSource = 'packageValue'; + options.hidden = false; + options.blocked = options.blocked || false; + if (options.sorter == null) { + options.sorter = RocketChat.settings._sorter[options.group]++; + } + if (options.enableQuery != null) { + options.enableQuery = JSON.stringify(options.enableQuery); + } + if (options.i18nDefaultQuery != null) { + options.i18nDefaultQuery = JSON.stringify(options.i18nDefaultQuery); + } + if (typeof process !== 'undefined' && process.env && process.env._id) { + let value = process.env[_id]; + if (value.toLowerCase() === 'true') { + value = true; + } else if (value.toLowerCase() === 'false') { + value = false; + } + options.processEnvValue = value; + options.valueSource = 'processEnvValue'; + } else if (Meteor.settings && Meteor.settings) { + const value = Meteor.settings[_id]; + options.meteorSettingsValue = value; + options.valueSource = 'meteorSettingsValue'; + } + if (options.i18nLabel == null) { + options.i18nLabel = _id; + } + if (options.i18nDescription == null) { + options.i18nDescription = `${ _id }_Description`; + } + if (blockedSettings[_id] != null) { + options.blocked = true; + } + if (hiddenSettings[_id] != null) { + options.hidden = true; + } + if (typeof process !== 'undefined' && process.env && process.env[`OVERWRITE_SETTING_${ _id }`]) { + let value = process.env[`OVERWRITE_SETTING_${ _id }`]; + if (value.toLowerCase() === 'true') { + value = true; + } else if (value.toLowerCase() === 'false') { + value = false; + } + options.value = value; + options.processEnvValue = value; + options.valueSource = 'processEnvValue'; + } + const updateOperations = { + $set: options, + $setOnInsert: { + createdAt: new Date + } + }; + if (options.editor != null) { + updateOperations.$setOnInsert.editor = options.editor; + delete options.editor; + } + if (options.value == null) { + if (options.force === true) { + updateOperations.$set.value = options.packageValue; + } else { + updateOperations.$setOnInsert.value = value; + } + } + const query = _.extend({ + _id + }, updateOperations.$set); + if (options.section == null) { + updateOperations.$unset = { + section: 1 + }; + query.section = { + $exists: false + }; + } + const existantSetting = RocketChat.models.Settings.db.findOne(query); + if (existantSetting != null) { + if (existantSetting.editor == null && updateOperations.$setOnInsert.editor != null) { + updateOperations.$set.editor = updateOperations.$setOnInsert.editor; + delete updateOperations.$setOnInsert.editor; + } + } else { + updateOperations.$set.ts = new Date; + } + return RocketChat.models.Settings.upsert({ + _id + }, updateOperations); +}; + + +/* +* Add a setting group +* @param {String} _id +*/ + +RocketChat.settings.addGroup = function(_id, options = {}, cb) { + if (!_id) { + return false; + } + if (_.isFunction(options)) { + cb = options; + options = {}; + } + if (options.i18nLabel == null) { + options.i18nLabel = _id; + } + if (options.i18nDescription == null) { + options.i18nDescription = `${ _id }_Description`; + } + options.ts = new Date; + options.blocked = false; + options.hidden = false; + if (blockedSettings[_id] != null) { + options.blocked = true; + } + if (hiddenSettings[_id] != null) { + options.hidden = true; + } + RocketChat.models.Settings.upsert({ + _id + }, { + $set: options, + $setOnInsert: { + type: 'group', + createdAt: new Date + } + }); + if (cb != null) { + cb.call({ + add(id, value, options) { + if (options == null) { + options = {}; + } + options.group = _id; + return RocketChat.settings.add(id, value, options); + }, + section(section, cb) { + return cb.call({ + add(id, value, options) { + if (options == null) { + options = {}; + } + options.group = _id; + options.section = section; + return RocketChat.settings.add(id, value, options); + } + }); + } + }); + } +}; + + +/* +* Remove a setting by id +* @param {String} _id +*/ + +RocketChat.settings.removeById = function(_id) { + if (!_id) { + return false; + } + return RocketChat.models.Settings.removeById(_id); +}; + + +/* +* Update a setting by id +* @param {String} _id +*/ + +RocketChat.settings.updateById = function(_id, value, editor) { + if (!_id || value == null) { + return false; + } + if (editor != null) { + return RocketChat.models.Settings.updateValueAndEditorById(_id, value, editor); + } + return RocketChat.models.Settings.updateValueById(_id, value); +}; + + +/* +* Update options of a setting by id +* @param {String} _id +*/ + +RocketChat.settings.updateOptionsById = function(_id, options) { + if (!_id || options == null) { + return false; + } + return RocketChat.models.Settings.updateOptionsById(_id, options); +}; + + +/* +* Update a setting by id +* @param {String} _id +*/ + +RocketChat.settings.clearById = function(_id) { + if (_id == null) { + return false; + } + return RocketChat.models.Settings.updateValueById(_id, undefined); +}; + + +/* +* Update a setting by id +*/ + +RocketChat.settings.init = function() { + RocketChat.settings.initialLoad = true; + RocketChat.models.Settings.find().observe({ + added(record) { + Meteor.settings[record._id] = record.value; + if (record.env === true) { + process.env[record._id] = record.value; + } + return RocketChat.settings.load(record._id, record.value, RocketChat.settings.initialLoad); + }, + changed(record) { + Meteor.settings[record._id] = record.value; + if (record.env === true) { + process.env[record._id] = record.value; + } + return RocketChat.settings.load(record._id, record.value, RocketChat.settings.initialLoad); + }, + removed(record) { + delete Meteor.settings[record._id]; + if (record.env === true) { + delete process.env[record._id]; + } + return RocketChat.settings.load(record._id, undefined, RocketChat.settings.initialLoad); + } + }); + RocketChat.settings.initialLoad = false; + RocketChat.settings.afterInitialLoad.forEach(fn => fn(Meteor.settings)); +}; + +RocketChat.settings.afterInitialLoad = []; + +RocketChat.settings.onAfterInitialLoad = function(fn) { + RocketChat.settings.afterInitialLoad.push(fn); + if (RocketChat.settings.initialLoad === false) { + return fn(Meteor.settings); + } +}; diff --git a/packages/rocketchat-lib/server/lib/PushNotification.js b/packages/rocketchat-lib/server/lib/PushNotification.js index fd1bdb8db6047f74c112d76ed6a8da96a97eddba..fca87e6455a869ddca2569533480a4802b12dec1 100644 --- a/packages/rocketchat-lib/server/lib/PushNotification.js +++ b/packages/rocketchat-lib/server/lib/PushNotification.js @@ -16,7 +16,7 @@ class PushNotification { return hash; } - send({ roomName, roomId, username, message, usersTo, payload }) { + send({ roomName, roomId, username, message, usersTo, payload, badge = 1 }) { let title; if (roomName && roomName !== '') { title = `${ roomName }`; @@ -27,7 +27,7 @@ class PushNotification { const icon = RocketChat.settings.get('Assets_favicon_192').url || RocketChat.settings.get('Assets_favicon_192').defaultUrl; const config = { from: 'push', - badge: 1, + badge, sound: 'default', title, text: message, diff --git a/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js b/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js index ea6ef470118a7f2b16568127f7e02eb2154c4700..b4802e357f85740c9ba1cef4042f87e89cd899d7 100644 --- a/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js +++ b/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js @@ -1,6 +1,14 @@ /* globals Push */ import moment from 'moment'; +function getBadgeCount(userId) { + const subscriptions = RocketChat.models.Subscriptions.findUnreadByUserId(userId).fetch(); + + return subscriptions.reduce((unread, sub) => { + return sub.unread + unread; + }, 0); +} + RocketChat.callbacks.add('afterSaveMessage', function(message, room) { // skips this callback if the message was edited if (message.editedAt) { @@ -152,6 +160,7 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { roomId: message.rid, username: push_username, message: push_message, + badge: getBadgeCount(userOfMention._id), payload: { host: Meteor.absoluteUrl(), rid: message.rid, @@ -299,23 +308,25 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { if (userIdsToPushNotify.length > 0) { if (Push.enabled === true) { - RocketChat.PushNotification.send({ - roomId: message.rid, - roomName: push_room, - username: push_username, - message: push_message, - payload: { - host: Meteor.absoluteUrl(), - rid: message.rid, - sender: message.u, - type: room.t, - name: room.name - }, - usersTo: { - userId: { - $in: userIdsToPushNotify + // send a push notification for each user individually (to get his/her badge count) + userIdsToPushNotify.forEach((userIdToNotify) => { + RocketChat.PushNotification.send({ + roomId: message.rid, + roomName: push_room, + username: push_username, + message: push_message, + badge: getBadgeCount(userIdToNotify), + payload: { + host: Meteor.absoluteUrl(), + rid: message.rid, + sender: message.u, + type: room.t, + name: room.name + }, + usersTo: { + userId: userIdToNotify } - } + }); }); } } diff --git a/packages/rocketchat-lib/server/methods/deleteMessage.js b/packages/rocketchat-lib/server/methods/deleteMessage.js index 73dd607fafcfff64748b52ff567d55072a65402a..f6954129df151b7c5c601afa422165c720d47c2b 100644 --- a/packages/rocketchat-lib/server/methods/deleteMessage.js +++ b/packages/rocketchat-lib/server/methods/deleteMessage.js @@ -23,17 +23,18 @@ Meteor.methods({ action: 'Delete_message' }); } + const forceDelete = RocketChat.authz.hasPermission(Meteor.userId(), 'force-delete-message', originalMessage.rid); const hasPermission = RocketChat.authz.hasPermission(Meteor.userId(), 'delete-message', originalMessage.rid); const deleteAllowed = RocketChat.settings.get('Message_AllowDeleting'); const deleteOwn = originalMessage && originalMessage.u && originalMessage.u._id === Meteor.userId(); - if (!(hasPermission || (deleteAllowed && deleteOwn))) { + if (!(hasPermission || (deleteAllowed && deleteOwn)) && !(forceDelete)) { throw new Meteor.Error('error-action-not-allowed', 'Not allowed', { method: 'deleteMessage', action: 'Delete_message' }); } const blockDeleteInMinutes = RocketChat.settings.get('Message_AllowDeleting_BlockDeleteInMinutes'); - if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) { + if ((blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) || !(forceDelete)) { if (originalMessage.ts == null) { return; } diff --git a/packages/rocketchat-lib/server/methods/sendMessage.coffee b/packages/rocketchat-lib/server/methods/sendMessage.coffee deleted file mode 100644 index 95e8f71a66ab09d0ff5e427a4f07d33cf8ee3798..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/methods/sendMessage.coffee +++ /dev/null @@ -1,64 +0,0 @@ -import moment from 'moment' - -Meteor.methods - sendMessage: (message) -> - - check message, Object - - if not Meteor.userId() - throw new Meteor.Error('error-invalid-user', "Invalid user", { method: 'sendMessage' }) - - if message.ts - tsDiff = Math.abs(moment(message.ts).diff()) - if tsDiff > 60000 - throw new Meteor.Error('error-message-ts-out-of-sync', 'Message timestamp is out of sync', { method: 'sendMessage', message_ts: message.ts, server_ts: new Date().getTime() }) - else if tsDiff > 10000 - message.ts = new Date() - else - message.ts = new Date() - - if message.msg?.length > RocketChat.settings.get('Message_MaxAllowedSize') - throw new Meteor.Error('error-message-size-exceeded', 'Message size exceeds Message_MaxAllowedSize', { method: 'sendMessage' }) - - user = RocketChat.models.Users.findOneById Meteor.userId(), fields: username: 1, name: 1 - - room = Meteor.call 'canAccessRoom', message.rid, user._id - - if not room - return false - - subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(message.rid, Meteor.userId()); - if subscription and (subscription.blocked or subscription.blocker) - RocketChat.Notifications.notifyUser Meteor.userId(), 'message', { - _id: Random.id() - rid: room._id - ts: new Date - msg: TAPi18n.__('room_is_blocked', {}, user.language) - } - return false - - if user.username in (room.muted or []) - RocketChat.Notifications.notifyUser Meteor.userId(), 'message', { - _id: Random.id() - rid: room._id - ts: new Date - msg: TAPi18n.__('You_have_been_muted', {}, user.language) - } - return false - - message.alias = user.name if not message.alias? and RocketChat.settings.get 'Message_SetNameToAliasEnabled' - if Meteor.settings.public.sandstorm - message.sandstormSessionId = this.connection.sandstormSessionId() - - RocketChat.metrics.messagesSent.inc() # This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 - RocketChat.sendMessage user, message, room - -# Limit a user, who does not have the "bot" role, to sending 5 msgs/second -DDPRateLimiter.addRule - type: 'method' - name: 'sendMessage' - userId: (userId) -> - user = RocketChat.models.Users.findOneById(userId) - return true if not user?.roles - return 'bot' not in user.roles -, 5, 1000 diff --git a/packages/rocketchat-lib/server/methods/sendMessage.js b/packages/rocketchat-lib/server/methods/sendMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..2f6e41502fc59988185f727a94da2a6e074fdee4 --- /dev/null +++ b/packages/rocketchat-lib/server/methods/sendMessage.js @@ -0,0 +1,81 @@ +import moment from 'moment'; + +Meteor.methods({ + sendMessage(message) { + check(message, Object); + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'sendMessage' + }); + } + if (message.ts) { + const tsDiff = Math.abs(moment(message.ts).diff()); + if (tsDiff > 60000) { + throw new Meteor.Error('error-message-ts-out-of-sync', 'Message timestamp is out of sync', { + method: 'sendMessage', + message_ts: message.ts, + server_ts: new Date().getTime() + }); + } else if (tsDiff > 10000) { + message.ts = new Date(); + } + } else { + message.ts = new Date(); + } + if (message.msg && message.msg.length > RocketChat.settings.get('Message_MaxAllowedSize')) { + throw new Meteor.Error('error-message-size-exceeded', 'Message size exceeds Message_MaxAllowedSize', { + method: 'sendMessage' + }); + } + const user = RocketChat.models.Users.findOneById(Meteor.userId(), { + fields: { + username: 1, + name: 1 + } + }); + const room = Meteor.call('canAccessRoom', message.rid, user._id); + if (!room) { + return false; + } + const subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(message.rid, Meteor.userId()); + if (subscription && subscription.blocked || subscription.blocker) { + RocketChat.Notifications.notifyUser(Meteor.userId(), 'message', { + _id: Random.id(), + rid: room._id, + ts: new Date, + msg: TAPi18n.__('room_is_blocked', {}, user.language) + }); + return false; + } + + if ((room.muted||[]).includes(user.username)) { + RocketChat.Notifications.notifyUser(Meteor.userId(), 'message', { + _id: Random.id(), + rid: room._id, + ts: new Date, + msg: TAPi18n.__('You_have_been_muted', {}, user.language) + }); + return false; + } + if (message.alias == null && RocketChat.settings.get('Message_SetNameToAliasEnabled')) { + message.alias = user.name; + } + if (Meteor.settings['public'].sandstorm) { + message.sandstormSessionId = this.connection.sandstormSessionId(); + } + RocketChat.metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 + return RocketChat.sendMessage(user, message, room); + } +}); +// Limit a user, who does not have the "bot" role, to sending 5 msgs/second +DDPRateLimiter.addRule({ + type: 'method', + name: 'sendMessage', + userId(userId) { + const user = RocketChat.models.Users.findOneById(userId); + if (user == null || !user.roles) { + return true; + } + return user.roles.includes('bot'); + } +}, 5, 1000); diff --git a/packages/rocketchat-lib/server/models/Messages.coffee b/packages/rocketchat-lib/server/models/Messages.coffee deleted file mode 100644 index 8ac7b4efcd7bb954d68857c58fce794e8f7ba67d..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/models/Messages.coffee +++ /dev/null @@ -1,462 +0,0 @@ -RocketChat.models.Messages = new class extends RocketChat.models._Base - constructor: -> - super('message') - - @tryEnsureIndex { 'rid': 1, 'ts': 1 } - @tryEnsureIndex { 'ts': 1 } - @tryEnsureIndex { 'u._id': 1 } - @tryEnsureIndex { 'editedAt': 1 }, { sparse: 1 } - @tryEnsureIndex { 'editedBy._id': 1 }, { sparse: 1 } - @tryEnsureIndex { 'rid': 1, 't': 1, 'u._id': 1 } - @tryEnsureIndex { 'expireAt': 1 }, { expireAfterSeconds: 0 } - @tryEnsureIndex { 'msg': 'text' } - @tryEnsureIndex { 'file._id': 1 }, { sparse: 1 } - @tryEnsureIndex { 'mentions.username': 1 }, { sparse: 1 } - @tryEnsureIndex { 'pinned': 1 }, { sparse: 1 } - @tryEnsureIndex { 'snippeted': 1 }, { sparse: 1 } - @tryEnsureIndex { 'location': '2dsphere' } - @tryEnsureIndex { 'slackBotId': 1, 'slackTs': 1 }, { sparse: 1 } - - # FIND - findByMention: (username, options) -> - query = - "mentions.username": username - - return @find query, options - - findVisibleByMentionAndRoomId: (username, rid, options) -> - query = - _hidden: { $ne: true } - "mentions.username": username - "rid": rid - - return @find query, options - - findVisibleByRoomId: (roomId, options) -> - query = - _hidden: - $ne: true - - rid: roomId - - return @find query, options - - findVisibleByRoomIdNotContainingTypes: (roomId, types, options) -> - query = - _hidden: - $ne: true - - rid: roomId - - if Match.test(types, [String]) and types.length > 0 - query.t = - $nin: types - - return @find query, options - - findInvisibleByRoomId: (roomId, options) -> - query = - _hidden: true - rid: roomId - - return @find query, options - - findVisibleByRoomIdAfterTimestamp: (roomId, timestamp, options) -> - query = - _hidden: - $ne: true - rid: roomId - ts: - $gt: timestamp - - return @find query, options - - findVisibleByRoomIdBeforeTimestamp: (roomId, timestamp, options) -> - query = - _hidden: - $ne: true - rid: roomId - ts: - $lt: timestamp - - return @find query, options - - findVisibleByRoomIdBeforeTimestampInclusive: (roomId, timestamp, options) -> - query = - _hidden: - $ne: true - rid: roomId - ts: - $lte: timestamp - - return @find query, options - - findVisibleByRoomIdBetweenTimestamps: (roomId, afterTimestamp, beforeTimestamp, options) -> - query = - _hidden: - $ne: true - rid: roomId - ts: - $gt: afterTimestamp - $lt: beforeTimestamp - - return @find query, options - - findVisibleByRoomIdBetweenTimestampsInclusive: (roomId, afterTimestamp, beforeTimestamp, options) -> - query = - _hidden: - $ne: true - rid: roomId - ts: - $gte: afterTimestamp - $lte: beforeTimestamp - - return @find query, options - - findVisibleByRoomIdBeforeTimestampNotContainingTypes: (roomId, timestamp, types, options) -> - query = - _hidden: - $ne: true - rid: roomId - ts: - $lt: timestamp - - if Match.test(types, [String]) and types.length > 0 - query.t = - $nin: types - - return @find query, options - - findVisibleByRoomIdBetweenTimestampsNotContainingTypes: (roomId, afterTimestamp, beforeTimestamp, types, options) -> - query = - _hidden: - $ne: true - rid: roomId - ts: - $gt: afterTimestamp - $lt: beforeTimestamp - - if Match.test(types, [String]) and types.length > 0 - query.t = - $nin: types - - return @find query, options - - findVisibleCreatedOrEditedAfterTimestamp: (timestamp, options) -> - query = - _hidden: { $ne: true } - $or: [ - ts: - $gt: timestamp - , - 'editedAt': - $gt: timestamp - ] - - return @find query, options - - findStarredByUserAtRoom: (userId, roomId, options) -> - query = - _hidden: { $ne: true } - 'starred._id': userId - rid: roomId - - return @find query, options - - findPinnedByRoom: (roomId, options) -> - query = - t: { $ne: 'rm' } - _hidden: { $ne: true } - pinned: true - rid: roomId - - return @find query, options - - findSnippetedByRoom: (roomId, options) -> - query = - _hidden: { $ne: true } - snippeted: true - rid: roomId - - return @find query, options - - getLastTimestamp: (options = {}) -> - query = { ts: { $exists: 1 } } - options.sort = { ts: -1 } - options.limit = 1 - - return @find(query, options)?.fetch?()?[0]?.ts - - findByRoomIdAndMessageIds: (rid, messageIds, options) -> - query = - rid: rid - _id: - $in: messageIds - - return @find query, options - - findOneBySlackBotIdAndSlackTs: (slackBotId, slackTs) -> - query = - slackBotId: slackBotId - slackTs: slackTs - - return @findOne query - - findOneBySlackTs: (slackTs) -> - query = - slackTs: slackTs - - return @findOne query - - cloneAndSaveAsHistoryById: (_id) -> - me = RocketChat.models.Users.findOneById Meteor.userId() - record = @findOneById _id - record._hidden = true - record.parent = record._id - record.editedAt = new Date - record.editedBy = - _id: Meteor.userId() - username: me.username - delete record._id - return @insert record - - # UPDATE - setHiddenById: (_id, hidden=true) -> - query = - _id: _id - - update = - $set: - _hidden: hidden - - return @update query, update - - setAsDeletedByIdAndUser: (_id, user) -> - query = - _id: _id - - update = - $set: - msg: '' - t: 'rm' - urls: [] - mentions: [] - attachments: [] - reactions: [] - editedAt: new Date() - editedBy: - _id: user._id - username: user.username - - return @update query, update - - setPinnedByIdAndUserId: (_id, pinnedBy, pinned=true, pinnedAt=0) -> - query = - _id: _id - - update = - $set: - pinned: pinned - pinnedAt: pinnedAt || new Date - pinnedBy: pinnedBy - - return @update query, update - - setSnippetedByIdAndUserId: (message, snippetName, snippetedBy, snippeted=true, snippetedAt=0) -> - query = - _id: message._id - - msg = "```" + message.msg + "```" - - update = - $set: - msg: msg - snippeted: snippeted - snippetedAt: snippetedAt || new Date - snippetedBy: snippetedBy - snippetName: snippetName - - return @update query, update - - setUrlsById: (_id, urls) -> - query = - _id: _id - - update = - $set: - urls: urls - - return @update query, update - - updateAllUsernamesByUserId: (userId, username) -> - query = - 'u._id': userId - - update = - $set: - "u.username": username - - return @update query, update, { multi: true } - - updateUsernameOfEditByUserId: (userId, username) -> - query = - 'editedBy._id': userId - - update = - $set: - "editedBy.username": username - - return @update query, update, { multi: true } - - updateUsernameAndMessageOfMentionByIdAndOldUsername: (_id, oldUsername, newUsername, newMessage) -> - query = - _id: _id - "mentions.username": oldUsername - - update = - $set: - "mentions.$.username": newUsername - "msg": newMessage - - return @update query, update - - updateUserStarById: (_id, userId, starred) -> - query = - _id: _id - - if starred - update = - $addToSet: - starred: { _id: userId } - else - update = - $pull: - starred: { _id: Meteor.userId() } - - return @update query, update - - upgradeEtsToEditAt: -> - query = - ets: { $exists: 1 } - - update = - $rename: - "ets": "editedAt" - - return @update query, update, { multi: true } - - setMessageAttachments: (_id, attachments) -> - query = - _id: _id - - update = - $set: - attachments: attachments - - return @update query, update - - setSlackBotIdAndSlackTs: (_id, slackBotId, slackTs) -> - query = - _id: _id - - update = - $set: - slackBotId: slackBotId - slackTs: slackTs - - return @update query, update - - - # INSERT - createWithTypeRoomIdMessageAndUser: (type, roomId, message, user, extraData) -> - room = RocketChat.models.Rooms.findOneById roomId, { fields: { sysMes: 1 }} - if room?.sysMes is false - return - record = - t: type - rid: roomId - ts: new Date - msg: message - u: - _id: user._id - username: user.username - groupable: false - - _.extend record, extraData - - record._id = @insertOrUpsert record - RocketChat.models.Rooms.incMsgCountById(room._id, 1) - return record - - createUserJoinWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'uj', roomId, message, user, extraData - - createUserLeaveWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'ul', roomId, message, user, extraData - - createUserRemovedWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'ru', roomId, message, user, extraData - - createUserAddedWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'au', roomId, message, user, extraData - - createCommandWithRoomIdAndUser: (command, roomId, user, extraData) -> - return @createWithTypeRoomIdMessageAndUser 'command', roomId, command, user, extraData - - createUserMutedWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'user-muted', roomId, message, user, extraData - - createUserUnmutedWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'user-unmuted', roomId, message, user, extraData - - createNewModeratorWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'new-moderator', roomId, message, user, extraData - - createModeratorRemovedWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'moderator-removed', roomId, message, user, extraData - - createNewOwnerWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'new-owner', roomId, message, user, extraData - - createOwnerRemovedWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'owner-removed', roomId, message, user, extraData - - createSubscriptionRoleAddedWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'subscription-role-added', roomId, message, user, extraData - - createSubscriptionRoleRemovedWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'subscription-role-removed', roomId, message, user, extraData - - # REMOVE - removeById: (_id) -> - query = - _id: _id - - return @remove query - - removeByRoomId: (roomId) -> - query = - rid: roomId - - return @remove query - - removeByUserId: (userId) -> - query = - "u._id": userId - - return @remove query - - getMessageByFileId: (fileID) -> - return @findOne { 'file._id': fileID } diff --git a/packages/rocketchat-lib/server/models/Messages.js b/packages/rocketchat-lib/server/models/Messages.js new file mode 100644 index 0000000000000000000000000000000000000000..dabf189366333778851460137a3699200b24ef18 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Messages.js @@ -0,0 +1,580 @@ +RocketChat.models.Messages = new class extends RocketChat.models._Base { + constructor() { + super('message'); + + this.tryEnsureIndex({ 'rid': 1, 'ts': 1 }); + this.tryEnsureIndex({ 'ts': 1 }); + this.tryEnsureIndex({ 'u._id': 1 }); + this.tryEnsureIndex({ 'editedAt': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'editedBy._id': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'rid': 1, 't': 1, 'u._id': 1 }); + this.tryEnsureIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 }); + this.tryEnsureIndex({ 'msg': 'text' }); + this.tryEnsureIndex({ 'file._id': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'mentions.username': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'pinned': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'snippeted': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'location': '2dsphere' }); + this.tryEnsureIndex({ 'slackBotId': 1, 'slackTs': 1 }, { sparse: 1 }); + } + + // FIND + findByMention(username, options) { + const query = {'mentions.username': username}; + + return this.find(query, options); + } + + findVisibleByMentionAndRoomId(username, rid, options) { + const query = { + _hidden: { $ne: true }, + 'mentions.username': username, + rid + }; + + return this.find(query, options); + } + + findVisibleByRoomId(roomId, options) { + const query = { + _hidden: { + $ne: true + }, + + rid: roomId + }; + + return this.find(query, options); + } + + findVisibleByRoomIdNotContainingTypes(roomId, types, options) { + const query = { + _hidden: { + $ne: true + }, + + rid: roomId + }; + + if (Match.test(types, [String]) && (types.length > 0)) { + query.t = + {$nin: types}; + } + + return this.find(query, options); + } + + findInvisibleByRoomId(roomId, options) { + const query = { + _hidden: true, + rid: roomId + }; + + return this.find(query, options); + } + + findVisibleByRoomIdAfterTimestamp(roomId, timestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $gt: timestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBeforeTimestamp(roomId, timestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $lt: timestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBeforeTimestampInclusive(roomId, timestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $lte: timestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBetweenTimestamps(roomId, afterTimestamp, beforeTimestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $gt: afterTimestamp, + $lt: beforeTimestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBetweenTimestampsInclusive(roomId, afterTimestamp, beforeTimestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $gte: afterTimestamp, + $lte: beforeTimestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBeforeTimestampNotContainingTypes(roomId, timestamp, types, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $lt: timestamp + } + }; + + if (Match.test(types, [String]) && (types.length > 0)) { + query.t = + {$nin: types}; + } + + return this.find(query, options); + } + + findVisibleByRoomIdBetweenTimestampsNotContainingTypes(roomId, afterTimestamp, beforeTimestamp, types, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $gt: afterTimestamp, + $lt: beforeTimestamp + } + }; + + if (Match.test(types, [String]) && (types.length > 0)) { + query.t = + {$nin: types}; + } + + return this.find(query, options); + } + + findVisibleCreatedOrEditedAfterTimestamp(timestamp, options) { + const query = { + _hidden: { $ne: true }, + $or: [{ + ts: { + $gt: timestamp + } + }, + { + 'editedAt': { + $gt: timestamp + } + } + ] + }; + + return this.find(query, options); + } + + findStarredByUserAtRoom(userId, roomId, options) { + const query = { + _hidden: { $ne: true }, + 'starred._id': userId, + rid: roomId + }; + + return this.find(query, options); + } + + findPinnedByRoom(roomId, options) { + const query = { + t: { $ne: 'rm' }, + _hidden: { $ne: true }, + pinned: true, + rid: roomId + }; + + return this.find(query, options); + } + + findSnippetedByRoom(roomId, options) { + const query = { + _hidden: { $ne: true }, + snippeted: true, + rid: roomId + }; + + return this.find(query, options); + } + + getLastTimestamp(options) { + if (options == null) { options = {}; } + const query = { ts: { $exists: 1 } }; + options.sort = { ts: -1 }; + options.limit = 1; + const [message] = this.find(query, options).fetch(); + return message && message.ts; + } + + findByRoomIdAndMessageIds(rid, messageIds, options) { + const query = { + rid, + _id: { + $in: messageIds + } + }; + + return this.find(query, options); + } + + findOneBySlackBotIdAndSlackTs(slackBotId, slackTs) { + const query = { + slackBotId, + slackTs + }; + + return this.findOne(query); + } + + findOneBySlackTs(slackTs) { + const query = {slackTs}; + + return this.findOne(query); + } + + cloneAndSaveAsHistoryById(_id) { + const me = RocketChat.models.Users.findOneById(Meteor.userId()); + const record = this.findOneById(_id); + record._hidden = true; + record.parent = record._id; + record.editedAt = new Date; + record.editedBy = { + _id: Meteor.userId(), + username: me.username + }; + delete record._id; + return this.insert(record); + } + + // UPDATE + setHiddenById(_id, hidden) { + if (hidden == null) { hidden = true; } + const query = {_id}; + + const update = { + $set: { + _hidden: hidden + } + }; + + return this.update(query, update); + } + + setAsDeletedByIdAndUser(_id, user) { + const query = {_id}; + + const update = { + $set: { + msg: '', + t: 'rm', + urls: [], + mentions: [], + attachments: [], + reactions: [], + editedAt: new Date(), + editedBy: { + _id: user._id, + username: user.username + } + } + }; + + return this.update(query, update); + } + + setPinnedByIdAndUserId(_id, pinnedBy, pinned, pinnedAt) { + if (pinned == null) { pinned = true; } + if (pinnedAt == null) { pinnedAt = 0; } + const query = {_id}; + + const update = { + $set: { + pinned, + pinnedAt: pinnedAt || new Date, + pinnedBy + } + }; + + return this.update(query, update); + } + + setSnippetedByIdAndUserId(message, snippetName, snippetedBy, snippeted, snippetedAt) { + if (snippeted == null) { snippeted = true; } + if (snippetedAt == null) { snippetedAt = 0; } + const query = {_id: message._id}; + + const msg = `\`\`\`${ message.msg }\`\`\``; + + const update = { + $set: { + msg, + snippeted, + snippetedAt: snippetedAt || new Date, + snippetedBy, + snippetName + } + }; + + return this.update(query, update); + } + + setUrlsById(_id, urls) { + const query = {_id}; + + const update = { + $set: { + urls + } + }; + + return this.update(query, update); + } + + updateAllUsernamesByUserId(userId, username) { + const query = {'u._id': userId}; + + const update = { + $set: { + 'u.username': username + } + }; + + return this.update(query, update, { multi: true }); + } + + updateUsernameOfEditByUserId(userId, username) { + const query = {'editedBy._id': userId}; + + const update = { + $set: { + 'editedBy.username': username + } + }; + + return this.update(query, update, { multi: true }); + } + + updateUsernameAndMessageOfMentionByIdAndOldUsername(_id, oldUsername, newUsername, newMessage) { + const query = { + _id, + 'mentions.username': oldUsername + }; + + const update = { + $set: { + 'mentions.$.username': newUsername, + 'msg': newMessage + } + }; + + return this.update(query, update); + } + + updateUserStarById(_id, userId, starred) { + let update; + const query = {_id}; + + if (starred) { + update = { + $addToSet: { + starred: { _id: userId } + } + }; + } else { + update = { + $pull: { + starred: { _id: Meteor.userId() } + } + }; + } + + return this.update(query, update); + } + + upgradeEtsToEditAt() { + const query = {ets: { $exists: 1 }}; + + const update = { + $rename: { + 'ets': 'editedAt' + } + }; + + return this.update(query, update, { multi: true }); + } + + setMessageAttachments(_id, attachments) { + const query = {_id}; + + const update = { + $set: { + attachments + } + }; + + return this.update(query, update); + } + + setSlackBotIdAndSlackTs(_id, slackBotId, slackTs) { + const query = {_id}; + + const update = { + $set: { + slackBotId, + slackTs + } + }; + + return this.update(query, update); + } + + + // INSERT + createWithTypeRoomIdMessageAndUser(type, roomId, message, user, extraData) { + const room = RocketChat.models.Rooms.findOneById(roomId, { fields: { sysMes: 1 }}); + if ((room != null ? room.sysMes : undefined) === false) { + return; + } + const record = { + t: type, + rid: roomId, + ts: new Date, + msg: message, + u: { + _id: user._id, + username: user.username + }, + groupable: false + }; + + _.extend(record, extraData); + + record._id = this.insertOrUpsert(record); + RocketChat.models.Rooms.incMsgCountById(room._id, 1); + return record; + } + + createUserJoinWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('uj', roomId, message, user, extraData); + } + + createUserLeaveWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('ul', roomId, message, user, extraData); + } + + createUserRemovedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('ru', roomId, message, user, extraData); + } + + createUserAddedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('au', roomId, message, user, extraData); + } + + createCommandWithRoomIdAndUser(command, roomId, user, extraData) { + return this.createWithTypeRoomIdMessageAndUser('command', roomId, command, user, extraData); + } + + createUserMutedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('user-muted', roomId, message, user, extraData); + } + + createUserUnmutedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('user-unmuted', roomId, message, user, extraData); + } + + createNewModeratorWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('new-moderator', roomId, message, user, extraData); + } + + createModeratorRemovedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('moderator-removed', roomId, message, user, extraData); + } + + createNewOwnerWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('new-owner', roomId, message, user, extraData); + } + + createOwnerRemovedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('owner-removed', roomId, message, user, extraData); + } + + createSubscriptionRoleAddedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('subscription-role-added', roomId, message, user, extraData); + } + + createSubscriptionRoleRemovedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('subscription-role-removed', roomId, message, user, extraData); + } + + // REMOVE + removeById(_id) { + const query = {_id}; + + return this.remove(query); + } + + removeByRoomId(roomId) { + const query = {rid: roomId}; + + return this.remove(query); + } + + removeByUserId(userId) { + const query = {'u._id': userId}; + + return this.remove(query); + } + + getMessageByFileId(fileID) { + return this.findOne({ 'file._id': fileID }); + } +}; diff --git a/packages/rocketchat-lib/server/models/Rooms.coffee b/packages/rocketchat-lib/server/models/Rooms.coffee deleted file mode 100644 index 1d58e578e117942e42cdc12ab374527076574ddd..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/models/Rooms.coffee +++ /dev/null @@ -1,633 +0,0 @@ -class ModelRooms extends RocketChat.models._Base - constructor: -> - super(arguments...) - - @tryEnsureIndex { 'name': 1 }, { unique: 1, sparse: 1 } - @tryEnsureIndex { 'default': 1 } - @tryEnsureIndex { 'usernames': 1 } - @tryEnsureIndex { 't': 1 } - @tryEnsureIndex { 'u._id': 1 } - - this.cache.ignoreUpdatedFields.push('msgs', 'lm') - this.cache.ensureIndex(['t', 'name'], 'unique') - this.cache.options = {fields: {usernames: 0}} - - findOneByIdOrName: (_idOrName, options) -> - query = { - $or: [{ - _id: _idOrName - }, { - name: _idOrName - }] - } - - return this.findOne(query, options) - - findOneByImportId: (_id, options) -> - query = - importIds: _id - - return @findOne query, options - - findOneByName: (name, options) -> - query = - name: name - - return @findOne query, options - - findOneByNameAndType: (name, type, options) -> - query = - name: name - t: type - - return @findOne query, options - - findOneByIdContainingUsername: (_id, username, options) -> - query = - _id: _id - usernames: username - - return @findOne query, options - - findOneByNameAndTypeNotContainingUsername: (name, type, username, options) -> - query = - name: name - t: type - usernames: - $ne: username - - return @findOne query, options - - - # FIND - - findById: (roomId, options) -> - return @find { _id: roomId }, options - - findByIds: (roomIds, options) -> - return @find { _id: $in: [].concat roomIds }, options - - findByType: (type, options) -> - query = - t: type - - return @find query, options - - findByTypes: (types, options) -> - query = - t: - $in: types - - return @find query, options - - findByUserId: (userId, options) -> - query = - "u._id": userId - - return @find query, options - - findBySubscriptionUserId: (userId, options) -> - if this.useCache - data = RocketChat.models.Subscriptions.findByUserId(userId).fetch() - data = data.map (item) -> - if item._room - return item._room - console.log('Empty Room for Subscription', item); - return {} - return this.arrayToCursor this.processQueryOptionsOnResult(data, options) - - data = RocketChat.models.Subscriptions.findByUserId(userId, {fields: {rid: 1}}).fetch() - data = data.map (item) -> item.rid - - query = - _id: - $in: data - - this.find query, options - - findBySubscriptionUserIdUpdatedAfter: (userId, _updatedAt, options) -> - if this.useCache - data = RocketChat.models.Subscriptions.findByUserId(userId).fetch() - data = data.map (item) -> - if item._room - return item._room - console.log('Empty Room for Subscription', item); - return {} - data = data.filter (item) -> item._updatedAt > _updatedAt - return this.arrayToCursor this.processQueryOptionsOnResult(data, options) - - ids = RocketChat.models.Subscriptions.findByUserId(userId, {fields: {rid: 1}}).fetch() - ids = ids.map (item) -> item.rid - - query = - _id: - $in: ids - _updatedAt: - $gt: _updatedAt - - this.find query, options - - findByNameContaining: (name, options) -> - nameRegex = new RegExp s.trim(s.escapeRegExp(name)), "i" - - query = - $or: [ - name: nameRegex - , - t: 'd' - usernames: nameRegex - ] - - return @find query, options - - findByNameContainingTypesWithUsername: (name, types, options) -> - nameRegex = new RegExp s.trim(s.escapeRegExp(name)), "i" - - $or = [] - for type in types - obj = {name: nameRegex, t: type.type} - if type.username? - obj.usernames = type.username - if type.ids? - obj._id = $in: type.ids - $or.push obj - - query = - $or: $or - - return @find query, options - - findContainingTypesWithUsername: (types, options) -> - - $or = [] - for type in types - obj = {t: type.type} - if type.username? - obj.usernames = type.username - if type.ids? - obj._id = $in: type.ids - $or.push obj - - query = - $or: $or - - return @find query, options - - findByNameContainingAndTypes: (name, types, options) -> - nameRegex = new RegExp s.trim(s.escapeRegExp(name)), "i" - - query = - t: - $in: types - $or: [ - name: nameRegex - , - t: 'd' - usernames: nameRegex - ] - - return @find query, options - - findByNameAndType: (name, type, options) -> - query = - t: type - name: name - - return @find query, options - - findByNameAndTypeNotDefault: (name, type, options) -> - query = - t: type - name: name - default: - $ne: true - - return @find query, options - - findByNameAndTypeNotContainingUsername: (name, type, username, options) -> - query = - t: type - name: name - usernames: - $ne: username - - return @find query, options - - findByNameStartingAndTypes: (name, types, options) -> - nameRegex = new RegExp "^" + s.trim(s.escapeRegExp(name)), "i" - - query = - t: - $in: types - $or: [ - name: nameRegex - , - t: 'd' - usernames: nameRegex - ] - - return @find query, options - - findByDefaultAndTypes: (defaultValue, types, options) -> - query = - default: defaultValue - t: - $in: types - - return @find query, options - - findByTypeContainingUsername: (type, username, options) -> - query = - t: type - usernames: username - - return @find query, options - - findByTypeContainingUsernames: (type, username, options) -> - query = - t: type - usernames: { $all: [].concat(username) } - - return @find query, options - - findByTypesAndNotUserIdContainingUsername: (types, userId, username, options) -> - query = - t: - $in: types - uid: - $ne: userId - usernames: username - - return @find query, options - - findByContainingUsername: (username, options) -> - query = - usernames: username - - return @find query, options - - findByTypeAndName: (type, name, options) -> - if this.useCache - return this.cache.findByIndex('t,name', [type, name], options) - - query = - name: name - t: type - - return @find query, options - - findByTypeAndNameContainingUsername: (type, name, username, options) -> - query = - name: name - t: type - usernames: username - - return @find query, options - - findByTypeAndArchivationState: (type, archivationstate, options) -> - query = - t: type - - if archivationstate - query.archived = true - else - query.archived = { $ne: true } - - return @find query, options - - # UPDATE - addImportIds: (_id, importIds) -> - importIds = [].concat(importIds); - query = - _id: _id - - update = - $addToSet: - importIds: - $each: importIds - - return @update query, update - - archiveById: (_id) -> - query = - _id: _id - - update = - $set: - archived: true - - return @update query, update - - unarchiveById: (_id) -> - query = - _id: _id - - update = - $set: - archived: false - - return @update query, update - - addUsernameById: (_id, username, muted) -> - query = - _id: _id - - update = - $addToSet: - usernames: username - - if muted - update.$addToSet.muted = username - - return @update query, update - - addUsernamesById: (_id, usernames) -> - query = - _id: _id - - update = - $addToSet: - usernames: - $each: usernames - - return @update query, update - - addUsernameByName: (name, username) -> - query = - name: name - - update = - $addToSet: - usernames: username - - return @update query, update - - removeUsernameById: (_id, username) -> - query = - _id: _id - - update = - $pull: - usernames: username - - return @update query, update - - removeUsernamesById: (_id, usernames) -> - query = - _id: _id - - update = - $pull: - usernames: - $in: usernames - - return @update query, update - - removeUsernameFromAll: (username) -> - query = - usernames: username - - update = - $pull: - usernames: username - - return @update query, update, { multi: true } - - removeUsernameByName: (name, username) -> - query = - name: name - - update = - $pull: - usernames: username - - return @update query, update - - setNameById: (_id, name) -> - query = - _id: _id - - update = - $set: - name: name - - return @update query, update - - incMsgCountById: (_id, inc=1) -> - query = - _id: _id - - update = - $inc: - msgs: inc - - return @update query, update - - incMsgCountAndSetLastMessageTimestampById: (_id, inc=1, lastMessageTimestamp) -> - query = - _id: _id - - update = - $set: - lm: lastMessageTimestamp - $inc: - msgs: inc - - return @update query, update - - replaceUsername: (previousUsername, username) -> - query = - usernames: previousUsername - - update = - $set: - "usernames.$": username - - return @update query, update, { multi: true } - - replaceMutedUsername: (previousUsername, username) -> - query = - muted: previousUsername - - update = - $set: - "muted.$": username - - return @update query, update, { multi: true } - - replaceUsernameOfUserByUserId: (userId, username) -> - query = - "u._id": userId - - update = - $set: - "u.username": username - - return @update query, update, { multi: true } - - setJoinCodeById: (_id, joinCode) -> - query = - _id: _id - - if joinCode?.trim() isnt '' - update = - $set: - joinCodeRequired: true - joinCode: joinCode - else - update = - $set: - joinCodeRequired: false - $unset: - joinCode: 1 - - return @update query, update - - setUserById: (_id, user) -> - query = - _id: _id - - update = - $set: - u: - _id: user._id - username: user.username - - return @update query, update - - setTypeById: (_id, type) -> - query = - _id: _id - update = - $set: - t: type - if type == 'p' - update.$unset = {default: ''} - - return @update query, update - - setTopicById: (_id, topic) -> - query = - _id: _id - - update = - $set: - topic: topic - - return @update query, update - - setAnnouncementById: (_id, announcement) -> - query = - _id: _id - - update = - $set: - announcement: announcement - - return @update query, update - - muteUsernameByRoomId: (_id, username) -> - query = - _id: _id - - update = - $addToSet: - muted: username - - return @update query, update - - unmuteUsernameByRoomId: (_id, username) -> - query = - _id: _id - - update = - $pull: - muted: username - - return @update query, update - - saveDefaultById: (_id, defaultValue) -> - query = - _id: _id - - update = - $set: - default: defaultValue is 'true' - - return @update query, update - - setTopicAndTagsById: (_id, topic, tags) -> - setData = {} - unsetData = {} - - if topic? - if not _.isEmpty(s.trim(topic)) - setData.topic = s.trim(topic) - else - unsetData.topic = 1 - - if tags? - if not _.isEmpty(s.trim(tags)) - setData.tags = s.trim(tags).split(',').map((tag) => return s.trim(tag)) - else - unsetData.tags = 1 - - update = {} - - if not _.isEmpty setData - update.$set = setData - - if not _.isEmpty unsetData - update.$unset = unsetData - - if _.isEmpty update - return - - return @update { _id: _id }, update - - # INSERT - createWithTypeNameUserAndUsernames: (type, name, user, usernames, extraData) -> - room = - name: name - t: type - usernames: usernames - msgs: 0 - u: - _id: user._id - username: user.username - - _.extend room, extraData - - room._id = @insert room - return room - - createWithIdTypeAndName: (_id, type, name, extraData) -> - room = - _id: _id - ts: new Date() - t: type - name: name - usernames: [] - msgs: 0 - - _.extend room, extraData - - @insert room - return room - - - # REMOVE - removeById: (_id) -> - query = - _id: _id - - return @remove query - - removeByTypeContainingUsername: (type, username) -> - query = - t: type - usernames: username - - return @remove query - -RocketChat.models.Rooms = new ModelRooms('room', true) diff --git a/packages/rocketchat-lib/server/models/Rooms.js b/packages/rocketchat-lib/server/models/Rooms.js new file mode 100644 index 0000000000000000000000000000000000000000..763155e1c83f0464ea9442780ffe36f360077c3c --- /dev/null +++ b/packages/rocketchat-lib/server/models/Rooms.js @@ -0,0 +1,760 @@ +class ModelRooms extends RocketChat.models._Base { + constructor() { + super(...arguments); + + this.tryEnsureIndex({ 'name': 1 }, { unique: 1, sparse: 1 }); + this.tryEnsureIndex({ 'default': 1 }); + this.tryEnsureIndex({ 'usernames': 1 }); + this.tryEnsureIndex({ 't': 1 }); + this.tryEnsureIndex({ 'u._id': 1 }); + + this.cache.ignoreUpdatedFields.push('msgs', 'lm'); + this.cache.ensureIndex(['t', 'name'], 'unique'); + this.cache.options = {fields: {usernames: 0}}; + } + + findOneByIdOrName(_idOrName, options) { + const query = { + $or: [{ + _id: _idOrName + }, { + name: _idOrName + }] + }; + + return this.findOne(query, options); + } + + findOneByImportId(_id, options) { + const query = {importIds: _id}; + + return this.findOne(query, options); + } + + findOneByName(name, options) { + const query = {name}; + + return this.findOne(query, options); + } + + findOneByNameAndType(name, type, options) { + const query = { + name, + t: type + }; + + return this.findOne(query, options); + } + + findOneByIdContainingUsername(_id, username, options) { + const query = { + _id, + usernames: username + }; + + return this.findOne(query, options); + } + + findOneByNameAndTypeNotContainingUsername(name, type, username, options) { + const query = { + name, + t: type, + usernames: { + $ne: username + } + }; + + return this.findOne(query, options); + } + + + // FIND + + findById(roomId, options) { + return this.find({ _id: roomId }, options); + } + + findByIds(roomIds, options) { + return this.find({ _id: {$in: [].concat(roomIds)} }, options); + } + + findByType(type, options) { + const query = {t: type}; + + return this.find(query, options); + } + + findByTypes(types, options) { + const query = { + t: { + $in: types + } + }; + + return this.find(query, options); + } + + findByUserId(userId, options) { + const query = {'u._id': userId}; + + return this.find(query, options); + } + + findBySubscriptionUserId(userId, options) { + let data; + if (this.useCache) { + data = RocketChat.models.Subscriptions.findByUserId(userId).fetch(); + data = data.map(function(item) { + if (item._room) { + return item._room; + } + console.log('Empty Room for Subscription', item); + return {}; + }); + return this.arrayToCursor(this.processQueryOptionsOnResult(data, options)); + } + + data = RocketChat.models.Subscriptions.findByUserId(userId, {fields: {rid: 1}}).fetch(); + data = data.map(item => item.rid); + + const query = { + _id: { + $in: data + } + }; + + return this.find(query, options); + } + + findBySubscriptionUserIdUpdatedAfter(userId, _updatedAt, options) { + if (this.useCache) { + let data = RocketChat.models.Subscriptions.findByUserId(userId).fetch(); + data = data.map(function(item) { + if (item._room) { + return item._room; + } + console.log('Empty Room for Subscription', item); + return {}; + }); + data = data.filter(item => item._updatedAt > _updatedAt); + return this.arrayToCursor(this.processQueryOptionsOnResult(data, options)); + } + + let ids = RocketChat.models.Subscriptions.findByUserId(userId, {fields: {rid: 1}}).fetch(); + ids = ids.map(item => item.rid); + + const query = { + _id: { + $in: ids + }, + _updatedAt: { + $gt: _updatedAt + } + }; + + return this.find(query, options); + } + + findByNameContaining(name, options) { + const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i'); + + const query = { + $or: [ + {name: nameRegex}, + { + t: 'd', + usernames: nameRegex + } + ] + }; + + return this.find(query, options); + } + + findByNameContainingTypesWithUsername(name, types, options) { + const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i'); + + const $or = []; + for (const type of Array.from(types)) { + const obj = {name: nameRegex, t: type.type}; + if (type.username != null) { + obj.usernames = type.username; + } + if (type.ids != null) { + obj._id = {$in: type.ids}; + } + $or.push(obj); + } + + const query = {$or}; + + return this.find(query, options); + } + + findContainingTypesWithUsername(types, options) { + + const $or = []; + for (const type of Array.from(types)) { + const obj = {t: type.type}; + if (type.username != null) { + obj.usernames = type.username; + } + if (type.ids != null) { + obj._id = {$in: type.ids}; + } + $or.push(obj); + } + + const query = {$or}; + + return this.find(query, options); + } + + findByNameContainingAndTypes(name, types, options) { + const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i'); + + const query = { + t: { + $in: types + }, + $or: [ + {name: nameRegex}, + { + t: 'd', + usernames: nameRegex + } + ] + }; + + return this.find(query, options); + } + + findByNameAndTypeNotContainingUsername(name, type, username, options) { + const query = { + t: type, + name, + usernames: { + $ne: username + } + }; + + return this.find(query, options); + } + + findByNameStartingAndTypes(name, types, options) { + const nameRegex = new RegExp(`^${ s.trim(s.escapeRegExp(name)) }`, 'i'); + + const query = { + t: { + $in: types + }, + $or: [ + {name: nameRegex}, + { + t: 'd', + usernames: nameRegex + } + ] + }; + + return this.find(query, options); + } + + findByDefaultAndTypes(defaultValue, types, options) { + const query = { + default: defaultValue, + t: { + $in: types + } + }; + + return this.find(query, options); + } + + findByTypeContainingUsername(type, username, options) { + const query = { + t: type, + usernames: username + }; + + return this.find(query, options); + } + + findByTypeContainingUsernames(type, username, options) { + const query = { + t: type, + usernames: { $all: [].concat(username) } + }; + + return this.find(query, options); + } + + findByTypesAndNotUserIdContainingUsername(types, userId, username, options) { + const query = { + t: { + $in: types + }, + uid: { + $ne: userId + }, + usernames: username + }; + + return this.find(query, options); + } + + findByContainingUsername(username, options) { + const query = {usernames: username}; + + return this.find(query, options); + } + + findByTypeAndName(type, name, options) { + if (this.useCache) { + return this.cache.findByIndex('t,name', [type, name], options); + } + + const query = { + name, + t: type + }; + + return this.find(query, options); + } + + findByTypeAndNameContainingUsername(type, name, username, options) { + const query = { + name, + t: type, + usernames: username + }; + + return this.find(query, options); + } + + findByTypeAndArchivationState(type, archivationstate, options) { + const query = {t: type}; + + if (archivationstate) { + query.archived = true; + } else { + query.archived = { $ne: true }; + } + + return this.find(query, options); + } + + // UPDATE + addImportIds(_id, importIds) { + importIds = [].concat(importIds); + const query = {_id}; + + const update = { + $addToSet: { + importIds: { + $each: importIds + } + } + }; + + return this.update(query, update); + } + + archiveById(_id) { + const query = {_id}; + + const update = { + $set: { + archived: true + } + }; + + return this.update(query, update); + } + + unarchiveById(_id) { + const query = {_id}; + + const update = { + $set: { + archived: false + } + }; + + return this.update(query, update); + } + + addUsernameById(_id, username, muted) { + const query = {_id}; + + const update = { + $addToSet: { + usernames: username + } + }; + + if (muted) { + update.$addToSet.muted = username; + } + + return this.update(query, update); + } + + addUsernamesById(_id, usernames) { + const query = {_id}; + + const update = { + $addToSet: { + usernames: { + $each: usernames + } + } + }; + + return this.update(query, update); + } + + addUsernameByName(name, username) { + const query = {name}; + + const update = { + $addToSet: { + usernames: username + } + }; + + return this.update(query, update); + } + + removeUsernameById(_id, username) { + const query = {_id}; + + const update = { + $pull: { + usernames: username + } + }; + + return this.update(query, update); + } + + removeUsernamesById(_id, usernames) { + const query = {_id}; + + const update = { + $pull: { + usernames: { + $in: usernames + } + } + }; + + return this.update(query, update); + } + + removeUsernameFromAll(username) { + const query = {usernames: username}; + + const update = { + $pull: { + usernames: username + } + }; + + return this.update(query, update, { multi: true }); + } + + removeUsernameByName(name, username) { + const query = {name}; + + const update = { + $pull: { + usernames: username + } + }; + + return this.update(query, update); + } + + setNameById(_id, name) { + const query = {_id}; + + const update = { + $set: { + name + } + }; + + return this.update(query, update); + } + + incMsgCountById(_id, inc) { + if (inc == null) { inc = 1; } + const query = {_id}; + + const update = { + $inc: { + msgs: inc + } + }; + + return this.update(query, update); + } + + incMsgCountAndSetLastMessageTimestampById(_id, inc, lastMessageTimestamp) { + if (inc == null) { inc = 1; } + const query = {_id}; + + const update = { + $set: { + lm: lastMessageTimestamp + }, + $inc: { + msgs: inc + } + }; + + return this.update(query, update); + } + + replaceUsername(previousUsername, username) { + const query = {usernames: previousUsername}; + + const update = { + $set: { + 'usernames.$': username + } + }; + + return this.update(query, update, { multi: true }); + } + + replaceMutedUsername(previousUsername, username) { + const query = {muted: previousUsername}; + + const update = { + $set: { + 'muted.$': username + } + }; + + return this.update(query, update, { multi: true }); + } + + replaceUsernameOfUserByUserId(userId, username) { + const query = {'u._id': userId}; + + const update = { + $set: { + 'u.username': username + } + }; + + return this.update(query, update, { multi: true }); + } + + setJoinCodeById(_id, joinCode) { + let update; + const query = {_id}; + + if ((joinCode != null ? joinCode.trim() : undefined) !== '') { + update = { + $set: { + joinCodeRequired: true, + joinCode + } + }; + } else { + update = { + $set: { + joinCodeRequired: false + }, + $unset: { + joinCode: 1 + } + }; + } + + return this.update(query, update); + } + + setUserById(_id, user) { + const query = {_id}; + + const update = { + $set: { + u: { + _id: user._id, + username: user.username + } + } + }; + + return this.update(query, update); + } + + setTypeById(_id, type) { + const query = {_id}; + const update = { + $set: { + t: type + } + }; + if (type === 'p') { + update.$unset = {default: ''}; + } + + return this.update(query, update); + } + + setTopicById(_id, topic) { + const query = {_id}; + + const update = { + $set: { + topic + } + }; + + return this.update(query, update); + } + + setAnnouncementById(_id, announcement) { + const query = {_id}; + + const update = { + $set: { + announcement + } + }; + + return this.update(query, update); + } + + muteUsernameByRoomId(_id, username) { + const query = {_id}; + + const update = { + $addToSet: { + muted: username + } + }; + + return this.update(query, update); + } + + unmuteUsernameByRoomId(_id, username) { + const query = {_id}; + + const update = { + $pull: { + muted: username + } + }; + + return this.update(query, update); + } + + saveDefaultById(_id, defaultValue) { + const query = {_id}; + + const update = { + $set: { + default: defaultValue === 'true' + } + }; + + return this.update(query, update); + } + + setTopicAndTagsById(_id, topic, tags) { + const setData = {}; + const unsetData = {}; + + if (topic != null) { + if (!_.isEmpty(s.trim(topic))) { + setData.topic = s.trim(topic); + } else { + unsetData.topic = 1; + } + } + + if (tags != null) { + if (!_.isEmpty(s.trim(tags))) { + setData.tags = s.trim(tags).split(',').map(tag => s.trim(tag)); + } else { + unsetData.tags = 1; + } + } + + const update = {}; + + if (!_.isEmpty(setData)) { + update.$set = setData; + } + + if (!_.isEmpty(unsetData)) { + update.$unset = unsetData; + } + + if (_.isEmpty(update)) { + return; + } + + return this.update({ _id }, update); + } + + // INSERT + createWithTypeNameUserAndUsernames(type, name, user, usernames, extraData) { + const room = { + name, + t: type, + usernames, + msgs: 0, + u: { + _id: user._id, + username: user.username + } + }; + + _.extend(room, extraData); + + room._id = this.insert(room); + return room; + } + + createWithIdTypeAndName(_id, type, name, extraData) { + const room = { + _id, + ts: new Date(), + t: type, + name, + usernames: [], + msgs: 0 + }; + + _.extend(room, extraData); + + this.insert(room); + return room; + } + + + // REMOVE + removeById(_id) { + const query = {_id}; + + return this.remove(query); + } + + removeByTypeContainingUsername(type, username) { + const query = { + t: type, + usernames: username + }; + + return this.remove(query); + } +} + +RocketChat.models.Rooms = new ModelRooms('room', true); diff --git a/packages/rocketchat-lib/server/models/Settings.coffee b/packages/rocketchat-lib/server/models/Settings.coffee deleted file mode 100644 index ce0d805be06fba3013292ad68fbb7877d4fba131..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/models/Settings.coffee +++ /dev/null @@ -1,144 +0,0 @@ -class ModelSettings extends RocketChat.models._Base - constructor: -> - super(arguments...) - - @tryEnsureIndex { 'blocked': 1 }, { sparse: 1 } - @tryEnsureIndex { 'hidden': 1 }, { sparse: 1 } - - # FIND - findById: (_id) -> - query = - _id: _id - - return @find query - - findOneNotHiddenById: (_id) -> - query = - _id: _id - hidden: { $ne: true } - - return @findOne query - - findByIds: (_id = []) -> - _id = [].concat _id - - query = - _id: - $in: _id - - return @find query - - findByRole: (role, options) -> - query = - role: role - - return @find query, options - - findPublic: (options) -> - query = - public: true - - return @find query, options - - findNotHiddenPublic: (ids = [])-> - filter = - hidden: { $ne: true } - public: true - - if ids.length > 0 - filter._id = - $in: ids - - return @find filter, { fields: _id: 1, value: 1 } - - findNotHiddenPublicUpdatedAfter: (updatedAt) -> - filter = - hidden: { $ne: true } - public: true - _updatedAt: - $gt: updatedAt - - return @find filter, { fields: _id: 1, value: 1 } - - findNotHiddenPrivate: -> - return @find { - hidden: { $ne: true } - public: { $ne: true } - } - - findNotHidden: (options) -> - return @find { hidden: { $ne: true } }, options - - findNotHiddenUpdatedAfter: (updatedAt)-> - return @find { - hidden: { $ne: true } - _updatedAt: - $gt: updatedAt - } - - # UPDATE - updateValueById: (_id, value) -> - query = - blocked: { $ne: true } - value: { $ne: value } - _id: _id - - update = - $set: - value: value - - return @update query, update - - updateValueAndEditorById: (_id, value, editor) -> - query = - blocked: { $ne: true } - value: { $ne: value } - _id: _id - - update = - $set: - value: value - editor: editor - - return @update query, update - - updateValueNotHiddenById: (_id, value) -> - query = - _id: _id - hidden: { $ne: true } - blocked: { $ne: true } - - update = - $set: - value: value - - return @update query, update - - updateOptionsById: (_id, options) -> - query = - blocked: { $ne: true } - _id: _id - - update = - $set: options - - return @update query, update - - # INSERT - createWithIdAndValue: (_id, value) -> - record = - _id: _id - value: value - _createdAt: new Date - - return @insert record - - # REMOVE - removeById: (_id) -> - query = - blocked: { $ne: true } - _id: _id - - return @remove query - -RocketChat.models.Settings = new ModelSettings('settings', true) diff --git a/packages/rocketchat-lib/server/models/Settings.js b/packages/rocketchat-lib/server/models/Settings.js new file mode 100644 index 0000000000000000000000000000000000000000..7da29bee37fafcb3cf00e99b8e4cd1cf2dadf580 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Settings.js @@ -0,0 +1,178 @@ +class ModelSettings extends RocketChat.models._Base { + constructor() { + super(...arguments); + + this.tryEnsureIndex({ 'blocked': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'hidden': 1 }, { sparse: 1 }); + } + + // FIND + findById(_id) { + const query = {_id}; + + return this.find(query); + } + + findOneNotHiddenById(_id) { + const query = { + _id, + hidden: { $ne: true } + }; + + return this.findOne(query); + } + + findByIds(_id = []) { + _id = [].concat(_id); + + const query = { + _id: { + $in: _id + } + }; + + return this.find(query); + } + + findByRole(role, options) { + const query = {role}; + + return this.find(query, options); + } + + findPublic(options) { + const query = {public: true}; + + return this.find(query, options); + } + + findNotHiddenPublic(ids = []) { + const filter = { + hidden: { $ne: true }, + public: true + }; + + if (ids.length > 0) { + filter._id = + {$in: ids}; + } + + return this.find(filter, { fields: {_id: 1, value: 1} }); + } + + findNotHiddenPublicUpdatedAfter(updatedAt) { + const filter = { + hidden: { $ne: true }, + public: true, + _updatedAt: { + $gt: updatedAt + } + }; + + return this.find(filter, { fields: {_id: 1, value: 1} }); + } + + findNotHiddenPrivate() { + return this.find({ + hidden: { $ne: true }, + public: { $ne: true } + }); + } + + findNotHidden(options) { + return this.find({ hidden: { $ne: true } }, options); + } + + findNotHiddenUpdatedAfter(updatedAt) { + return this.find({ + hidden: { $ne: true }, + _updatedAt: { + $gt: updatedAt + } + }); + } + + // UPDATE + updateValueById(_id, value) { + const query = { + blocked: { $ne: true }, + value: { $ne: value }, + _id + }; + + const update = { + $set: { + value + } + }; + + return this.update(query, update); + } + + updateValueAndEditorById(_id, value, editor) { + const query = { + blocked: { $ne: true }, + value: { $ne: value }, + _id + }; + + const update = { + $set: { + value, + editor + } + }; + + return this.update(query, update); + } + + updateValueNotHiddenById(_id, value) { + const query = { + _id, + hidden: { $ne: true }, + blocked: { $ne: true } + }; + + const update = { + $set: { + value + } + }; + + return this.update(query, update); + } + + updateOptionsById(_id, options) { + const query = { + blocked: { $ne: true }, + _id + }; + + const update = {$set: options}; + + return this.update(query, update); + } + + // INSERT + createWithIdAndValue(_id, value) { + const record = { + _id, + value, + _createdAt: new Date + }; + + return this.insert(record); + } + + // REMOVE + removeById(_id) { + const query = { + blocked: { $ne: true }, + _id + }; + + return this.remove(query); + } +} + +RocketChat.models.Settings = new ModelSettings('settings', true); diff --git a/packages/rocketchat-lib/server/models/Subscriptions.coffee b/packages/rocketchat-lib/server/models/Subscriptions.coffee deleted file mode 100644 index 95f12bff51ff2b341bc873211297db331cc5f31e..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/models/Subscriptions.coffee +++ /dev/null @@ -1,438 +0,0 @@ -class ModelSubscriptions extends RocketChat.models._Base - constructor: -> - super(arguments...) - - @tryEnsureIndex { 'rid': 1, 'u._id': 1 }, { unique: 1 } - @tryEnsureIndex { 'rid': 1, 'alert': 1, 'u._id': 1 } - @tryEnsureIndex { 'rid': 1, 'roles': 1 } - @tryEnsureIndex { 'u._id': 1, 'name': 1, 't': 1 } - @tryEnsureIndex { 'u._id': 1, 'name': 1, 't': 1, 'code': 1 }, { unique: 1 } - @tryEnsureIndex { 'open': 1 } - @tryEnsureIndex { 'alert': 1 } - @tryEnsureIndex { 'unread': 1 } - @tryEnsureIndex { 'ts': 1 } - @tryEnsureIndex { 'ls': 1 } - @tryEnsureIndex { 'audioNotification': 1 }, { sparse: 1 } - @tryEnsureIndex { 'desktopNotifications': 1 }, { sparse: 1 } - @tryEnsureIndex { 'mobilePushNotifications': 1 }, { sparse: 1 } - @tryEnsureIndex { 'emailNotifications': 1 }, { sparse: 1 } - @tryEnsureIndex { 'autoTranslate': 1 }, { sparse: 1 } - @tryEnsureIndex { 'autoTranslateLanguage': 1 }, { sparse: 1 } - - this.cache.ensureIndex('rid', 'array') - this.cache.ensureIndex('u._id', 'array') - this.cache.ensureIndex('name', 'array') - this.cache.ensureIndex(['rid', 'u._id'], 'unique') - this.cache.ensureIndex(['name', 'u._id'], 'unique') - - - # FIND ONE - findOneByRoomIdAndUserId: (roomId, userId) -> - if this.useCache - return this.cache.findByIndex('rid,u._id', [roomId, userId]).fetch() - query = - rid: roomId - "u._id": userId - - return @findOne query - - findOneByRoomNameAndUserId: (roomName, userId) -> - if this.useCache - return this.cache.findByIndex('name,u._id', [roomName, userId]).fetch() - query = - name: roomName - "u._id": userId - - return @findOne query - - # FIND - findByUserId: (userId, options) -> - if this.useCache - return this.cache.findByIndex('u._id', userId, options) - - query = - "u._id": userId - - return @find query, options - - findByUserIdUpdatedAfter: (userId, updatedAt, options) -> - query = - "u._id": userId - _updatedAt: - $gt: updatedAt - - return @find query, options - - # FIND - findByRoomIdAndRoles: (roomId, roles, options) -> - roles = [].concat roles - query = - "rid": roomId - "roles": { $in: roles } - - return @find query, options - - findByType: (types, options) -> - query = - t: - $in: types - - return @find query, options - - findByTypeAndUserId: (type, userId, options) -> - query = - t: type - 'u._id': userId - - return @find query, options - - findByTypeNameAndUserId: (type, name, userId, options) -> - query = - t: type - name: name - 'u._id': userId - - return @find query, options - - findByRoomId: (roomId, options) -> - if this.useCache - return this.cache.findByIndex('rid', roomId, options) - - query = - rid: roomId - - return @find query, options - - findByRoomIdAndNotUserId: (roomId, userId, options) -> - query = - rid: roomId - 'u._id': - $ne: userId - - return @find query, options - - getLastSeen: (options = {}) -> - query = { ls: { $exists: 1 } } - options.sort = { ls: -1 } - options.limit = 1 - - return @find(query, options)?.fetch?()?[0]?.ls - - findByRoomIdAndUserIds: (roomId, userIds) -> - query = - rid: roomId - 'u._id': - $in: userIds - - return @find query - - # UPDATE - archiveByRoomId: (roomId) -> - query = - rid: roomId - - update = - $set: - alert: false - open: false - archived: true - - return @update query, update, { multi: true } - - unarchiveByRoomId: (roomId) -> - query = - rid: roomId - - update = - $set: - alert: false - open: true - archived: false - - return @update query, update, { multi: true } - - hideByRoomIdAndUserId: (roomId, userId) -> - query = - rid: roomId - 'u._id': userId - - update = - $set: - alert: false - open: false - - return @update query, update - - openByRoomIdAndUserId: (roomId, userId) -> - query = - rid: roomId - 'u._id': userId - - update = - $set: - open: true - - return @update query, update - - setAsReadByRoomIdAndUserId: (roomId, userId) -> - query = - rid: roomId - 'u._id': userId - - update = - $set: - open: true - alert: false - unread: 0 - ls: new Date - - return @update query, update - - setAsUnreadByRoomIdAndUserId: (roomId, userId, firstMessageUnreadTimestamp) -> - query = - rid: roomId - 'u._id': userId - - update = - $set: - open: true - alert: true - ls: firstMessageUnreadTimestamp - - return @update query, update - - setFavoriteByRoomIdAndUserId: (roomId, userId, favorite=true) -> - query = - rid: roomId - 'u._id': userId - - update = - $set: - f: favorite - - return @update query, update - - updateNameAndAlertByRoomId: (roomId, name) -> - query = - rid: roomId - - update = - $set: - name: name - alert: true - - return @update query, update, { multi: true } - - updateNameByRoomId: (roomId, name) -> - query = - rid: roomId - - update = - $set: - name: name - - return @update query, update, { multi: true } - - setUserUsernameByUserId: (userId, username) -> - query = - "u._id": userId - - update = - $set: - "u.username": username - - return @update query, update, { multi: true } - - setNameForDirectRoomsWithOldName: (oldName, name) -> - query = - name: oldName - t: "d" - - update = - $set: - name: name - - return @update query, update, { multi: true } - - incUnreadOfDirectForRoomIdExcludingUserId: (roomId, userId, inc=1) -> - query = - rid: roomId - t: 'd' - 'u._id': - $ne: userId - - update = - $set: - alert: true - open: true - $inc: - unread: inc - - return @update query, update, { multi: true } - - incUnreadForRoomIdExcludingUserId: (roomId, userId, inc=1) -> - query = - rid: roomId - 'u._id': - $ne: userId - - update = - $set: - alert: true - open: true - $inc: - unread: inc - - return @update query, update, { multi: true } - - incUnreadForRoomIdAndUserIds: (roomId, userIds, inc=1) -> - query = - rid: roomId - 'u._id': - $in: userIds - - update = - $set: - alert: true - open: true - $inc: - unread: inc - - return @update query, update, { multi: true } - - setAlertForRoomIdExcludingUserId: (roomId, userId) -> - query = - rid: roomId - 'u._id': - $ne: userId - $or: [ - { alert: { $ne: true } } - { open: { $ne: true } } - ] - - update = - $set: - alert: true - open: true - - return @update query, update, { multi: true } - - setBlockedByRoomId: (rid, blocked, blocker) -> - query = - rid: rid - 'u._id': blocked - - update = - $set: - blocked: true - - query2 = - rid: rid - 'u._id': blocker - - update2 = - $set: - blocker: true - - return @update(query, update) and @update(query2, update2) - - unsetBlockedByRoomId: (rid, blocked, blocker) -> - query = - rid: rid - 'u._id': blocked - - update = - $unset: - blocked: 1 - - query2 = - rid: rid - 'u._id': blocker - - update2 = - $unset: - blocker: 1 - - return @update(query, update) and @update(query2, update2) - - updateTypeByRoomId: (roomId, type) -> - query = - rid: roomId - - update = - $set: - t: type - - return @update query, update, { multi: true } - - addRoleById: (_id, role) -> - query = - _id: _id - - update = - $addToSet: - roles: role - - return @update query, update - - removeRoleById: (_id, role) -> - query = - _id: _id - - update = - $pull: - roles: role - - return @update query, update - - setArchivedByUsername: (username, archived) -> - query = - t: 'd' - name: username - - update = - $set: - archived: archived - - return @update query, update, { multi: true } - - # INSERT - createWithRoomAndUser: (room, user, extraData) -> - subscription = - open: false - alert: false - unread: 0 - ts: room.ts - rid: room._id - name: room.name - t: room.t - u: - _id: user._id - username: user.username - - _.extend subscription, extraData - - return @insert subscription - - - # REMOVE - removeByUserId: (userId) -> - query = - "u._id": userId - - return @remove query - - removeByRoomId: (roomId) -> - query = - rid: roomId - - return @remove query - - removeByRoomIdAndUserId: (roomId, userId) -> - query = - rid: roomId - "u._id": userId - - return @remove query - -RocketChat.models.Subscriptions = new ModelSubscriptions('subscription', true) diff --git a/packages/rocketchat-lib/server/models/Subscriptions.js b/packages/rocketchat-lib/server/models/Subscriptions.js new file mode 100644 index 0000000000000000000000000000000000000000..67986040e02bef61521b0f90767550f23608f759 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Subscriptions.js @@ -0,0 +1,570 @@ +class ModelSubscriptions extends RocketChat.models._Base { + constructor() { + super(...arguments); + + this.tryEnsureIndex({ 'rid': 1, 'u._id': 1 }, { unique: 1 }); + this.tryEnsureIndex({ 'rid': 1, 'alert': 1, 'u._id': 1 }); + this.tryEnsureIndex({ 'rid': 1, 'roles': 1 }); + this.tryEnsureIndex({ 'u._id': 1, 'name': 1, 't': 1 }); + this.tryEnsureIndex({ 'u._id': 1, 'name': 1, 't': 1, 'code': 1 }, { unique: 1 }); + this.tryEnsureIndex({ 'open': 1 }); + this.tryEnsureIndex({ 'alert': 1 }); + this.tryEnsureIndex({ 'unread': 1 }); + this.tryEnsureIndex({ 'ts': 1 }); + this.tryEnsureIndex({ 'ls': 1 }); + this.tryEnsureIndex({ 'audioNotification': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'desktopNotifications': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'mobilePushNotifications': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'emailNotifications': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'autoTranslate': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'autoTranslateLanguage': 1 }, { sparse: 1 }); + + this.cache.ensureIndex('rid', 'array'); + this.cache.ensureIndex('u._id', 'array'); + this.cache.ensureIndex('name', 'array'); + this.cache.ensureIndex(['rid', 'u._id'], 'unique'); + this.cache.ensureIndex(['name', 'u._id'], 'unique'); + } + + + // FIND ONE + findOneByRoomIdAndUserId(roomId, userId) { + if (this.useCache) { + return this.cache.findByIndex('rid,u._id', [roomId, userId]).fetch(); + } + const query = { + rid: roomId, + 'u._id': userId + }; + + return this.findOne(query); + } + + findOneByRoomNameAndUserId(roomName, userId) { + if (this.useCache) { + return this.cache.findByIndex('name,u._id', [roomName, userId]).fetch(); + } + const query = { + name: roomName, + 'u._id': userId + }; + + return this.findOne(query); + } + + // FIND + findByUserId(userId, options) { + if (this.useCache) { + return this.cache.findByIndex('u._id', userId, options); + } + + const query = + {'u._id': userId}; + + return this.find(query, options); + } + + findByUserIdUpdatedAfter(userId, updatedAt, options) { + const query = { + 'u._id': userId, + _updatedAt: { + $gt: updatedAt + } + }; + + return this.find(query, options); + } + + // FIND + findByRoomIdAndRoles(roomId, roles, options) { + roles = [].concat(roles); + const query = { + 'rid': roomId, + 'roles': { $in: roles } + }; + + return this.find(query, options); + } + + findByType(types, options) { + const query = { + t: { + $in: types + } + }; + + return this.find(query, options); + } + + findByTypeAndUserId(type, userId, options) { + const query = { + t: type, + 'u._id': userId + }; + + return this.find(query, options); + } + + findByTypeNameAndUserId(type, name, userId, options) { + const query = { + t: type, + name, + 'u._id': userId + }; + + return this.find(query, options); + } + + findByRoomId(roomId, options) { + if (this.useCache) { + return this.cache.findByIndex('rid', roomId, options); + } + + const query = + {rid: roomId}; + + return this.find(query, options); + } + + findByRoomIdAndNotUserId(roomId, userId, options) { + const query = { + rid: roomId, + 'u._id': { + $ne: userId + } + }; + + return this.find(query, options); + } + + getLastSeen(options) { + if (options == null) { options = {}; } + const query = { ls: { $exists: 1 } }; + options.sort = { ls: -1 }; + options.limit = 1; + const [subscription] = this.find(query, options).fetch(); + return subscription && subscription.ls; + } + + findByRoomIdAndUserIds(roomId, userIds) { + const query = { + rid: roomId, + 'u._id': { + $in: userIds + } + }; + + return this.find(query); + } + + // UPDATE + archiveByRoomId(roomId) { + const query = + {rid: roomId}; + + const update = { + $set: { + alert: false, + open: false, + archived: true + } + }; + + return this.update(query, update, { multi: true }); + } + + unarchiveByRoomId(roomId) { + const query = + {rid: roomId}; + + const update = { + $set: { + alert: false, + open: true, + archived: false + } + }; + + return this.update(query, update, { multi: true }); + } + + hideByRoomIdAndUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + alert: false, + open: false + } + }; + + return this.update(query, update); + } + + openByRoomIdAndUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + open: true + } + }; + + return this.update(query, update); + } + + setAsReadByRoomIdAndUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + open: true, + alert: false, + unread: 0, + ls: new Date + } + }; + + return this.update(query, update); + } + + setAsUnreadByRoomIdAndUserId(roomId, userId, firstMessageUnreadTimestamp) { + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + open: true, + alert: true, + ls: firstMessageUnreadTimestamp + } + }; + + return this.update(query, update); + } + + setFavoriteByRoomIdAndUserId(roomId, userId, favorite) { + if (favorite == null) { favorite = true; } + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + f: favorite + } + }; + + return this.update(query, update); + } + + updateNameAndAlertByRoomId(roomId, name) { + const query = + {rid: roomId}; + + const update = { + $set: { + name, + alert: true + } + }; + + return this.update(query, update, { multi: true }); + } + + updateNameByRoomId(roomId, name) { + const query = + {rid: roomId}; + + const update = { + $set: { + name + } + }; + + return this.update(query, update, { multi: true }); + } + + setUserUsernameByUserId(userId, username) { + const query = + {'u._id': userId}; + + const update = { + $set: { + 'u.username': username + } + }; + + return this.update(query, update, { multi: true }); + } + + setNameForDirectRoomsWithOldName(oldName, name) { + const query = { + name: oldName, + t: 'd' + }; + + const update = { + $set: { + name + } + }; + + return this.update(query, update, { multi: true }); + } + + incUnreadOfDirectForRoomIdExcludingUserId(roomId, userId, inc) { + if (inc == null) { inc = 1; } + const query = { + rid: roomId, + t: 'd', + 'u._id': { + $ne: userId + } + }; + + const update = { + $set: { + alert: true, + open: true + }, + $inc: { + unread: inc + } + }; + + return this.update(query, update, { multi: true }); + } + + incUnreadForRoomIdExcludingUserId(roomId, userId, inc) { + if (inc == null) { inc = 1; } + const query = { + rid: roomId, + 'u._id': { + $ne: userId + } + }; + + const update = { + $set: { + alert: true, + open: true + }, + $inc: { + unread: inc + } + }; + + return this.update(query, update, { multi: true }); + } + + incUnreadForRoomIdAndUserIds(roomId, userIds, inc) { + if (inc == null) { inc = 1; } + const query = { + rid: roomId, + 'u._id': { + $in: userIds + } + }; + + const update = { + $set: { + alert: true, + open: true + }, + $inc: { + unread: inc + } + }; + + return this.update(query, update, { multi: true }); + } + + setAlertForRoomIdExcludingUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': { + $ne: userId + }, + $or: [ + { alert: { $ne: true } }, + { open: { $ne: true } } + ] + }; + + const update = { + $set: { + alert: true, + open: true + } + }; + + return this.update(query, update, { multi: true }); + } + + setBlockedByRoomId(rid, blocked, blocker) { + const query = { + rid, + 'u._id': blocked + }; + + const update = { + $set: { + blocked: true + } + }; + + const query2 = { + rid, + 'u._id': blocker + }; + + const update2 = { + $set: { + blocker: true + } + }; + + return this.update(query, update) && this.update(query2, update2); + } + + unsetBlockedByRoomId(rid, blocked, blocker) { + const query = { + rid, + 'u._id': blocked + }; + + const update = { + $unset: { + blocked: 1 + } + }; + + const query2 = { + rid, + 'u._id': blocker + }; + + const update2 = { + $unset: { + blocker: 1 + } + }; + + return this.update(query, update) && this.update(query2, update2); + } + + updateTypeByRoomId(roomId, type) { + const query = + {rid: roomId}; + + const update = { + $set: { + t: type + } + }; + + return this.update(query, update, { multi: true }); + } + + addRoleById(_id, role) { + const query = + {_id}; + + const update = { + $addToSet: { + roles: role + } + }; + + return this.update(query, update); + } + + removeRoleById(_id, role) { + const query = + {_id}; + + const update = { + $pull: { + roles: role + } + }; + + return this.update(query, update); + } + + setArchivedByUsername(username, archived) { + const query = { + t: 'd', + name: username + }; + + const update = { + $set: { + archived + } + }; + + return this.update(query, update, { multi: true }); + } + + // INSERT + createWithRoomAndUser(room, user, extraData) { + const subscription = { + open: false, + alert: false, + unread: 0, + ts: room.ts, + rid: room._id, + name: room.name, + t: room.t, + u: { + _id: user._id, + username: user.username + } + }; + + _.extend(subscription, extraData); + + return this.insert(subscription); + } + + + // REMOVE + removeByUserId(userId) { + const query = + {'u._id': userId}; + + return this.remove(query); + } + + removeByRoomId(roomId) { + const query = + {rid: roomId}; + + return this.remove(query); + } + + removeByRoomIdAndUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': userId + }; + + return this.remove(query); + } +} + +RocketChat.models.Subscriptions = new ModelSubscriptions('subscription', true); diff --git a/packages/rocketchat-lib/server/models/Uploads.coffee b/packages/rocketchat-lib/server/models/Uploads.coffee deleted file mode 100644 index 53dd79256891953202f59590c6f88784485a227a..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/models/Uploads.coffee +++ /dev/null @@ -1,73 +0,0 @@ -RocketChat.models.Uploads = new class extends RocketChat.models._Base - constructor: -> - super('uploads') - - @tryEnsureIndex { 'rid': 1 } - @tryEnsureIndex { 'uploadedAt': 1 } - - findNotHiddenFilesOfRoom: (roomId, limit) -> - fileQuery = - rid: roomId - complete: true - uploading: false - _hidden: - $ne: true - - fileOptions = - limit: limit - sort: - uploadedAt: -1 - fields: - _id: 1 - userId: 1 - rid: 1 - name: 1 - description: 1 - type: 1 - url: 1 - uploadedAt: 1 - - return @find fileQuery, fileOptions - - insertFileInit: (roomId, userId, store, file, extra) -> - fileData = - rid: roomId - userId: userId - store: store - complete: false - uploading: true - progress: 0 - extension: s.strRightBack(file.name, '.') - uploadedAt: new Date() - - _.extend(fileData, file, extra); - - if @model.direct?.insert? - file = @model.direct.insert fileData - else - file = @insert fileData - - return file - - updateFileComplete: (fileId, userId, file) -> - if not fileId - return - - filter = - _id: fileId - userId: userId - - update = - $set: - complete: true - uploading: false - progress: 1 - - update.$set = _.extend file, update.$set - - if @model.direct?.insert? - result = @model.direct.update filter, update - else - result = @update filter, update - - return result diff --git a/packages/rocketchat-lib/server/models/Uploads.js b/packages/rocketchat-lib/server/models/Uploads.js new file mode 100644 index 0000000000000000000000000000000000000000..252d48fb8782fd5a38169ec8d0a10db91664d158 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Uploads.js @@ -0,0 +1,91 @@ +RocketChat.models.Uploads = new class extends RocketChat.models._Base { + constructor() { + super('uploads'); + + this.tryEnsureIndex({ 'rid': 1 }); + this.tryEnsureIndex({ 'uploadedAt': 1 }); + } + + findNotHiddenFilesOfRoom(roomId, limit) { + const fileQuery = { + rid: roomId, + complete: true, + uploading: false, + _hidden: { + $ne: true + } + }; + + const fileOptions = { + limit, + sort: { + uploadedAt: -1 + }, + fields: { + _id: 1, + userId: 1, + rid: 1, + name: 1, + description: 1, + type: 1, + url: 1, + uploadedAt: 1 + } + }; + + return this.find(fileQuery, fileOptions); + } + + insertFileInit(roomId, userId, store, file, extra) { + const fileData = { + rid: roomId, + userId, + store, + complete: false, + uploading: true, + progress: 0, + extension: s.strRightBack(file.name, '.'), + uploadedAt: new Date() + }; + + _.extend(fileData, file, extra); + + if ((this.model.direct != null ? this.model.direct.insert : undefined) != null) { + file = this.model.direct.insert(fileData); + } else { + file = this.insert(fileData); + } + + return file; + } + + updateFileComplete(fileId, userId, file) { + let result; + if (!fileId) { + return; + } + + const filter = { + _id: fileId, + userId + }; + + const update = { + $set: { + complete: true, + uploading: false, + progress: 1 + } + }; + + update.$set = _.extend(file, update.$set); + + if ((this.model.direct != null ? this.model.direct.insert : undefined) != null) { + result = this.model.direct.update(filter, update); + } else { + result = this.update(filter, update); + } + + return result; + } +}; diff --git a/packages/rocketchat-lib/server/models/Users.coffee b/packages/rocketchat-lib/server/models/Users.coffee deleted file mode 100644 index 83519874f9fa19860fbe18696af350ca3a88e233..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/models/Users.coffee +++ /dev/null @@ -1,428 +0,0 @@ -class ModelUsers extends RocketChat.models._Base - constructor: -> - super(arguments...) - - @tryEnsureIndex { 'roles': 1 }, { sparse: 1 } - @tryEnsureIndex { 'name': 1 } - @tryEnsureIndex { 'lastLogin': 1 } - @tryEnsureIndex { 'status': 1 } - @tryEnsureIndex { 'active': 1 }, { sparse: 1 } - @tryEnsureIndex { 'statusConnection': 1 }, { sparse: 1 } - @tryEnsureIndex { 'type': 1 } - - this.cache.ensureIndex('username', 'unique') - - findOneByImportId: (_id, options) -> - return @findOne { importIds: _id }, options - - findOneByUsername: (username, options) -> - query = - username: username - - return @findOne query, options - - findOneByEmailAddress: (emailAddress, options) -> - query = - 'emails.address': new RegExp("^" + s.escapeRegExp(emailAddress) + "$", 'i') - - return @findOne query, options - - findOneAdmin: (admin, options) -> - query = - admin: admin - - return @findOne query, options - - findOneByIdAndLoginToken: (_id, token, options) -> - query = - _id: _id - 'services.resume.loginTokens.hashedToken' : Accounts._hashLoginToken(token) - - return @findOne query, options - - - # FIND - findById: (userId) -> - query = - _id: userId - - return @find query - - findUsersNotOffline: (options) -> - query = - username: - $exists: 1 - status: - $in: ['online', 'away', 'busy'] - - return @find query, options - - - findByUsername: (username, options) -> - query = - username: username - - return @find query, options - - findUsersByUsernamesWithHighlights: (usernames, options) -> - if this.useCache - result = - fetch: () -> - return RocketChat.models.Users.getDynamicView('highlights').data().filter (record) -> - return usernames.indexOf(record.username) > -1 - count: () -> - return result.fetch().length - forEach: (fn) -> - return result.fetch().forEach(fn) - return result - - query = - username: { $in: usernames } - 'settings.preferences.highlights.0': - $exists: true - - return @find query, options - - findActiveByUsernameOrNameRegexWithExceptions: (searchTerm, exceptions = [], options = {}) -> - if not _.isArray exceptions - exceptions = [ exceptions ] - - termRegex = new RegExp s.escapeRegExp(searchTerm), 'i' - query = { - $or: [{ - username: termRegex - }, { - name: termRegex - }], - active: true, - type: { - $in: ['user', 'bot'] - }, - $and: [{ - username: { - $exists: true - } - }, { - username: { - $nin: exceptions - } - }] - } - - return @find query, options - - findByActiveUsersExcept: (searchTerm, exceptions = [], options = {}) -> - if not _.isArray exceptions - exceptions = [ exceptions ] - - termRegex = new RegExp s.escapeRegExp(searchTerm), 'i' - query = - $and: [ - { - active: true - $or: [ - { - username: termRegex - } - { - name: termRegex - } - ] - } - { - username: { $exists: true, $nin: exceptions } - } - ] - - return @find query, options - - findUsersByNameOrUsername: (nameOrUsername, options) -> - query = - username: - $exists: 1 - - $or: [ - {name: nameOrUsername} - {username: nameOrUsername} - ] - - type: - $in: ['user'] - - return @find query, options - - findByUsernameNameOrEmailAddress: (usernameNameOrEmailAddress, options) -> - query = - $or: [ - {name: usernameNameOrEmailAddress} - {username: usernameNameOrEmailAddress} - {'emails.address': usernameNameOrEmailAddress} - ] - type: - $in: ['user', 'bot'] - - return @find query, options - - findLDAPUsers: (options) -> - query = - ldap: true - - return @find query, options - - findCrowdUsers: (options) -> - query = - crowd: true - - return @find query, options - - getLastLogin: (options = {}) -> - query = { lastLogin: { $exists: 1 } } - options.sort = { lastLogin: -1 } - options.limit = 1 - - return @find(query, options)?.fetch?()?[0]?.lastLogin - - findUsersByUsernames: (usernames, options) -> - query = - username: - $in: usernames - - return @find query, options - - # UPDATE - addImportIds: (_id, importIds) -> - importIds = [].concat(importIds) - - query = - _id: _id - - update = - $addToSet: - importIds: - $each: importIds - - return @update query, update - - updateLastLoginById: (_id) -> - update = - $set: - lastLogin: new Date - - return @update _id, update - - setServiceId: (_id, serviceName, serviceId) -> - update = - $set: {} - - serviceIdKey = "services.#{serviceName}.id" - update.$set[serviceIdKey] = serviceId - - return @update _id, update - - setUsername: (_id, username) -> - update = - $set: username: username - - return @update _id, update - - setEmail: (_id, email) -> - update = - $set: - emails: [ - address: email - verified: false - ] - - return @update _id, update - - setEmailVerified: (_id, email) -> - query = - _id: _id - emails: - $elemMatch: - address: email - verified: false - - update = - $set: - 'emails.$.verified': true - - return @update query, update - - setName: (_id, name) -> - update = - $set: - name: name - - return @update _id, update - - setCustomFields: (_id, fields) -> - values = {} - for key, value of fields - values["customFields.#{key}"] = value - - update = - $set: values - - return @update _id, update - - setAvatarOrigin: (_id, origin) -> - update = - $set: - avatarOrigin: origin - - return @update _id, update - - unsetAvatarOrigin: (_id) -> - update = - $unset: - avatarOrigin: 1 - - return @update _id, update - - setUserActive: (_id, active=true) -> - update = - $set: - active: active - - return @update _id, update - - setAllUsersActive: (active) -> - update = - $set: - active: active - - return @update {}, update, { multi: true } - - unsetLoginTokens: (_id) -> - update = - $set: - "services.resume.loginTokens" : [] - - return @update _id, update - - unsetRequirePasswordChange: (_id) -> - update = - $unset: - "requirePasswordChange" : true - "requirePasswordChangeReason" : true - - return @update _id, update - - resetPasswordAndSetRequirePasswordChange: (_id, requirePasswordChange, requirePasswordChangeReason) -> - update = - $unset: - "services.password": 1 - $set: - "requirePasswordChange" : requirePasswordChange, - "requirePasswordChangeReason": requirePasswordChangeReason - - return @update _id, update - - setLanguage: (_id, language) -> - update = - $set: - language: language - - return @update _id, update - - setProfile: (_id, profile) -> - update = - $set: - "settings.profile": profile - - return @update _id, update - - setPreferences: (_id, preferences) -> - update = - $set: - "settings.preferences": preferences - - return @update _id, update - - setUtcOffset: (_id, utcOffset) -> - query = - _id: _id - utcOffset: - $ne: utcOffset - - update = - $set: - utcOffset: utcOffset - - return @update query, update - - saveUserById: (_id, data) -> - setData = {} - unsetData = {} - - if data.name? - if not _.isEmpty(s.trim(data.name)) - setData.name = s.trim(data.name) - else - unsetData.name = 1 - - if data.email? - if not _.isEmpty(s.trim(data.email)) - setData.emails = [ - address: s.trim(data.email) - ] - else - unsetData.emails = 1 - - if data.phone? - if not _.isEmpty(s.trim(data.phone)) - setData.phone = [ - phoneNumber: s.trim(data.phone) - ] - else - unsetData.phone = 1 - - update = {} - - if not _.isEmpty setData - update.$set = setData - - if not _.isEmpty unsetData - update.$unset = unsetData - - if _.isEmpty update - return true - - return @update { _id: _id }, update - - # INSERT - create: (data) -> - user = - createdAt: new Date - avatarOrigin: 'none' - - _.extend user, data - - return @insert user - - - # REMOVE - removeById: (_id) -> - return @remove _id - - ### - Find users to send a message by email if: - - he is not online - - has a verified email - - has not disabled email notifications - - `active` is equal to true (false means they were deactivated and can't login) - ### - getUsersToSendOfflineEmail: (usersIds) -> - query = - _id: - $in: usersIds - active: true - status: 'offline' - statusConnection: - $ne: 'online' - 'emails.verified': true - - return @find query, { fields: { name: 1, username: 1, emails: 1, 'settings.preferences.emailNotificationMode': 1 } } - -RocketChat.models.Users = new ModelUsers(Meteor.users, true) diff --git a/packages/rocketchat-lib/server/models/Users.js b/packages/rocketchat-lib/server/models/Users.js new file mode 100644 index 0000000000000000000000000000000000000000..941416475c6ee9be2099b28b3c4c490e2e306758 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Users.js @@ -0,0 +1,538 @@ +class ModelUsers extends RocketChat.models._Base { + constructor() { + super(...arguments); + + this.tryEnsureIndex({ 'roles': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'name': 1 }); + this.tryEnsureIndex({ 'lastLogin': 1 }); + this.tryEnsureIndex({ 'status': 1 }); + this.tryEnsureIndex({ 'active': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'statusConnection': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'type': 1 }); + + this.cache.ensureIndex('username', 'unique'); + } + + findOneByImportId(_id, options) { + return this.findOne({ importIds: _id }, options); + } + + findOneByUsername(username, options) { + const query = {username}; + + return this.findOne(query, options); + } + + findOneByEmailAddress(emailAddress, options) { + const query = {'emails.address': new RegExp(`^${ s.escapeRegExp(emailAddress) }$`, 'i')}; + + return this.findOne(query, options); + } + + findOneAdmin(admin, options) { + const query = {admin}; + + return this.findOne(query, options); + } + + findOneByIdAndLoginToken(_id, token, options) { + const query = { + _id, + 'services.resume.loginTokens.hashedToken' : Accounts._hashLoginToken(token) + }; + + return this.findOne(query, options); + } + + + // FIND + findById(userId) { + const query = {_id: userId}; + + return this.find(query); + } + + findUsersNotOffline(options) { + const query = { + username: { + $exists: 1 + }, + status: { + $in: ['online', 'away', 'busy'] + } + }; + + return this.find(query, options); + } + + + findByUsername(username, options) { + const query = {username}; + + return this.find(query, options); + } + + findUsersByUsernamesWithHighlights(usernames, options) { + if (this.useCache) { + const result = { + fetch() { + return RocketChat.models.Users.getDynamicView('highlights').data().filter(record => usernames.indexOf(record.username) > -1); + }, + count() { + return result.fetch().length; + }, + forEach(fn) { + return result.fetch().forEach(fn); + } + }; + return result; + } + + const query = { + username: { $in: usernames }, + 'settings.preferences.highlights.0': { + $exists: true + } + }; + + return this.find(query, options); + } + + findActiveByUsernameOrNameRegexWithExceptions(searchTerm, exceptions, options) { + if (exceptions == null) { exceptions = []; } + if (options == null) { options = {}; } + if (!_.isArray(exceptions)) { + exceptions = [ exceptions ]; + } + + const termRegex = new RegExp(s.escapeRegExp(searchTerm), 'i'); + const query = { + $or: [{ + username: termRegex + }, { + name: termRegex + }], + active: true, + type: { + $in: ['user', 'bot'] + }, + $and: [{ + username: { + $exists: true + } + }, { + username: { + $nin: exceptions + } + }] + }; + + return this.find(query, options); + } + + findByActiveUsersExcept(searchTerm, exceptions, options) { + if (exceptions == null) { exceptions = []; } + if (options == null) { options = {}; } + if (!_.isArray(exceptions)) { + exceptions = [ exceptions ]; + } + + const termRegex = new RegExp(s.escapeRegExp(searchTerm), 'i'); + const query = { + $and: [ + { + active: true, + $or: [ + { + username: termRegex + }, + { + name: termRegex + } + ] + }, + { + username: { $exists: true, $nin: exceptions } + } + ] + }; + + return this.find(query, options); + } + + findUsersByNameOrUsername(nameOrUsername, options) { + const query = { + username: { + $exists: 1 + }, + + $or: [ + {name: nameOrUsername}, + {username: nameOrUsername} + ], + + type: { + $in: ['user'] + } + }; + + return this.find(query, options); + } + + findByUsernameNameOrEmailAddress(usernameNameOrEmailAddress, options) { + const query = { + $or: [ + {name: usernameNameOrEmailAddress}, + {username: usernameNameOrEmailAddress}, + {'emails.address': usernameNameOrEmailAddress} + ], + type: { + $in: ['user', 'bot'] + } + }; + + return this.find(query, options); + } + + findLDAPUsers(options) { + const query = {ldap: true}; + + return this.find(query, options); + } + + findCrowdUsers(options) { + const query = {crowd: true}; + + return this.find(query, options); + } + + getLastLogin(options) { + if (options == null) { options = {}; } + const query = { lastLogin: { $exists: 1 } }; + options.sort = { lastLogin: -1 }; + options.limit = 1; + const [user] = this.find(query, options).fetch(); + return user && user.lastLogin; + } + + findUsersByUsernames(usernames, options) { + const query = { + username: { + $in: usernames + } + }; + + return this.find(query, options); + } + + // UPDATE + addImportIds(_id, importIds) { + importIds = [].concat(importIds); + + const query = {_id}; + + const update = { + $addToSet: { + importIds: { + $each: importIds + } + } + }; + + return this.update(query, update); + } + + updateLastLoginById(_id) { + const update = { + $set: { + lastLogin: new Date + } + }; + + return this.update(_id, update); + } + + setServiceId(_id, serviceName, serviceId) { + const update = + {$set: {}}; + + const serviceIdKey = `services.${ serviceName }.id`; + update.$set[serviceIdKey] = serviceId; + + return this.update(_id, update); + } + + setUsername(_id, username) { + const update = + {$set: {username}}; + + return this.update(_id, update); + } + + setEmail(_id, email) { + const update = { + $set: { + emails: [{ + address: email, + verified: false + } + ] + } + }; + + return this.update(_id, update); + } + + setEmailVerified(_id, email) { + const query = { + _id, + emails: { + $elemMatch: { + address: email, + verified: false + } + } + }; + + const update = { + $set: { + 'emails.$.verified': true + } + }; + + return this.update(query, update); + } + + setName(_id, name) { + const update = { + $set: { + name + } + }; + + return this.update(_id, update); + } + + setCustomFields(_id, fields) { + const values = {}; + Object.keys(fields).reduce(key => { + const value = fields[key]; + values[`customFields.${ key }`] = value; + }); + + const update = {$set: values}; + + return this.update(_id, update); + } + + setAvatarOrigin(_id, origin) { + const update = { + $set: { + avatarOrigin: origin + } + }; + + return this.update(_id, update); + } + + unsetAvatarOrigin(_id) { + const update = { + $unset: { + avatarOrigin: 1 + } + }; + + return this.update(_id, update); + } + + setUserActive(_id, active) { + if (active == null) { active = true; } + const update = { + $set: { + active + } + }; + + return this.update(_id, update); + } + + setAllUsersActive(active) { + const update = { + $set: { + active + } + }; + + return this.update({}, update, { multi: true }); + } + + unsetLoginTokens(_id) { + const update = { + $set: { + 'services.resume.loginTokens' : [] + } + }; + + return this.update(_id, update); + } + + unsetRequirePasswordChange(_id) { + const update = { + $unset: { + 'requirePasswordChange' : true, + 'requirePasswordChangeReason' : true + } + }; + + return this.update(_id, update); + } + + resetPasswordAndSetRequirePasswordChange(_id, requirePasswordChange, requirePasswordChangeReason) { + const update = { + $unset: { + 'services.password': 1 + }, + $set: { + requirePasswordChange, + requirePasswordChangeReason + } + }; + + return this.update(_id, update); + } + + setLanguage(_id, language) { + const update = { + $set: { + language + } + }; + + return this.update(_id, update); + } + + setProfile(_id, profile) { + const update = { + $set: { + 'settings.profile': profile + } + }; + + return this.update(_id, update); + } + + setPreferences(_id, preferences) { + const update = { + $set: { + 'settings.preferences': preferences + } + }; + + return this.update(_id, update); + } + + setUtcOffset(_id, utcOffset) { + const query = { + _id, + utcOffset: { + $ne: utcOffset + } + }; + + const update = { + $set: { + utcOffset + } + }; + + return this.update(query, update); + } + + saveUserById(_id, data) { + const setData = {}; + const unsetData = {}; + + if (data.name != null) { + if (!_.isEmpty(s.trim(data.name))) { + setData.name = s.trim(data.name); + } else { + unsetData.name = 1; + } + } + + if (data.email != null) { + if (!_.isEmpty(s.trim(data.email))) { + setData.emails = [{address: s.trim(data.email)}]; + } else { + unsetData.emails = 1; + } + } + + if (data.phone != null) { + if (!_.isEmpty(s.trim(data.phone))) { + setData.phone = [{phoneNumber: s.trim(data.phone)}]; + } else { + unsetData.phone = 1; + } + } + + const update = {}; + + if (!_.isEmpty(setData)) { + update.$set = setData; + } + + if (!_.isEmpty(unsetData)) { + update.$unset = unsetData; + } + + if (_.isEmpty(update)) { + return true; + } + + return this.update({ _id }, update); + } + +// INSERT + create(data) { + const user = { + createdAt: new Date, + avatarOrigin: 'none' + }; + + _.extend(user, data); + + return this.insert(user); + } + + +// REMOVE + removeById(_id) { + return this.remove(_id); + } + +/* +Find users to send a message by email if: +- he is not online +- has a verified email +- has not disabled email notifications +- `active` is equal to true (false means they were deactivated and can't login) +*/ + getUsersToSendOfflineEmail(usersIds) { + const query = { + _id: { + $in: usersIds + }, + active: true, + status: 'offline', + statusConnection: { + $ne: 'online' + }, + 'emails.verified': true + }; + + return this.find(query, { fields: { name: 1, username: 1, emails: 1, 'settings.preferences.emailNotificationMode': 1 } }); + } +} + +RocketChat.models.Users = new ModelUsers(Meteor.users, true); diff --git a/packages/rocketchat-lib/server/startup/settings.js b/packages/rocketchat-lib/server/startup/settings.js index f222176bb388af3e33fd09fd9ab7809bd5e3622a..bd77f407b7d70c0e630c151e1142befee161be11 100644 --- a/packages/rocketchat-lib/server/startup/settings.js +++ b/packages/rocketchat-lib/server/startup/settings.js @@ -481,6 +481,20 @@ RocketChat.settings.addGroup('Email', function() { }); }); this.section('SMTP', function() { + this.add('SMTP_Protocol', 'smtp', { + type: 'select', + values: [ + { + key: 'smtp', + i18nLabel: 'smtp' + }, { + key: 'smtps', + i18nLabel: 'smtps' + } + ], + env: true, + i18nLabel: 'Protocol' + }); this.add('SMTP_Host', '', { type: 'string', env: true, @@ -491,6 +505,11 @@ RocketChat.settings.addGroup('Email', function() { env: true, i18nLabel: 'Port' }); + this.add('SMTP_Pool', true, { + type: 'boolean', + env: true, + i18nLabel: 'Pool' + }); this.add('SMTP_Username', '', { type: 'string', env: true, diff --git a/packages/rocketchat-lib/server/startup/settingsOnLoadSMTP.js b/packages/rocketchat-lib/server/startup/settingsOnLoadSMTP.js index 5a2fb4d672958b0254562900b5e0e3e76ad5288d..8a3601061aa628763199130f1c8761aea2a0def4 100644 --- a/packages/rocketchat-lib/server/startup/settingsOnLoadSMTP.js +++ b/packages/rocketchat-lib/server/startup/settingsOnLoadSMTP.js @@ -1,14 +1,22 @@ const buildMailURL = _.debounce(function() { console.log('Updating process.env.MAIL_URL'); + if (RocketChat.settings.get('SMTP_Host')) { - process.env.MAIL_URL = 'smtp://'; + process.env.MAIL_URL = `${ RocketChat.settings.get('SMTP_Protocol') }://`; + if (RocketChat.settings.get('SMTP_Username') && RocketChat.settings.get('SMTP_Password')) { process.env.MAIL_URL += `${ encodeURIComponent(RocketChat.settings.get('SMTP_Username')) }:${ encodeURIComponent(RocketChat.settings.get('SMTP_Password')) }@`; } + process.env.MAIL_URL += encodeURIComponent(RocketChat.settings.get('SMTP_Host')); + if (RocketChat.settings.get('SMTP_Port')) { - return process.env.MAIL_URL += `:${ parseInt(RocketChat.settings.get('SMTP_Port')) }`; + process.env.MAIL_URL += `:${ parseInt(RocketChat.settings.get('SMTP_Port')) }`; } + + process.env.MAIL_URL += `?pool=${ RocketChat.settings.get('SMTP_Pool') }`; + + return process.env.MAIL_URL; } }, 500); @@ -34,6 +42,14 @@ RocketChat.settings.onload('SMTP_Password', function(key, value) { } }); +RocketChat.settings.onload('SMTP_Protocol', function() { + return buildMailURL(); +}); + +RocketChat.settings.onload('SMTP_Pool', function() { + return buildMailURL(); +}); + Meteor.startup(function() { return buildMailURL(); }); diff --git a/packages/rocketchat-livechat/app/i18n/de.i18n.json b/packages/rocketchat-livechat/app/i18n/de.i18n.json index 2474b11a94af432dd4ce804d57cccf4c25c02241..625393ec2916c035c7c79ea01c4d942700d3c4d3 100644 --- a/packages/rocketchat-livechat/app/i18n/de.i18n.json +++ b/packages/rocketchat-livechat/app/i18n/de.i18n.json @@ -2,9 +2,11 @@ "Additional_Feedback": "Zusätzliches Feedback", "Appearance": "Erscheinungsbild", "Are_you_sure_do_you_want_end_this_chat": "Sind Sie sich sicher diesen Chat zu beenden?", + "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "Sind Sie sich sicher diesen Chat zu beenden und den Bereich zu wechseln?", "Cancel": "Abbrechen", "Change": "Ändern", "Chat_ended": "Chat beendet!", + "Choose_a_new_department": "Wähle einen neuen Bereich", "Close_menu": "Menü schließen", "Conversation_finished": "Gespräch beendet", "End_chat": "Chat beenden", @@ -17,6 +19,7 @@ "No": "Nein", "Options": "Optionen", "Please_answer_survey": "Bitte nehmen Sie sich einen Moment Zeit, um kurz einige Fragen zu dem Gespräch zu beantworten.", + "Please_choose_a_department": "Wähle einen Bereich", "Please_fill_name_and_email": "Bitte geben Sie einen Namen und eine E-Mail-Adresse ein.", "Powered_by": "Unterstützt von", "Request_video_chat": "Video-Chat anfragen", diff --git a/packages/rocketchat-livechat/app/i18n/fr.i18n.json b/packages/rocketchat-livechat/app/i18n/fr.i18n.json index 61c3e8275f9535f5f04d4fff47883223ea071d66..1e07751cfee850f71f29c8db077827c480b1a6f9 100644 --- a/packages/rocketchat-livechat/app/i18n/fr.i18n.json +++ b/packages/rocketchat-livechat/app/i18n/fr.i18n.json @@ -2,30 +2,30 @@ "Additional_Feedback": "Commentaires supplémentaires", "Appearance": "Apparence", "Are_you_sure_do_you_want_end_this_chat": "Êtes-vous sûr de vouloir mettre fin à cette conversation ?", - "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "Etes vous sûr de vouloir terminer ce chat en direct et changer de département ?", + "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "Etes vous sûr de vouloir terminer ce chat en direct et changer de service ?", "Cancel": "Annuler", "Change": "Changer", "Chat_ended": "Conversation terminée !", - "Choose_a_new_department": "Choisir un nouveau département", + "Choose_a_new_department": "Choisir un nouveau service", "Close_menu": "Fermer le menu", "Conversation_finished": "Conversation terminée", "End_chat": "Mettre fin à la conversation", - "How_friendly_was_the_chat_agent": "L'assistant du chat était-il sympathique ?", - "How_knowledgeable_was_the_chat_agent": "Les réponses de l'assistant du chat était-elles adaptées ?", - "How_responsive_was_the_chat_agent": "L'assistant du chat a-t-il répondu à vos questions ?", + "How_friendly_was_the_chat_agent": "Votre interlocuteur était-il sympathique ?", + "How_knowledgeable_was_the_chat_agent": "Les réponses de votre interlocuteur étaient-elles adaptées ?", + "How_responsive_was_the_chat_agent": "Votre interlocuteur a-t-il répondu à vos questions ?", "How_satisfied_were_you_with_this_chat": "Êtes-vous satisfait de ce chat?", "Installation": "Installation", "New_messages": "Nouveaux messages", "No": "Non", "Options": "Options", "Please_answer_survey": "Merci de prendre un moment pour répondre à un sondage rapide à propos de ce chat ", - "Please_choose_a_department": "Merci de choisir un département", - "Please_fill_name_and_email": "Veuillez remplir votre nom et votre adresse e-mail", + "Please_choose_a_department": "Merci de choisir un service", + "Please_fill_name_and_email": "Veuillez saisir votre nom et votre adresse e-mail", "Powered_by": "Propulsé par", "Request_video_chat": "Demander un chat vidéo", - "Select_a_department": "Sélectionner un département", - "Switch_department": "Changer de département", - "Department_switched": "Département changé", + "Select_a_department": "Sélectionner un service", + "Switch_department": "Changer de service", + "Department_switched": "Changement de service effectué", "Send": "Envoyer", "Skip": "Passer", "Start_Chat": "Démarrer un chat", diff --git a/packages/rocketchat-livechat/app/i18n/ko.i18n.json b/packages/rocketchat-livechat/app/i18n/ko.i18n.json index 569097d1f0411eafdfb62e43a354fc82712b98e8..04b448a9da550beb7da684f58e86a97f6c95f8ba 100644 --- a/packages/rocketchat-livechat/app/i18n/ko.i18n.json +++ b/packages/rocketchat-livechat/app/i18n/ko.i18n.json @@ -2,30 +2,30 @@ "Additional_Feedback": "추가 ì˜ê²¬", "Appearance": "모양", "Are_you_sure_do_you_want_end_this_chat": "ì´ ì±„íŒ…ì„ ì •ë§ ëë‚´ì‹œê² ìŠµë‹ˆê¹Œ?", - "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "ì •ë§ ì´ ì±„íŒ…ì„ ì¢…ë£Œí•˜ê³ ë¶€ì„œë¥¼ ë³€ê²½í•˜ê² ìŠµë‹ˆê¹Œ?", + "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "현재 ì§„í–‰ì¤‘ì¸ ì±„íŒ…ì„ ì¢…ë£Œí•˜ê³ ë¶€ì„œë¥¼ ë³€ê²½í•˜ì‹œê² ìŠµë‹ˆê¹Œ?", "Cancel": "취소", "Change": "변경", - "Chat_ended": "채팅 종료", - "Choose_a_new_department": "새로운 부서를 ì„ íƒí•˜ì„¸ìš”.", + "Chat_ended": "ì±„íŒ…ì´ ì¢…ë£Œë˜ì—ˆìŠµë‹ˆë‹¤. ", + "Choose_a_new_department": "새 부서를 ì„ íƒí•´ì£¼ì„¸ìš”", "Close_menu": "메뉴 닫기", "Conversation_finished": "대화 종료ë¨", "End_chat": "채팅 ë남", - "How_friendly_was_the_chat_agent": "채팅 담당ìžëŠ” 얼마나 ì¹œì ˆí–ˆë‚˜ìš”?", - "How_knowledgeable_was_the_chat_agent": "채팅 ì—ì´ì „íŠ¸ì˜ ê¸°ë°˜ ì§€ì‹ì´ í’부했나요?", - "How_responsive_was_the_chat_agent": "채팅 담당ìžëŠ” 얼마나 ë¹ ë¥´ê²Œ ì‘답했나요?", - "How_satisfied_were_you_with_this_chat": "ì±„íŒ…ì— ì–¼ë§ˆë‚˜ 만족했나요?", + "How_friendly_was_the_chat_agent": "ìƒë‹´ì‚¬ê°€ ì¹œì ˆí–ˆë‚˜ìš”?", + "How_knowledgeable_was_the_chat_agent": "ìƒë‹´ì‚¬ì˜ ê´€ë ¨ 업무 ì§€ì‹ì´ 충분했나요?", + "How_responsive_was_the_chat_agent": "삼담사가 ë¹ ë¥´ê²Œ ì‘답했나요?", + "How_satisfied_were_you_with_this_chat": "채팅 ë‚´ìš©ì— ì–¼ë§ˆë‚˜ 만족 하였나요?", "Installation": "설치", "New_messages": "새 메시지", "No": "아니오", "Options": "옵션", "Please_answer_survey": "ì´ ì±„íŒ…ì— ëŒ€í•œ 간단한 설문 ì¡°ì‚¬ì— ì‘답하기 위해 ìž ì‹œ 시간ì„ë‚´ì–´ 주시기 ë°”ëžë‹ˆë‹¤", - "Please_choose_a_department": "부서를 ì„ íƒí•˜ì„¸ìš”.", + "Please_choose_a_department": "부서를 ì„ íƒí•´ì£¼ì„¸ìš”", "Please_fill_name_and_email": "ì´ë¦„ê³¼ ì´ë©”ì¼ì„ ìž…ë ¥í•˜ì„¸ìš”", - "Powered_by": "ì— ì˜í•´ 구ë™", + "Powered_by": "ì§€ì›ì„받는", "Request_video_chat": "비디오채팅 ìš”ì²", "Select_a_department": "부서를 ì„ íƒí•´ì£¼ì„¸ìš”", "Switch_department": "부서 변경", - "Department_switched": "부서가 변경ë˜ì—ˆìŠµë‹ˆë‹¤.", + "Department_switched": "부서가 변경ë˜ì—ˆìŠµë‹ˆë‹¤", "Send": "ì „ì†¡", "Skip": "건너뛰기", "Start_Chat": "채팅 시작", @@ -33,13 +33,14 @@ "Survey_instructions": "ë‹¹ì‹ ì˜ ë§Œì¡±ë„를 í‰ê°€í•´ 주세요. ë§¤ìš°ë¶ˆë§Œì€ 1, 매우 ë§Œì¡±ì€ 5 입니다.", "Thank_you_for_your_feedback": "ì˜ê²¬ì„ ë³´ë‚´ 주셔서 ê°ì‚¬í•©ë‹ˆë‹¤", "Thanks_We_ll_get_back_to_you_soon": "ê°ì‚¬í•©ë‹ˆë‹¤! ê³§ 다시 ì—°ë½ë“œë¦¬ê² 습니다", - "transcript_sent": "채팅 ë‚´ìš©ì„ ë°œì†¡í–ˆìŠµë‹ˆë‹¤.", - "Type_your_email": "ì´ë©”ì¼ì„ ìž…ë ¥", - "Type_your_message": "메시지를 ìž…ë ¥", - "Type_your_name": "ë‹¹ì‹ ì˜ ì´ë¦„ì„ ìž…ë ¥", - "User_joined": "ì‚¬ìš©ìž ê°€ìž…", - "User_left": "ì‚¬ìš©ìž ì™¼ìª½", - "We_are_offline_Sorry_for_the_inconvenience": "우리는 오프ë¼ì¸ ìƒíƒœìž…니다. ë¶ˆíŽ¸ì„ ë“œë ¤ 죄송합니다.", + "transcript_sent": "채팅 ë‚´ìš©ì„ ë°œì†¡í–ˆìŠµë‹ˆë‹¤", + "Type_your_email": "ì´ë©”ì¼ì„ ìž…ë ¥í•´ì£¼ì„¸ìš”", + "Type_your_message": "메시지를 ìž…ë ¥í•´ì£¼ì„¸ìš”", + "Type_your_name": "ì´ë¦„ì„ ìž…ë ¥í•´ì£¼ì„¸ìš”", + "User_joined": "사용ìžê°€ 참여하였습니다", + "User_left": "사용ìžê°€ ë– ë‚¬ìŠµë‹ˆë‹¤", + "We_are_offline_Sorry_for_the_inconvenience": "현재 오프ë¼ì¸ ìƒíƒœìž…니다. ë¶ˆíŽ¸ì„ ë“œë ¤ 죄송합니다", "Yes": "예", + "You": "ë‹¹ì‹ ", "You_must_complete_all_fields": "ëª¨ë“ í•„ë“œë¥¼ 작성해야합니다" } \ No newline at end of file diff --git a/packages/rocketchat-livechat/app/i18n/no.i18n.json b/packages/rocketchat-livechat/app/i18n/no.i18n.json index d6955adb8572b4c553abd56118203f4cf81085bd..539088fa6dc64634a059a906eb8aa1673a967c2a 100644 --- a/packages/rocketchat-livechat/app/i18n/no.i18n.json +++ b/packages/rocketchat-livechat/app/i18n/no.i18n.json @@ -1,4 +1,29 @@ { "Additional_Feedback": "Tilleggs Tilbakemelding", - "No": "Nei" + "Appearance": "Utseende", + "Are_you_sure_do_you_want_end_this_chat": "Er du sikker pÃ¥ at du vil avslutte denne samtalen?", + "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "Er du sikker pÃ¥ at du vil avslutte denne samtalen og bytte avdeling?", + "Cancel": "Avbryt", + "Change": "Endre", + "Chat_ended": "Samtalen er avsluttet!", + "Choose_a_new_department": "Velg ny avdeling", + "Close_menu": "Lukk meny", + "Conversation_finished": "Samtalen er avsluttet", + "End_chat": "Avslutt samtale", + "How_friendly_was_the_chat_agent": "Hvor vennlig var personen du pratet med?", + "How_responsive_was_the_chat_agent": "Hvor raskt svarte personen du pratet med?", + "How_satisfied_were_you_with_this_chat": "Er du fornøyd med samtalen?", + "Installation": "Installasjon", + "New_messages": "Ny melding", + "No": "Nei", + "Options": "Egenskaper", + "Send": "Send", + "Skip": "Hopp over", + "Start_Chat": "Start samtale", + "Survey": "Undersøkelse", + "Type_your_message": "Skriv inn din beskjed", + "Type_your_name": "Skriv inn ditt navn", + "Yes": "Ja", + "You": "Deg", + "You_must_complete_all_fields": "Du mÃ¥ fylle inn alle feltene" } \ No newline at end of file diff --git a/packages/rocketchat-slackbridge/slackbridge.js b/packages/rocketchat-slackbridge/slackbridge.js index 2dd320a735f2cf7cac5cd94ff66031f1797f00c1..b4bfa1686deb599108879dcbe182eea77856bb3a 100644 --- a/packages/rocketchat-slackbridge/slackbridge.js +++ b/packages/rocketchat-slackbridge/slackbridge.js @@ -80,8 +80,9 @@ class SlackBridge { if (!_.isEmpty(slackMsgTxt)) { slackMsgTxt = slackMsgTxt.replace(/<!everyone>/g, '@all'); slackMsgTxt = slackMsgTxt.replace(/<!channel>/g, '@all'); - slackMsgTxt = slackMsgTxt.replace(/>/g, '<'); - slackMsgTxt = slackMsgTxt.replace(/</g, '>'); + slackMsgTxt = slackMsgTxt.replace(/<!here>/g, '@here'); + slackMsgTxt = slackMsgTxt.replace(/>/g, '>'); + slackMsgTxt = slackMsgTxt.replace(/</g, '<'); slackMsgTxt = slackMsgTxt.replace(/&/g, '&'); slackMsgTxt = slackMsgTxt.replace(/:simple_smile:/g, ':smile:'); slackMsgTxt = slackMsgTxt.replace(/:memo:/g, ':pencil:'); @@ -594,7 +595,6 @@ class SlackBridge { const fileId = Meteor.fileStore.create(details); if (fileId) { Meteor.fileStore.write(stream, fileId, (err, file) => { - console.log('fileStore.write', file); if (err) { throw new Error(err); } else { diff --git a/packages/rocketchat-slashcommands-mute/server/mute.js b/packages/rocketchat-slashcommands-mute/server/mute.js index 38273ff18feeea43344ccd36e2e675efd4caeeef..21601cebe0807cc5ea9a2d02f4cb79240fb37f17 100644 --- a/packages/rocketchat-slashcommands-mute/server/mute.js +++ b/packages/rocketchat-slashcommands-mute/server/mute.js @@ -26,7 +26,7 @@ RocketChat.slashCommands.add('mute', function Mute(command, params, item) { }); return; } - if ((room.usernames || []).includes(username)) { + if ((room.usernames || []).includes(username) === false) { RocketChat.Notifications.notifyUser(Meteor.userId(), 'message', { _id: Random.id(), rid: item.rid, diff --git a/packages/rocketchat-slashcommands-mute/server/unmute.js b/packages/rocketchat-slashcommands-mute/server/unmute.js index c5a3f02cfa108f79dff266141c707cf0c961a748..0d5d6a93e12e94494702711645a7143215570c2f 100644 --- a/packages/rocketchat-slashcommands-mute/server/unmute.js +++ b/packages/rocketchat-slashcommands-mute/server/unmute.js @@ -26,7 +26,7 @@ RocketChat.slashCommands.add('unmute', function Unmute(command, params, item) { }, user.language) }); } - if ((room.usernames || []).includes(username)) { + if ((room.usernames || []).includes(username) === false) { return RocketChat.Notifications.notifyUser(Meteor.userId(), 'message', { _id: Random.id(), rid: item.rid, diff --git a/packages/rocketchat-theme/server/server.js b/packages/rocketchat-theme/server/server.js index 940e4a1fdddfd7f53919abb4298c0725976360ee..16b254185f6a216f9efd063cebab78132f276b46 100644 --- a/packages/rocketchat-theme/server/server.js +++ b/packages/rocketchat-theme/server/server.js @@ -71,17 +71,16 @@ RocketChat.theme = new class { this.compileDelayed = _.debounce(Meteor.bindEnvironment(this.compile.bind(this)), 100); Meteor.startup(() => { RocketChat.settings.onAfterInitialLoad(() => { - RocketChat.settings.get('*', Meteor.bindEnvironment((key, value) => { + RocketChat.settings.get(/^theme-./, Meteor.bindEnvironment((key, value) => { if (key === 'theme-custom-css' && value != null) { this.customCSS = value; - } else if (/^theme-.+/.test(key) === true) { + } else { const name = key.replace(/^theme-[a-z]+-/, ''); if (this.variables[name] != null) { this.variables[name].value = value; } - } else { - return; } + this.compileDelayed(); })); }); @@ -89,7 +88,6 @@ RocketChat.theme = new class { } compile() { - let content = [this.getVariablesAsLess()]; content.push(...this.files.map((name) => Assets.getText(name))); diff --git a/packages/rocketchat-ui-account/client/account.coffee b/packages/rocketchat-ui-account/client/account.coffee deleted file mode 100644 index 21a9874c2416e37cf31dd1cf312ca400eca0ee80..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-account/client/account.coffee +++ /dev/null @@ -1,4 +0,0 @@ -Template.account.onRendered -> - Tracker.afterFlush -> - SideNav.setFlex "accountFlex" - SideNav.openFlex() diff --git a/packages/rocketchat-ui-account/client/account.js b/packages/rocketchat-ui-account/client/account.js new file mode 100644 index 0000000000000000000000000000000000000000..214d55abe8c614d84b784a996c3180ca5219db58 --- /dev/null +++ b/packages/rocketchat-ui-account/client/account.js @@ -0,0 +1,6 @@ +Template.account.onRendered(function() { + Tracker.afterFlush(function() { + SideNav.setFlex('accountFlex'); + SideNav.openFlex(); + }); +}); diff --git a/packages/rocketchat-ui-account/client/accountFlex.coffee b/packages/rocketchat-ui-account/client/accountFlex.coffee deleted file mode 100644 index 1e04bc7ce919da8250f2979a7f5823abe066cf95..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-account/client/accountFlex.coffee +++ /dev/null @@ -1,21 +0,0 @@ -Template.accountFlex.events - 'mouseenter header': -> - SideNav.overArrow() - - 'mouseleave header': -> - SideNav.leaveArrow() - - 'click header': -> - SideNav.closeFlex() - - 'click .cancel-settings': -> - SideNav.closeFlex() - - 'click .account-link': -> - menu.close() - -Template.accountFlex.helpers - allowUserProfileChange: -> - return RocketChat.settings.get("Accounts_AllowUserProfileChange") - allowUserAvatarChange: -> - return RocketChat.settings.get("Accounts_AllowUserAvatarChange") \ No newline at end of file diff --git a/packages/rocketchat-ui-account/client/accountFlex.js b/packages/rocketchat-ui-account/client/accountFlex.js new file mode 100644 index 0000000000000000000000000000000000000000..7d6d789084c824f1744a4c41b8c4509d732c4ef6 --- /dev/null +++ b/packages/rocketchat-ui-account/client/accountFlex.js @@ -0,0 +1,27 @@ +/*globals menu */ +Template.accountFlex.events({ + 'mouseenter header'() { + SideNav.overArrow(); + }, + 'mouseleave header'() { + SideNav.leaveArrow(); + }, + 'click header'() { + SideNav.closeFlex(); + }, + 'click .cancel-settings'() { + SideNav.closeFlex(); + }, + 'click .account-link'() { + menu.close(); + } +}); + +Template.accountFlex.helpers({ + allowUserProfileChange() { + return RocketChat.settings.get('Accounts_AllowUserProfileChange'); + }, + allowUserAvatarChange() { + return RocketChat.settings.get('Accounts_AllowUserAvatarChange'); + } +}); diff --git a/packages/rocketchat-ui-account/client/accountPreferences.coffee b/packages/rocketchat-ui-account/client/accountPreferences.coffee deleted file mode 100644 index 5ed2003c658c3cbd1259a8361af956d6b877accc..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-account/client/accountPreferences.coffee +++ /dev/null @@ -1,145 +0,0 @@ -import toastr from 'toastr' -Template.accountPreferences.helpers - audioAssets: -> - return RocketChat.CustomSounds && RocketChat.CustomSounds.getList && RocketChat.CustomSounds.getList() || []; - - newMessageNotification: -> - return Meteor.user()?.settings?.preferences?.newMessageNotification || 'chime' - - newRoomNotification: -> - return Meteor.user()?.settings?.preferences?.newRoomNotification || 'door' - - languages: -> - languages = TAPi18n.getLanguages() - result = [] - for key, language of languages - result.push _.extend(language, { key: key }) - return _.sortBy(result, 'key') - - userLanguage: (key) -> - return (Meteor.user().language or defaultUserLanguage())?.split('-').shift().toLowerCase() is key - - checked: (property, value, defaultValue) -> - if not Meteor.user()?.settings?.preferences?[property]? and defaultValue is true - currentValue = value - else if Meteor.user()?.settings?.preferences?[property]? - currentValue = !!Meteor.user()?.settings?.preferences?[property] - - return currentValue is value - - selected: (property, value, defaultValue) -> - if not Meteor.user()?.settings?.preferences?[property] - return defaultValue is true - else - return Meteor.user()?.settings?.preferences?[property] == value - - highlights: -> - return Meteor.user()?.settings?.preferences?['highlights']?.join(', ') - - desktopNotificationEnabled: -> - return (KonchatNotification.notificationStatus.get() is 'granted') or (window.Notification && Notification.permission is "granted") - - desktopNotificationDisabled: -> - return (KonchatNotification.notificationStatus.get() is 'denied') or (window.Notification && Notification.permission is "denied") - - desktopNotificationDuration: -> - return Meteor.user()?.settings?.preferences?.desktopNotificationDuration - 0 - - showRoles: -> - return RocketChat.settings.get('UI_DisplayRoles'); - -Template.accountPreferences.onCreated -> - settingsTemplate = this.parentTemplate(3) - settingsTemplate.child ?= [] - settingsTemplate.child.push this - - @useEmojis = new ReactiveVar not Meteor.user()?.settings?.preferences?.useEmojis? or Meteor.user().settings.preferences.useEmojis - instance = @ - @autorun -> - if instance.useEmojis.get() - Tracker.afterFlush -> - $('#convertAsciiEmoji').show() - else - Tracker.afterFlush -> - $('#convertAsciiEmoji').hide() - - @clearForm = -> - @find('#language').value = localStorage.getItem('userLanguage') - - @save = -> - instance = @ - data = {} - - reload = false - selectedLanguage = $('#language').val() - - if localStorage.getItem('userLanguage') isnt selectedLanguage - localStorage.setItem 'userLanguage', selectedLanguage - data.language = selectedLanguage - reload = true - - data.newRoomNotification = $('select[name=newRoomNotification]').val() - data.newMessageNotification = $('select[name=newMessageNotification]').val() - data.useEmojis = $('input[name=useEmojis]:checked').val() - data.convertAsciiEmoji = $('input[name=convertAsciiEmoji]:checked').val() - data.saveMobileBandwidth = $('input[name=saveMobileBandwidth]:checked').val() - data.collapseMediaByDefault = $('input[name=collapseMediaByDefault]:checked').val() - data.viewMode = parseInt($('#viewMode').find('select').val()) - data.hideUsernames = $('#hideUsernames').find('input:checked').val() - data.hideRoles = $('#hideRoles').find('input:checked').val() - data.hideFlexTab = $('#hideFlexTab').find('input:checked').val() - data.hideAvatars = $('#hideAvatars').find('input:checked').val() - data.mergeChannels = $('#mergeChannels').find('input:checked').val() - data.sendOnEnter = $('#sendOnEnter').find('select').val() - data.unreadRoomsMode = $('input[name=unreadRoomsMode]:checked').val() - data.autoImageLoad = $('input[name=autoImageLoad]:checked').val() - data.emailNotificationMode = $('select[name=emailNotificationMode]').val() - data.highlights = _.compact(_.map($('[name=highlights]').val().split(','), (e) -> return _.trim(e))) - data.desktopNotificationDuration = $('input[name=desktopNotificationDuration]').val() - data.unreadAlert = $('#unreadAlert').find('input:checked').val() - - Meteor.call 'saveUserPreferences', data, (error, results) -> - if results - toastr.success t('Preferences_saved') - instance.clearForm() - if reload - setTimeout -> - Meteor._reload.reload() - , 1000 - - if error - handleError(error) - -Template.accountPreferences.onRendered -> - Tracker.afterFlush -> - SideNav.setFlex "accountFlex" - SideNav.openFlex() - -Template.accountPreferences.events - 'click .submit button': (e, t) -> - t.save() - - 'change input[name=useEmojis]': (e, t) -> - t.useEmojis.set $(e.currentTarget).val() is '1' - - 'click .enable-notifications': -> - KonchatNotification.getDesktopPermission() - - 'click .test-notifications': -> - KonchatNotification.notify - duration: $('input[name=desktopNotificationDuration]').val() - payload: - sender: - username: 'rocket.cat' - title: TAPi18n.__('Desktop_Notification_Test') - text: TAPi18n.__('This_is_a_desktop_notification') - - 'change .audio': (e) -> - e.preventDefault() - audio = $(e.currentTarget).val() - if audio is 'none' - return - - if audio - $audio = $('audio#' + audio) - $audio?[0]?.play() diff --git a/packages/rocketchat-ui-account/client/accountPreferences.js b/packages/rocketchat-ui-account/client/accountPreferences.js new file mode 100644 index 0000000000000000000000000000000000000000..bfd096e5f0dfd909e26e1914d31786cc81a285b0 --- /dev/null +++ b/packages/rocketchat-ui-account/client/accountPreferences.js @@ -0,0 +1,180 @@ +/*globals defaultUserLanguage, KonchatNotification */ +import toastr from 'toastr'; +Template.accountPreferences.helpers({ + audioAssets() { + return (RocketChat.CustomSounds && RocketChat.CustomSounds.getList && RocketChat.CustomSounds.getList()) || []; + }, + newMessageNotification() { + const user = Meteor.user(); + return (user && user.settings && user.settings.preferences && user.settings.preferences.newMessageNotification) || 'chime'; + }, + newRoomNotification() { + const user = Meteor.user(); + return (user && user.settings && user.settings.preferences && user.settings.preferences.newRoomNotification) || 'door'; + }, + languages() { + const languages = TAPi18n.getLanguages(); + + const result = Object.keys(languages).map((key) => { + const language = languages[key]; + return _.extend(language, { key }); + }); + + return _.sortBy(result, 'key'); + }, + userLanguage(key) { + const user = Meteor.user(); + let result = undefined; + if (user.language) { + result = user.language.split('-').shift().toLowerCase() === key; + } else if (defaultUserLanguage()) { + result = defaultUserLanguage().split('-').shift().toLowerCase() === key; + } + return result; + }, + checked(property, value, defaultValue) { + const user = Meteor.user(); + const propertyeExists = !!(user && user.settings && user.settings.preferences && user.settings.preferences[property]); + let currentValue; + if (propertyeExists) { + currentValue = !!user.settings.preferences[property]; + } else if (!propertyeExists && defaultValue === true) { + currentValue = value; + } + return currentValue === value; + }, + selected(property, value, defaultValue) { + const user = Meteor.user(); + const propertyeExists = !!(user && user.settings && user.settings.preferences && user.settings.preferences[property]); + if (propertyeExists) { + return user.settings.preferences[property] === value; + } else { + return defaultValue === true; + } + }, + highlights() { + const user = Meteor.user(); + return user && user.settings && user.settings.preferences && user.settings.preferences['highlights'] && user.settings.preferences['highlights'].join(', '); + }, + desktopNotificationEnabled() { + return KonchatNotification.notificationStatus.get() === 'granted' || (window.Notification && Notification.permission === 'granted'); + }, + desktopNotificationDisabled() { + return KonchatNotification.notificationStatus.get() === 'denied' || (window.Notification && Notification.permission === 'denied'); + }, + desktopNotificationDuration() { + const user = Meteor.user(); + return user && user.settings && user.settings.preferences && user.settings.preferences.desktopNotificationDuration; + }, + showRoles() { + return RocketChat.settings.get('UI_DisplayRoles'); + } +}); + +Template.accountPreferences.onCreated(function() { + const settingsTemplate = this.parentTemplate(3); + if (settingsTemplate.child == null) { + settingsTemplate.child = []; + } + settingsTemplate.child.push(this); + const user = Meteor.user(); + if (user && user.settings && user.settings.preferences) { + this.useEmojis = new ReactiveVar(user.settings.preferences.desktopNotificationDuration == null || user.settings.preferences.useEmojis); + } + let instance = this; + this.autorun(() => { + if (instance.useEmojis && instance.useEmojis.get()) { + Tracker.afterFlush(() => $('#convertAsciiEmoji').show()); + } else { + Tracker.afterFlush(() => $('#convertAsciiEmoji').hide()); + } + }); + this.clearForm = function() { + this.find('#language').value = localStorage.getItem('userLanguage'); + }; + this.save = function() { + instance = this; + const data = {}; + let reload = false; + const selectedLanguage = $('#language').val(); + if (localStorage.getItem('userLanguage') !== selectedLanguage) { + localStorage.setItem('userLanguage', selectedLanguage); + data.language = selectedLanguage; + reload = true; + } + data.newRoomNotification = $('select[name=newRoomNotification]').val(); + data.newMessageNotification = $('select[name=newMessageNotification]').val(); + data.useEmojis = $('input[name=useEmojis]:checked').val(); + data.convertAsciiEmoji = $('input[name=convertAsciiEmoji]:checked').val(); + data.saveMobileBandwidth = $('input[name=saveMobileBandwidth]:checked').val(); + data.collapseMediaByDefault = $('input[name=collapseMediaByDefault]:checked').val(); + data.viewMode = parseInt($('#viewMode').find('select').val()); + data.hideUsernames = $('#hideUsernames').find('input:checked').val(); + data.hideRoles = $('#hideRoles').find('input:checked').val(); + data.hideFlexTab = $('#hideFlexTab').find('input:checked').val(); + data.hideAvatars = $('#hideAvatars').find('input:checked').val(); + data.mergeChannels = $('#mergeChannels').find('input:checked').val(); + data.sendOnEnter = $('#sendOnEnter').find('select').val(); + data.unreadRoomsMode = $('input[name=unreadRoomsMode]:checked').val(); + data.autoImageLoad = $('input[name=autoImageLoad]:checked').val(); + data.emailNotificationMode = $('select[name=emailNotificationMode]').val(); + data.highlights = _.compact(_.map($('[name=highlights]').val().split(','), function(e) { + return _.trim(e); + })); + data.desktopNotificationDuration = $('input[name=desktopNotificationDuration]').val(); + data.unreadAlert = $('#unreadAlert').find('input:checked').val(); + Meteor.call('saveUserPreferences', data, function(error, results) { + if (results) { + toastr.success(t('Preferences_saved')); + instance.clearForm(); + if (reload) { + setTimeout(function() { + Meteor._reload.reload(); + }, 1000); + } + } + if (error) { + return handleError(error); + } + }); + }; +}); + +Template.accountPreferences.onRendered(function() { + Tracker.afterFlush(function() { + SideNav.setFlex('accountFlex'); + SideNav.openFlex(); + }); +}); + +Template.accountPreferences.events({ + 'click .submit button'(e, t) { + t.save(); + }, + 'change input[name=useEmojis]'(e, t) { + t.useEmojis.set($(e.currentTarget).val() === '1'); + }, + 'click .enable-notifications'() { + KonchatNotification.getDesktopPermission(); + }, + 'click .test-notifications'() { + KonchatNotification.notify({ + duration: $('input[name=desktopNotificationDuration]').val(), + payload: { sender: { username: 'rocket.cat' } + }, + title: TAPi18n.__('Desktop_Notification_Test'), + text: TAPi18n.__('This_is_a_desktop_notification') + }); + }, + 'change .audio'(e) { + e.preventDefault(); + const audio = $(e.currentTarget).val(); + if (audio === 'none') { + return; + } + if (audio) { + const $audio = $(`audio#${ audio }`); + return $audio && $audio[0] && $audio.play(); + } + } +}); diff --git a/packages/rocketchat-ui-account/client/accountProfile.coffee b/packages/rocketchat-ui-account/client/accountProfile.coffee deleted file mode 100644 index d1e0979cb50046048f215eed555d97fe814bc0e8..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-account/client/accountProfile.coffee +++ /dev/null @@ -1,203 +0,0 @@ -import toastr from 'toastr' -Template.accountProfile.helpers - allowDeleteOwnAccount: -> - return RocketChat.settings.get('Accounts_AllowDeleteOwnAccount') - - realname: -> - return Meteor.user().name - - username: -> - return Meteor.user().username - - email: -> - return Meteor.user().emails?[0]?.address - - emailVerified: -> - return Meteor.user().emails?[0]?.verified - - allowUsernameChange: -> - return RocketChat.settings.get("Accounts_AllowUsernameChange") and RocketChat.settings.get("LDAP_Enable") isnt true - - allowEmailChange: -> - return RocketChat.settings.get("Accounts_AllowEmailChange") - - usernameChangeDisabled: -> - return t('Username_Change_Disabled') - - allowPasswordChange: -> - return RocketChat.settings.get("Accounts_AllowPasswordChange") - - passwordChangeDisabled: -> - return t('Password_Change_Disabled') - - customFields: -> - return Meteor.user().customFields - -Template.accountProfile.onCreated -> - settingsTemplate = this.parentTemplate(3) - settingsTemplate.child ?= [] - settingsTemplate.child.push this - - @clearForm = -> - @find('#password').value = '' - - @changePassword = (newPassword, callback) -> - instance = @ - if not newPassword - return callback() - - else - if !RocketChat.settings.get("Accounts_AllowPasswordChange") - toastr.remove(); - toastr.error t('Password_Change_Disabled') - instance.clearForm() - return - - @save = (typedPassword) -> - instance = @ - - data = { typedPassword: typedPassword } - - if _.trim($('#password').val()) and RocketChat.settings.get("Accounts_AllowPasswordChange") - data.newPassword = $('#password').val() - - if _.trim $('#realname').val() - data.realname = _.trim $('#realname').val() - - if _.trim($('#username').val()) isnt Meteor.user().username - if !RocketChat.settings.get("Accounts_AllowUsernameChange") - toastr.remove(); - toastr.error t('Username_Change_Disabled') - instance.clearForm() - return - else - data.username = _.trim $('#username').val() - - if _.trim($('#email').val()) isnt Meteor.user().emails?[0]?.address - if !RocketChat.settings.get("Accounts_AllowEmailChange") - toastr.remove(); - toastr.error t('Email_Change_Disabled') - instance.clearForm() - return - else - data.email = _.trim $('#email').val() - - customFields = {} - $('[data-customfield=true]').each () -> - customFields[this.name] = $(this).val() or '' - - Meteor.call 'saveUserProfile', data, customFields, (error, results) -> - if results - toastr.remove(); - toastr.success t('Profile_saved_successfully') - swal.close() - instance.clearForm() - - if error - toastr.remove(); - handleError(error) - -Template.accountProfile.onRendered -> - Tracker.afterFlush -> - # this should throw an error-template - FlowRouter.go("home") if !RocketChat.settings.get("Accounts_AllowUserProfileChange") - SideNav.setFlex "accountFlex" - SideNav.openFlex() - -Template.accountProfile.events - 'click .submit button': (e, instance) -> - user = Meteor.user() - reqPass = ((_.trim($('#email').val()) isnt user?.emails?[0]?.address) or _.trim($('#password').val())) and s.trim(user?.services?.password?.bcrypt) - unless reqPass - return instance.save() - - swal - title: t("Please_enter_your_password"), - text: t("For_your_security_you_must_enter_your_current_password_to_continue"), - type: "input", - inputType: "password", - showCancelButton: true, - closeOnConfirm: false, - confirmButtonText: t('Save'), - cancelButtonText: t('Cancel') - - , (typedPassword) => - if typedPassword - toastr.remove(); - toastr.warning(t("Please_wait_while_your_profile_is_being_saved")); - instance.save(SHA256(typedPassword)) - else - swal.showInputError(t("You_need_to_type_in_your_password_in_order_to_do_this")); - return false; - 'click .logoutOthers button': (event, templateInstance) -> - Meteor.logoutOtherClients (error) -> - if error - toastr.remove(); - handleError(error) - else - toastr.remove(); - toastr.success t('Logged_out_of_other_clients_successfully') - 'click .delete-account button': (e) -> - e.preventDefault(); - if s.trim Meteor.user()?.services?.password?.bcrypt - swal - title: t("Are_you_sure_you_want_to_delete_your_account"), - text: t("If_you_are_sure_type_in_your_password"), - type: "input", - inputType: "password", - showCancelButton: true, - closeOnConfirm: false, - confirmButtonText: t('Delete') - cancelButtonText: t('Cancel') - - , (typedPassword) => - if typedPassword - toastr.remove(); - toastr.warning(t("Please_wait_while_your_account_is_being_deleted")); - Meteor.call 'deleteUserOwnAccount', SHA256(typedPassword), (error, results) -> - if error - toastr.remove(); - swal.showInputError(t("Your_password_is_wrong")); - else - swal.close(); - else - swal.showInputError(t("You_need_to_type_in_your_password_in_order_to_do_this")); - return false; - else - swal - title: t("Are_you_sure_you_want_to_delete_your_account"), - text: t("If_you_are_sure_type_in_your_username"), - type: "input", - showCancelButton: true, - closeOnConfirm: false, - confirmButtonText: t('Delete') - cancelButtonText: t('Cancel') - - , (deleteConfirmation) => - if deleteConfirmation is Meteor.user()?.username - toastr.remove(); - toastr.warning(t("Please_wait_while_your_account_is_being_deleted")); - Meteor.call 'deleteUserOwnAccount', deleteConfirmation, (error, results) -> - if error - toastr.remove(); - swal.showInputError(t("Your_password_is_wrong")); - else - swal.close(); - else - swal.showInputError(t("You_need_to_type_in_your_username_in_order_to_do_this")); - return false; - - 'click #resend-verification-email': (e) -> - e.preventDefault() - - e.currentTarget.innerHTML = e.currentTarget.innerHTML + ' ...' - e.currentTarget.disabled = true - - Meteor.call 'sendConfirmationEmail', Meteor.user().emails?[0]?.address, (error, results) => - if results - toastr.success t('Verification_email_sent') - else if error - handleError(error) - - e.currentTarget.innerHTML = e.currentTarget.innerHTML.replace(' ...', '') - e.currentTarget.disabled = false diff --git a/packages/rocketchat-ui-account/client/accountProfile.js b/packages/rocketchat-ui-account/client/accountProfile.js new file mode 100644 index 0000000000000000000000000000000000000000..2038b340d2de071b9dc9ae128ae566f073dc39f8 --- /dev/null +++ b/packages/rocketchat-ui-account/client/accountProfile.js @@ -0,0 +1,233 @@ +import toastr from 'toastr'; +Template.accountProfile.helpers({ + allowDeleteOwnAccount() { + return RocketChat.settings.get('Accounts_AllowDeleteOwnAccount'); + }, + realname() { + return Meteor.user().name; + }, + username() { + return Meteor.user().username; + }, + email() { + const user = Meteor.user(); + return user.emails && user.emails[0] && user.emails[0].address; + }, + emailVerified() { + const user = Meteor.user(); + return user.emails && user.emails[0] && user.emails[0].verified; + }, + allowUsernameChange() { + return RocketChat.settings.get('Accounts_AllowUsernameChange') && RocketChat.settings.get('LDAP_Enable') !== true; + }, + allowEmailChange() { + return RocketChat.settings.get('Accounts_AllowEmailChange'); + }, + usernameChangeDisabled() { + return t('Username_Change_Disabled'); + }, + allowPasswordChange() { + return RocketChat.settings.get('Accounts_AllowPasswordChange'); + }, + passwordChangeDisabled() { + return t('Password_Change_Disabled'); + }, + customFields() { + return Meteor.user().customFields; + } +}); + +Template.accountProfile.onCreated(function() { + const settingsTemplate = this.parentTemplate(3); + if (settingsTemplate.child == null) { + settingsTemplate.child = []; + } + settingsTemplate.child.push(this); + this.clearForm = function() { + this.find('#password').value = ''; + }; + this.changePassword = function(newPassword, callback) { + const instance = this; + if (!newPassword) { + return callback(); + } else if (!RocketChat.settings.get('Accounts_AllowPasswordChange')) { + toastr.remove(); + toastr.error(t('Password_Change_Disabled')); + instance.clearForm(); + return; + } + }; + this.save = function(typedPassword) { + const instance = this; + const data = { + typedPassword + }; + if (_.trim($('#password').val()) && RocketChat.settings.get('Accounts_AllowPasswordChange')) { + data.newPassword = $('#password').val(); + } + if (_.trim($('#realname').val())) { + data.realname = _.trim($('#realname').val()); + } + if (_.trim($('#username').val()) !== Meteor.user().username) { + if (!RocketChat.settings.get('Accounts_AllowUsernameChange')) { + toastr.remove(); + toastr.error(t('Username_Change_Disabled')); + instance.clearForm(); + return; + } else { + data.username = _.trim($('#username').val()); + } + } + const user = Meteor.user(); + if (_.trim($('#email').val()) !== (user.emails && user.emails[0] && user.emails[0].address)) { + if (!RocketChat.settings.get('Accounts_AllowEmailChange')) { + toastr.remove(); + toastr.error(t('Email_Change_Disabled')); + instance.clearForm(); + return; + } else { + data.email = _.trim($('#email').val()); + } + } + const customFields = {}; + $('[data-customfield=true]').each(function() { + customFields[this.name] = $(this).val() || ''; + }); + Meteor.call('saveUserProfile', data, customFields, function(error, results) { + if (results) { + toastr.remove(); + toastr.success(t('Profile_saved_successfully')); + swal.close(); + instance.clearForm(); + } + if (error) { + toastr.remove(); + return handleError(error); + } + }); + }; +}); + +Template.accountProfile.onRendered(function() { + Tracker.afterFlush(function() { + if (!RocketChat.settings.get('Accounts_AllowUserProfileChange')) { + FlowRouter.go('home'); + } + SideNav.setFlex('accountFlex'); + SideNav.openFlex(); + }); +}); + +Template.accountProfile.events({ + 'click .submit button'(e, instance) { + const user = Meteor.user(); + const reqPass = ((_.trim($('#email').val()) !== (user && user.emails && user.emails[0] && user.emails[0].address)) || _.trim($('#password').val())) && (user && user.services && user.services.password && s.trim(user.services.password.bcrypt)); + if (!reqPass) { + return instance.save(); + } + swal({ + title: t('Please_enter_your_password'), + text: t('For_your_security_you_must_enter_your_current_password_to_continue'), + type: 'input', + inputType: 'password', + showCancelButton: true, + closeOnConfirm: false, + confirmButtonText: t('Save'), + cancelButtonText: t('Cancel') + }, (typedPassword) => { + if (typedPassword) { + toastr.remove(); + toastr.warning(t('Please_wait_while_your_profile_is_being_saved')); + instance.save(SHA256(typedPassword)); + } else { + swal.showInputError(t('You_need_to_type_in_your_password_in_order_to_do_this')); + return false; + } + }); + }, + 'click .logoutOthers button'() { + Meteor.logoutOtherClients(function(error) { + if (error) { + toastr.remove(); + handleError(error); + } else { + toastr.remove(); + toastr.success(t('Logged_out_of_other_clients_successfully')); + } + }); + }, + 'click .delete-account button'(e) { + e.preventDefault(); + const user = Meteor.user(); + if (s.trim(user && user.services && user.services.password && user.services.password.bcrypt)) { + swal({ + title: t('Are_you_sure_you_want_to_delete_your_account'), + text: t('If_you_are_sure_type_in_your_password'), + type: 'input', + inputType: 'password', + showCancelButton: true, + closeOnConfirm: false, + confirmButtonText: t('Delete'), + cancelButtonText: t('Cancel') + }, (typedPassword) => { + if (typedPassword) { + toastr.remove(); + toastr.warning(t('Please_wait_while_your_account_is_being_deleted')); + Meteor.call('deleteUserOwnAccount', SHA256(typedPassword), function(error) { + if (error) { + toastr.remove(); + swal.showInputError(t('Your_password_is_wrong')); + } else { + swal.close(); + } + }); + } else { + swal.showInputError(t('You_need_to_type_in_your_password_in_order_to_do_this')); + return false; + } + }); + } else { + swal({ + title: t('Are_you_sure_you_want_to_delete_your_account'), + text: t('If_you_are_sure_type_in_your_username'), + type: 'input', + showCancelButton: true, + closeOnConfirm: false, + confirmButtonText: t('Delete'), + cancelButtonText: t('Cancel') + }, (deleteConfirmation) => { + const user = Meteor.user(); + if (deleteConfirmation === (user && user.username)) { + toastr.remove(); + toastr.warning(t('Please_wait_while_your_account_is_being_deleted')); + Meteor.call('deleteUserOwnAccount', deleteConfirmation, function(error) { + if (error) { + toastr.remove(); + swal.showInputError(t('Your_password_is_wrong')); + } else { + swal.close(); + } + }); + } else { + swal.showInputError(t('You_need_to_type_in_your_username_in_order_to_do_this')); + return false; + } + }); + } + }, + 'click #resend-verification-email'(e) { + const user = Meteor.user(); + e.preventDefault(); + e.currentTarget.innerHTML = `${ e.currentTarget.innerHTML } ...`; + e.currentTarget.disabled = true; + Meteor.call('sendConfirmationEmail', user.emails && user.emails[0] && user.emails[0].address((error, results) => { + if (results) { + toastr.success(t('Verification_email_sent')); + } else if (error) { + handleError(error); + } + e.currentTarget.innerHTML = e.currentTarget.innerHTML.replace(' ...', ''); + return e.currentTarget.disabled = false; + })); + } +}); diff --git a/packages/rocketchat-ui-account/client/avatar/avatar.coffee b/packages/rocketchat-ui-account/client/avatar/avatar.coffee deleted file mode 100644 index b0abe8754a17e541350f55cda4e2643c23d25398..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-account/client/avatar/avatar.coffee +++ /dev/null @@ -1,14 +0,0 @@ -Template.avatar.helpers - imageUrl: -> - username = this.username - if not username? and this.userId? - username = Meteor.users.findOne(this.userId)?.username - - if not username? - return - - Session.get "avatar_random_#{username}" - - url = getAvatarUrlFromUsername(username) - - return "background-image:url(#{url});" diff --git a/packages/rocketchat-ui-account/client/avatar/avatar.js b/packages/rocketchat-ui-account/client/avatar/avatar.js new file mode 100644 index 0000000000000000000000000000000000000000..74214467ccf50b1f8fb65e3a355fdf9bb8af81c7 --- /dev/null +++ b/packages/rocketchat-ui-account/client/avatar/avatar.js @@ -0,0 +1,15 @@ +Template.avatar.helpers({ + imageUrl() { + let username = this.username; + if (username == null && this.userId != null) { + const user = Meteor.users.findOne(this.userId); + username = user && user.username; + } + if (username == null) { + return; + } + Session.get(`avatar_random_${ username }`); + const url = getAvatarUrlFromUsername(username); + return `background-image:url(${ url });`; + } +}); diff --git a/packages/rocketchat-ui-account/client/avatar/prompt.coffee b/packages/rocketchat-ui-account/client/avatar/prompt.coffee deleted file mode 100644 index 50f3d8838fdfe26815adf9f85e9a23b04781255f..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-account/client/avatar/prompt.coffee +++ /dev/null @@ -1,105 +0,0 @@ -import toastr from 'toastr' -Template.avatarPrompt.onCreated -> - self = this - self.suggestions = new ReactiveVar - self.upload = new ReactiveVar - - self.getSuggestions = -> - self.suggestions.set undefined - Meteor.call 'getAvatarSuggestion', (error, avatars) -> - self.suggestions.set - ready: true - avatars: avatars - - self.getSuggestions() - -Template.avatarPrompt.onRendered -> - Tracker.afterFlush -> - # this should throw an error-template - FlowRouter.go("home") if !RocketChat.settings.get("Accounts_AllowUserAvatarChange") - SideNav.setFlex "accountFlex" - SideNav.openFlex() - -Template.avatarPrompt.helpers - suggestions: -> - return Template.instance().suggestions.get() - - suggestAvatar: (service) -> - suggestions = Template.instance().suggestions.get() - return RocketChat.settings.get("Accounts_OAuth_#{_.capitalize service}") and not suggestions.avatars[service] - - upload: -> - return Template.instance().upload.get() - - username: -> - return Meteor.user()?.username - - initialsUsername: -> - return '@'+Meteor.user()?.username - -Template.avatarPrompt.events - 'click .select-service': -> - if @service is 'initials' - Meteor.call 'resetAvatar', (err) -> - if err?.details?.timeToReset? - toastr.error t('error-too-many-requests', { seconds: parseInt(err.details.timeToReset / 1000) }) - else - toastr.success t('Avatar_changed_successfully') - RocketChat.callbacks.run('userAvatarSet', 'initials') - else if @service is 'url' - if _.trim $('#avatarurl').val() - Meteor.call 'setAvatarFromService', $('#avatarurl').val(), '', @service, (err) -> - if err - if err.details?.timeToReset? - toastr.error t('error-too-many-requests', { seconds: parseInt(err.details.timeToReset / 1000) }) - else - toastr.error t('Avatar_url_invalid_or_error') - else - toastr.success t('Avatar_changed_successfully') - RocketChat.callbacks.run('userAvatarSet', 'url') - else - toastr.error t('Please_enter_value_for_url') - else - tmpService = @service - Meteor.call 'setAvatarFromService', @blob, @contentType, @service, (err) -> - if err?.details?.timeToReset? - toastr.error t('error-too-many-requests', { seconds: parseInt(err.details.timeToReset / 1000) }) - else - toastr.success t('Avatar_changed_successfully') - RocketChat.callbacks.run('userAvatarSet', tmpService) - - 'click .login-with-service': (event, template) -> - loginWithService = "loginWith#{_.capitalize(this)}" - - serviceConfig = {} - - Meteor[loginWithService] serviceConfig, (error) -> - if error?.error is 'github-no-public-email' - alert t("github_no_public_email") - return - - console.log error - if error? - toastr.error error.message - return - - template.getSuggestions() - - 'change .avatar-file-input': (event, template) -> - e = event.originalEvent or event - files = e.target.files - if not files or files.length is 0 - files = e.dataTransfer?.files or [] - - for blob in files - if not /image\/.+/.test blob.type - return - - reader = new FileReader() - reader.readAsDataURL(blob) - reader.onloadend = -> - template.upload.set - service: 'upload' - contentType: blob.type - blob: reader.result - RocketChat.callbacks.run('userAvatarSet', 'upload') diff --git a/packages/rocketchat-ui-account/client/avatar/prompt.js b/packages/rocketchat-ui-account/client/avatar/prompt.js new file mode 100644 index 0000000000000000000000000000000000000000..b2a02bb5b8854a4e60d1271d444c53bcbd99cb9c --- /dev/null +++ b/packages/rocketchat-ui-account/client/avatar/prompt.js @@ -0,0 +1,129 @@ +import toastr from 'toastr'; +Template.avatarPrompt.onCreated(function() { + const self = this; + self.suggestions = new ReactiveVar; + self.upload = new ReactiveVar; + self.getSuggestions = function() { + self.suggestions.set(undefined); + Meteor.call('getAvatarSuggestion', function(error, avatars) { + self.suggestions.set({ ready: true, avatars }); + }); + }; + self.getSuggestions(); +}); + +Template.avatarPrompt.onRendered(function() { + Tracker.afterFlush(function() { + if (!RocketChat.settings.get('Accounts_AllowUserAvatarChange')) { + FlowRouter.go('home'); + } + SideNav.setFlex('accountFlex'); + SideNav.openFlex(); + }); +}); + +Template.avatarPrompt.helpers({ + suggestions() { + return Template.instance().suggestions.get(); + }, + suggestAvatar(service) { + const suggestions = Template.instance().suggestions.get(); + return RocketChat.settings.get(`Accounts_OAuth_${ _.capitalize(service) }`) && !suggestions.avatars[service]; + }, + upload() { + return Template.instance().upload.get(); + }, + username() { + const user = Meteor.user(); + return user && user.username; + }, + initialsUsername() { + const user = Meteor.user(); + return `@${ user && user.username }`; + } +}); + +Template.avatarPrompt.events({ + 'click .select-service'() { + if (this.service === 'initials') { + Meteor.call('resetAvatar', function(err) { + if (err && err.details.timeToReset && err.details.timeToReset) { + toastr.error(t('error-too-many-requests', { + seconds: parseInt(err.details.timeToReset / 1000) + })); + } else { + toastr.success(t('Avatar_changed_successfully')); + RocketChat.callbacks.run('userAvatarSet', 'initials'); + } + }); + } else if (this.service === 'url') { + if (_.trim($('#avatarurl').val())) { + Meteor.call('setAvatarFromService', $('#avatarurl').val(), '', this.service, function(err) { + if (err) { + if (err.details.timeToReset && err.details.timeToReset) { + toastr.error(t('error-too-many-requests', { + seconds: parseInt(err.details.timeToReset / 1000) + })); + } else { + toastr.error(t('Avatar_url_invalid_or_error')); + } + } else { + toastr.success(t('Avatar_changed_successfully')); + RocketChat.callbacks.run('userAvatarSet', 'url'); + } + }); + } else { + toastr.error(t('Please_enter_value_for_url')); + } + } else { + const tmpService = this.service; + Meteor.call('setAvatarFromService', this.blob, this.contentType, this.service, function(err) { + if (err && err.details.timeToReset && err.details.timeToReset) { + toastr.error(t('error-too-many-requests', { + seconds: parseInt(err.details.timeToReset / 1000) + })); + } else { + toastr.success(t('Avatar_changed_successfully')); + RocketChat.callbacks.run('userAvatarSet', tmpService); + } + }); + } + }, + 'click .login-with-service'(event, template) { + const loginWithService = `loginWith${ _.capitalize(this) }`; + const serviceConfig = {}; + Meteor[loginWithService](serviceConfig, function(error) { + if (error && error.error) { + if (error.error === 'github-no-public-email') { + return alert(t('github_no_public_email')); + } + console.log(error); + return toastr.error(error.message); + } + template.getSuggestions(); + }); + }, + 'change .avatar-file-input'(event, template) { + const e = event.originalEvent || event; + let files = e.target.files; + if (!files || files.length === 0) { + files = (e.dataTransfer && e.dataTransfer.files) || []; + } + Object.keys(files).forEach(key => { + const blob = files[key]; + if (!/image\/.+/.test(blob.type)) { + return; + } + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = function() { + template.upload.set({ + service: 'upload', + contentType: blob.type, + blob: reader.result + }); + RocketChat.callbacks.run('userAvatarSet', 'upload'); + }; + }); + } +}); diff --git a/packages/rocketchat-ui-account/package.js b/packages/rocketchat-ui-account/package.js index 29966722f51b55ef5bbc9d3e255079ca26454ef4..b461ce56a88373206c334e56bd41339f48132cca 100644 --- a/packages/rocketchat-ui-account/package.js +++ b/packages/rocketchat-ui-account/package.js @@ -14,7 +14,6 @@ Package.onUse(function(api) { api.use([ 'ecmascript', 'templating', - 'coffeescript', 'underscore', 'rocketchat:lib', 'sha' @@ -27,12 +26,12 @@ Package.onUse(function(api) { api.addFiles('client/avatar/avatar.html', 'client'); api.addFiles('client/avatar/prompt.html', 'client'); - api.addFiles('client/account.coffee', 'client'); - api.addFiles('client/accountFlex.coffee', 'client'); - api.addFiles('client/accountPreferences.coffee', 'client'); - api.addFiles('client/accountProfile.coffee', 'client'); - api.addFiles('client/avatar/avatar.coffee', 'client'); - api.addFiles('client/avatar/prompt.coffee', 'client'); + api.addFiles('client/account.js', 'client'); + api.addFiles('client/accountFlex.js', 'client'); + api.addFiles('client/accountPreferences.js', 'client'); + api.addFiles('client/accountProfile.js', 'client'); + api.addFiles('client/avatar/avatar.js', 'client'); + api.addFiles('client/avatar/prompt.js', 'client'); // api.addAssets('styles/side-nav.less', 'client'); }); diff --git a/packages/rocketchat-ui-admin/client/admin.coffee b/packages/rocketchat-ui-admin/client/admin.coffee deleted file mode 100644 index 9ca4269d86cf41f8d2460c560c767d9b5918e0b1..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-admin/client/admin.coffee +++ /dev/null @@ -1,494 +0,0 @@ -import toastr from 'toastr' -TempSettings = new Mongo.Collection null -RocketChat.TempSettings = TempSettings - -getDefaultSetting = (settingId) -> - return RocketChat.settings.collectionPrivate.findOne({_id: settingId}) - -setFieldValue = (settingId, value, type, editor) -> - input = $('.page-settings').find('[name="' + settingId + '"]') - - switch type - when 'boolean' - $('.page-settings').find('[name="' + settingId + '"][value="' + Number(value) + '"]').prop('checked', true).change() - when 'code' - input.next()[0].CodeMirror.setValue(value) - when 'color' - input.parents('.horizontal').find('select[name="color-editor"]').val(editor).change() - input.val(value).change() - - if editor is 'color' - new jscolor(input) - when 'roomPick' - selectedRooms = Template.instance().selectedRooms.get() - selectedRooms[settingId] = value - Template.instance().selectedRooms.set(selectedRooms) - TempSettings.update {_id: settingId}, - $set: - value: value - changed: JSON.stringify(RocketChat.settings.collectionPrivate.findOne(settingId).value) isnt JSON.stringify(value) - else - input.val(value).change() - -Template.admin.onCreated -> - if not RocketChat.settings.cachedCollectionPrivate? - RocketChat.settings.cachedCollectionPrivate = new RocketChat.CachedCollection({ name: 'private-settings', eventType: 'onLogged' }) - RocketChat.settings.collectionPrivate = RocketChat.settings.cachedCollectionPrivate.collection - RocketChat.settings.cachedCollectionPrivate.init() - - this.selectedRooms = new ReactiveVar {} - - RocketChat.settings.collectionPrivate.find().observe - added: (data) => - selectedRooms = this.selectedRooms.get() - if data.type is 'roomPick' - selectedRooms[data._id] = data.value - this.selectedRooms.set(selectedRooms) - TempSettings.insert data - changed: (data) => - selectedRooms = this.selectedRooms.get() - if data.type is 'roomPick' - selectedRooms[data._id] = data.value - this.selectedRooms.set(selectedRooms) - TempSettings.update data._id, data - removed: (data) => - selectedRooms = this.selectedRooms.get() - if data.type is 'roomPick' - delete selectedRooms[data._id] - this.selectedRooms.set(selectedRooms) - TempSettings.remove data._id - -Template.admin.onDestroyed -> - TempSettings.remove {} - -Template.admin.helpers - languages: -> - languages = TAPi18n.getLanguages() - result = [] - for key, language of languages - result.push _.extend(language, { key: key }) - result = _.sortBy(result, 'key') - result.unshift { - "name": "Default", - "en": "Default", - "key": "" - } - return result; - - appLanguage: (key) -> - return (RocketChat.settings.get('Language'))?.split('-').shift().toLowerCase() is key - - group: -> - groupId = FlowRouter.getParam('group') - group = RocketChat.settings.collectionPrivate.findOne { _id: groupId, type: 'group' } - - if not group - return - - settings = RocketChat.settings.collectionPrivate.find({ group: groupId }, {sort: {section: 1, sorter: 1, i18nLabel: 1}}).fetch() - - sections = {} - for setting in settings - if setting.i18nDefaultQuery? - if _.isString(setting.i18nDefaultQuery) - i18nDefaultQuery = JSON.parse(setting.i18nDefaultQuery) - else - i18nDefaultQuery = setting.i18nDefaultQuery - - if not _.isArray(i18nDefaultQuery) - i18nDefaultQuery = [i18nDefaultQuery] - - found = 0 - for item in i18nDefaultQuery - if RocketChat.settings.collectionPrivate.findOne(item)? - setting.value = TAPi18n.__(setting._id + '_Default') - - sections[setting.section or ''] ?= [] - sections[setting.section or ''].push setting - - group.sections = [] - for key, value of sections - group.sections.push - section: key - settings: value - - return group - - i18nDefaultValue: -> - return TAPi18n.__(@_id + '_Default') - - isDisabled: -> - if @blocked - return { disabled: 'disabled' } - - if not @enableQuery? - return {} - - if _.isString(@enableQuery) - enableQuery = JSON.parse(@enableQuery) - else - enableQuery = @enableQuery - - if not _.isArray(enableQuery) - enableQuery = [enableQuery] - - found = 0 - for item in enableQuery - if TempSettings.findOne(item)? - found++ - - return if found is enableQuery.length then {} else {disabled: 'disabled'} - - isReadonly: -> - if @readonly is true - return { readonly: 'readonly' } - - hasChanges: (section) -> - group = FlowRouter.getParam('group') - - query = - group: group - changed: true - - if section? - if section is '' - query.$or = [ - {section: ''} - {section: {$exists: false}} - ] - else - query.section = section - - return TempSettings.find(query).count() > 0 - - isSettingChanged: (id) -> - return TempSettings.findOne({_id: id}, {fields: {changed: 1}}).changed - - translateSection: (section) -> - if section.indexOf(':') > -1 - return section - - return t(section) - - label: -> - label = @i18nLabel or @_id - return TAPi18n.__ label if label - - description: -> - description = TAPi18n.__ @i18nDescription if @i18nDescription - if description? and description isnt @i18nDescription - return description - - sectionIsCustomOAuth: (section) -> - return /^Custom OAuth:\s.+/.test section - - callbackURL: (section) -> - id = s.strRight(section, 'Custom OAuth: ').toLowerCase() - return Meteor.absoluteUrl('_oauth/' + id) - - relativeUrl: (url) -> - return Meteor.absoluteUrl(url) - - selectedOption: (_id, val) -> - return RocketChat.settings.collectionPrivate.findOne({_id: _id})?.value is val - - random: -> - return Random.id() - - getEditorOptions: (readOnly = false) -> - return {} = - lineNumbers: true - mode: this.code or "javascript" - gutters: [ - "CodeMirror-linenumbers" - "CodeMirror-foldgutter" - ] - foldGutter: true - matchBrackets: true - autoCloseBrackets: true - matchTags: true, - showTrailingSpace: true - highlightSelectionMatches: true - readOnly: readOnly - - setEditorOnBlur: (_id) -> - Meteor.defer -> - return if not $('.code-mirror-box[data-editor-id="'+_id+'"] .CodeMirror')[0] - - codeMirror = $('.code-mirror-box[data-editor-id="'+_id+'"] .CodeMirror')[0].CodeMirror - if codeMirror.changeAdded is true - return - - onChange = -> - value = codeMirror.getValue() - TempSettings.update {_id: _id}, - $set: - value: value - changed: RocketChat.settings.collectionPrivate.findOne(_id).value isnt value - - onChangeDelayed = _.debounce onChange, 500 - - codeMirror.on 'change', onChangeDelayed - codeMirror.changeAdded = true - - return - - assetAccept: (fileConstraints) -> - if fileConstraints.extensions?.length > 0 - return '.' + fileConstraints.extensions.join(', .') - - autocompleteRoom: -> - return { - limit: 10 - # inputDelay: 300 - rules: [ - { - # @TODO maybe change this 'collection' and/or template - collection: 'CachedChannelList' - subscription: 'channelAndPrivateAutocomplete' - field: 'name' - template: Template.roomSearch - noMatchTemplate: Template.roomSearchEmpty - matchAll: true - selector: (match) -> - return { name: match } - sort: 'name' - } - ] - } - - selectedRooms: -> - return Template.instance().selectedRooms.get()[this._id] or [] - - getColorVariable: (color) -> - return color.replace(/theme-color-/, '@') - - showResetButton: -> - setting = TempSettings.findOne({ _id: @_id }, { fields: { value: 1, packageValue: 1 } }) - return @type isnt 'asset' and setting.value isnt setting.packageValue and not @blocked - -Template.admin.events - "change .input-monitor, keyup .input-monitor": _.throttle((e, t) -> - value = _.trim $(e.target).val() - - switch @type - when 'int' - value = parseInt(value) - when 'boolean' - value = value is "1" - - TempSettings.update {_id: @_id}, - $set: - value: value - changed: RocketChat.settings.collectionPrivate.findOne(@_id).value isnt value - , 500) - - "change select[name=color-editor]": (e, t) -> - value = _.trim $(e.target).val() - TempSettings.update {_id: @_id}, - $set: - editor: value - - "click .submit .discard": -> - group = FlowRouter.getParam('group') - - query = - group: group - changed: true - - settings = TempSettings.find(query, {fields: {_id: 1, value: 1, packageValue: 1}}).fetch() - - settings.forEach (setting) -> - oldSetting = RocketChat.settings.collectionPrivate.findOne({_id: setting._id}, {fields: {value: 1, type:1, editor: 1}}) - - setFieldValue(setting._id, oldSetting.value, oldSetting.type, oldSetting.editor) - - "click .reset-setting": (e, t) -> - e.preventDefault(); - settingId = $(e.target).data('setting') - if typeof settingId is 'undefined' then settingId = $(e.target).parent().data('setting') - - defaultValue = getDefaultSetting(settingId) - - setFieldValue(settingId, defaultValue.packageValue, defaultValue.type, defaultValue.editor) - - "click .reset-group": (e, t) -> - e.preventDefault(); - group = FlowRouter.getParam('group') - section = $(e.target).data('section') - - if section is "" - settings = TempSettings.find({group: group, section: {$exists: false}}, {fields: {_id: 1}}).fetch() - else - settings = TempSettings.find({group: group, section: section}, {fields: {_id: 1}}).fetch() - - settings.forEach (setting) -> - defaultValue = getDefaultSetting(setting._id) - setFieldValue(setting._id, defaultValue.packageValue, defaultValue.type, defaultValue.editor) - - TempSettings.update {_id: setting._id}, - $set: - value: defaultValue.packageValue - changed: RocketChat.settings.collectionPrivate.findOne(setting._id).value isnt defaultValue.packageValue - - "click .submit .save": (e, t) -> - group = FlowRouter.getParam('group') - - query = - group: group - changed: true - - settings = TempSettings.find(query, {fields: {_id: 1, value: 1, editor: 1}}).fetch() - - if not _.isEmpty settings - RocketChat.settings.batchSet settings, (err, success) -> - return handleError(err) if err - TempSettings.update({changed: true}, {$unset: {changed: 1}}) - toastr.success TAPi18n.__ 'Settings_updated' - - "click .submit .refresh-clients": (e, t) -> - Meteor.call 'refreshClients', -> - toastr.success TAPi18n.__ 'Clients_will_refresh_in_a_few_seconds' - - "click .submit .add-custom-oauth": (e, t) -> - config = - title: TAPi18n.__ 'Add_custom_oauth' - text: TAPi18n.__ 'Give_a_unique_name_for_the_custom_oauth' - type: "input", - showCancelButton: true, - closeOnConfirm: true, - inputPlaceholder: TAPi18n.__ 'Custom_oauth_unique_name' - - swal config, (inputValue) -> - if inputValue is false - return false - - if inputValue is "" - swal.showInputError TAPi18n.__ 'Name_cant_be_empty' - return false - - Meteor.call 'addOAuthService', inputValue, (err) -> - if err - handleError(err) - - "click .submit .refresh-oauth": (e, t) -> - toastr.info TAPi18n.__ 'Refreshing' - Meteor.call 'refreshOAuthService', (err) -> - if err - handleError(err) - else - toastr.success TAPi18n.__ 'Done' - - "click .submit .remove-custom-oauth": (e, t) -> - name = this.section.replace('Custom OAuth: ', '') - config = - title: TAPi18n.__ 'Are_you_sure' - type: "input", - type: 'warning' - showCancelButton: true - confirmButtonColor: '#DD6B55' - confirmButtonText: TAPi18n.__ 'Yes_delete_it' - cancelButtonText: TAPi18n.__ 'Cancel' - closeOnConfirm: true - - swal config, -> - Meteor.call 'removeOAuthService', name - - "click .delete-asset": -> - Meteor.call 'unsetAsset', @asset - - "change input[type=file]": (ev) -> - e = ev.originalEvent or ev - files = e.target.files - if not files or files.length is 0 - files = e.dataTransfer?.files or [] - - for blob in files - toastr.info TAPi18n.__ 'Uploading_file' - - # if @fileConstraints.contentType isnt blob.type - # toastr.error blob.type, TAPi18n.__ 'Invalid_file_type' - # return - - reader = new FileReader() - reader.readAsBinaryString(blob) - reader.onloadend = => - Meteor.call 'setAsset', reader.result, blob.type, @asset, (err, data) -> - if err? - handleError(err) - # toastr.error err.reason, TAPi18n.__ err.error - console.log err - return - - toastr.success TAPi18n.__ 'File_uploaded' - - "click .expand": (e) -> - $(e.currentTarget).closest('.section').removeClass('section-collapsed') - $(e.currentTarget).closest('button').removeClass('expand').addClass('collapse').find('span').text(TAPi18n.__ "Collapse") - $('.CodeMirror').each (index, codeMirror) -> - codeMirror.CodeMirror.refresh() - - "click .collapse": (e) -> - $(e.currentTarget).closest('.section').addClass('section-collapsed') - $(e.currentTarget).closest('button').addClass('expand').removeClass('collapse').find('span').text(TAPi18n.__ "Expand") - - "click button.action": (e) -> - if @type isnt 'action' - return - - Meteor.call @value, (err, data) -> - if err? - err.details = _.extend(err.details || {}, errorTitle: 'Error') - handleError(err) - return - - args = [data.message].concat data.params - - toastr.success TAPi18n.__.apply(TAPi18n, args), TAPi18n.__('Success') - - "click .button-fullscreen": -> - codeMirrorBox = $('.code-mirror-box[data-editor-id="'+this._id+'"]') - codeMirrorBox.addClass('code-mirror-box-fullscreen content-background-color') - codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh() - - "click .button-restore": -> - codeMirrorBox = $('.code-mirror-box[data-editor-id="'+this._id+'"]') - codeMirrorBox.removeClass('code-mirror-box-fullscreen content-background-color') - codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh() - - 'autocompleteselect .autocomplete': (event, instance, doc) -> - selectedRooms = instance.selectedRooms.get() - selectedRooms[this.id] = (selectedRooms[this.id] || []).concat doc - instance.selectedRooms.set selectedRooms - value = selectedRooms[this.id] - TempSettings.update {_id: this.id}, - $set: - value: value - changed: JSON.stringify(RocketChat.settings.collectionPrivate.findOne(this.id).value) isnt JSON.stringify(value) - event.currentTarget.value = '' - event.currentTarget.focus() - - 'click .remove-room': (event, instance) -> - docId = this._id - settingId = event.currentTarget.getAttribute('data-setting') - selectedRooms = instance.selectedRooms.get() - selectedRooms[settingId] = _.reject(selectedRooms[settingId] || [], (setting) -> setting._id is docId) - instance.selectedRooms.set selectedRooms - value = selectedRooms[settingId] - TempSettings.update {_id: settingId}, - $set: - value: value - changed: JSON.stringify(RocketChat.settings.collectionPrivate.findOne(settingId).value) isnt JSON.stringify(value) - -Template.admin.onRendered -> - Tracker.afterFlush -> - SideNav.setFlex "adminFlex" - SideNav.openFlex() - - Tracker.autorun -> - hasColor = TempSettings.findOne { group: FlowRouter.getParam('group'), type: 'color' }, { fields: { _id: 1 } } - if hasColor - Meteor.setTimeout -> - $('.colorpicker-input').each (index, el) -> - new jscolor(el) - , 400 diff --git a/packages/rocketchat-ui-admin/client/admin.js b/packages/rocketchat-ui-admin/client/admin.js new file mode 100644 index 0000000000000000000000000000000000000000..21568e61fd05a32a46f5b21b323b3f5000ecefb5 --- /dev/null +++ b/packages/rocketchat-ui-admin/client/admin.js @@ -0,0 +1,576 @@ +/*globals jscolor, i18nDefaultQuery */ +import toastr from 'toastr'; +const TempSettings = new Mongo.Collection(null); + +RocketChat.TempSettings = TempSettings; + +const getDefaultSetting = function(settingId) { + return RocketChat.settings.collectionPrivate.findOne({ + _id: settingId + }); +}; + +const setFieldValue = function(settingId, value, type, editor) { + const input = $('.page-settings').find(`[name="${ settingId }"]`); + switch (type) { + case 'boolean': + $('.page-settings').find(`[name="${ settingId }"][value="${ Number(value) }"]`).prop('checked', true).change(); + break; + case 'code': + input.next()[0].CodeMirror.setValue(value); + break; + case 'color': + input.parents('.horizontal').find('select[name="color-editor"]').val(editor).change(); + input.val(value).change(); + if (editor === 'color') { + new jscolor(input); //eslint-disable-line + } + break; + default: + input.val(value).change(); + } +}; + +Template.admin.onCreated(function() { + if (RocketChat.settings.cachedCollectionPrivate == null) { + RocketChat.settings.cachedCollectionPrivate = new RocketChat.CachedCollection({ + name: 'private-settings', + eventType: 'onLogged' + }); + RocketChat.settings.collectionPrivate = RocketChat.settings.cachedCollectionPrivate.collection; + RocketChat.settings.cachedCollectionPrivate.init(); + } + this.selectedRooms = new ReactiveVar({}); + RocketChat.settings.collectionPrivate.find().observe({ + added: (data) => { + const selectedRooms = this.selectedRooms.get(); + if (data.type === 'roomPick') { + selectedRooms[data._id] = data.value; + this.selectedRooms.set(selectedRooms); + } + TempSettings.insert(data); + }, + changed: (data) => { + const selectedRooms = this.selectedRooms.get(); + if (data.type === 'roomPick') { + selectedRooms[data._id] = data.value; + this.selectedRooms.set(selectedRooms); + } + TempSettings.update(data._id, data); + }, + removed: (data) => { + const selectedRooms = this.selectedRooms.get(); + if (data.type === 'roomPick') { + delete selectedRooms[data._id]; + this.selectedRooms.set(selectedRooms); + } + TempSettings.remove(data._id); + } + }); +}); + +Template.admin.onDestroyed(function() { + TempSettings.remove({}); +}); + +Template.admin.helpers({ + languages() { + const languages = TAPi18n.getLanguages(); + + let result = Object.keys(languages).map(key => { + const language = languages[key]; + return _.extend(language, { key }); + }); + + result = _.sortBy(result, 'key'); + result.unshift({ + 'name': 'Default', + 'en': 'Default', + 'key': '' + }); + return result; + }, + appLanguage(key) { + const setting = RocketChat.settings.get('Language'); + return setting && setting.split('-').shift().toLowerCase() === key; + }, + group() { + const groupId = FlowRouter.getParam('group'); + const group = RocketChat.settings.collectionPrivate.findOne({ + _id: groupId, + type: 'group' + }); + if (!group) { + return; + } + const settings = RocketChat.settings.collectionPrivate.find({ group: groupId }, { sort: { section: 1, sorter: 1, i18nLabel: 1 }}).fetch(); + const sections = {}; + + Object.keys(settings).forEach(key => { + const setting = settings[key]; + if (setting.i18nDefaultQuery != null) { + if (_.isString(setting.i18nDefaultQuery)) { + i18nDefaultQuery = JSON.parse(setting.i18nDefaultQuery); + } else { + i18nDefaultQuery = setting.i18nDefaultQuery; + } + if (!_.isArray(i18nDefaultQuery)) { + i18nDefaultQuery = [i18nDefaultQuery]; + } + Object.keys(i18nDefaultQuery).forEach(key => { + const item = i18nDefaultQuery[key]; + if (RocketChat.settings.collectionPrivate.findOne(item) != null) { + setting.value = TAPi18n.__(`${ setting._id }_Default`); + } + }); + } + const settingSection = setting.section || ''; + if (sections[settingSection] == null) { + sections[settingSection] = []; + } + sections[settingSection].push(setting); + }); + + group.sections = Object.keys(sections).map(key =>{ + const value = sections[key]; + return { + section: key, + settings: value + }; + }); + return group; + }, + i18nDefaultValue() { + return TAPi18n.__(`${ this._id }_Default`); + }, + isDisabled() { + let enableQuery; + if (this.blocked) { + return { + disabled: 'disabled' + }; + } + if (this.enableQuery == null) { + return {}; + } + if (_.isString(this.enableQuery)) { + enableQuery = JSON.parse(this.enableQuery); + } else { + enableQuery = this.enableQuery; + } + if (!_.isArray(enableQuery)) { + enableQuery = [enableQuery]; + } + let found = 0; + + Object.keys(enableQuery).forEach(key =>{ + const item = enableQuery[key]; + if (TempSettings.findOne(item) != null) { + found++; + } + }); + if (found === enableQuery.length) { + return {}; + } else { + return { + disabled: 'disabled' + }; + } + }, + isReadonly() { + if (this.readonly === true) { + return { + readonly: 'readonly' + }; + } + }, + hasChanges(section) { + const group = FlowRouter.getParam('group'); + const query = { + group, + changed: true + }; + if (section != null) { + if (section === '') { + query.$or = [ + { + section: '' + }, { + section: { + $exists: false + } + } + ]; + } else { + query.section = section; + } + } + return TempSettings.find(query).count() > 0; + }, + isSettingChanged(id) { + return TempSettings.findOne({ + _id: id + }, { + fields: { + changed: 1 + } + }).changed; + }, + translateSection(section) { + if (section.indexOf(':') > -1) { + return section; + } + return t(section); + }, + label() { + const label = this.i18nLabel || this._id; + if (label) { + return TAPi18n.__(label); + } + }, + description() { + let description; + if (this.i18nDescription) { + description = TAPi18n.__(this.i18nDescription); + } + if ((description != null) && description !== this.i18nDescription) { + return description; + } + }, + sectionIsCustomOAuth(section) { + return /^Custom OAuth:\s.+/.test(section); + }, + callbackURL(section) { + const id = s.strRight(section, 'Custom OAuth: ').toLowerCase(); + return Meteor.absoluteUrl(`_oauth/${ id }`); + }, + relativeUrl(url) { + return Meteor.absoluteUrl(url); + }, + selectedOption(_id, val) { + const option = RocketChat.settings.collectionPrivate.findOne({ _id }); + return option && option.value === val; + }, + random() { + return Random.id(); + }, + getEditorOptions(readOnly = false) { + return { + lineNumbers: true, + mode: this.code || 'javascript', + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + foldGutter: true, + matchBrackets: true, + autoCloseBrackets: true, + matchTags: true, + showTrailingSpace: true, + highlightSelectionMatches: true, + readOnly + }; + }, + setEditorOnBlur(_id) { + Meteor.defer(function() { + if (!$(`.code-mirror-box[data-editor-id="${ _id }"] .CodeMirror`)[0]) { + return; + } + const codeMirror = $(`.code-mirror-box[data-editor-id="${ _id }"] .CodeMirror`)[0].CodeMirror; + if (codeMirror.changeAdded === true) { + return; + } + const onChange = function() { + const value = codeMirror.getValue(); + TempSettings.update({ _id }, { $set: { value, changed: RocketChat.settings.collectionPrivate.findOne(_id).value !== value }}); + }; + const onChangeDelayed = _.debounce(onChange, 500); + codeMirror.on('change', onChangeDelayed); + codeMirror.changeAdded = true; + }); + }, + assetAccept(fileConstraints) { + if (fileConstraints.extensions && fileConstraints.extensions.length) { + return `.${ fileConstraints.extensions.join(', .') }`; + } + }, + autocompleteRoom() { + return { + limit: 10, + //inputDelay: 300 + rules: [ + { + //@TODO maybe change this 'collection' and/or template + collection: 'CachedChannelList', + subscription: 'channelAndPrivateAutocomplete', + field: 'name', + template: Template.roomSearch, + noMatchTemplate: Template.roomSearchEmpty, + matchAll: true, + selector(match) { + return { + name: match + }; + }, + sort: 'name' + } + ] + }; + }, + selectedRooms() { + return Template.instance().selectedRooms.get()[this._id] || []; + }, + getColorVariable(color) { + return color.replace(/theme-color-/, '@'); + }, + showResetButton() { + const setting = TempSettings.findOne({ _id: this._id }, { fields: { value: 1, packageValue: 1 }}); + return this.type !== 'asset' && setting.value !== setting.packageValue && !this.blocked; + } +}); + +Template.admin.events({ + 'change .input-monitor, keyup .input-monitor': _.throttle(function(e) { + let value = _.trim($(e.target).val()); + switch (this.type) { + case 'int': + value = parseInt(value); + break; + case 'boolean': + value = value === '1'; + } + TempSettings.update({ + _id: this._id + }, { + $set: { + value, + changed: RocketChat.settings.collectionPrivate.findOne(this._id).value !== value + } + }); + }, 500), + 'change select[name=color-editor]'(e) { + const value = _.trim($(e.target).val()); + TempSettings.update({ _id: this._id }, { $set: { editor: value }}); + }, + 'click .submit .discard'() { + const group = FlowRouter.getParam('group'); + const query = { + group, + changed: true + }; + const settings = TempSettings.find(query, { + fields: { _id: 1, value: 1, packageValue: 1 }}).fetch(); + settings.forEach(function(setting) { + const oldSetting = RocketChat.settings.collectionPrivate.findOne({ _id: setting._id }, { fields: { value: 1, type: 1, editor: 1 }}); + setFieldValue(setting._id, oldSetting.value, oldSetting.type, oldSetting.editor); + }); + }, + 'click .reset-setting'(e) { + e.preventDefault(); + let settingId = $(e.target).data('setting'); + if (typeof settingId === 'undefined') { + settingId = $(e.target).parent().data('setting'); + } + const defaultValue = getDefaultSetting(settingId); + setFieldValue(settingId, defaultValue.packageValue, defaultValue.type, defaultValue.editor); + }, + 'click .reset-group'(e) { + let settings; + e.preventDefault(); + const group = FlowRouter.getParam('group'); + const section = $(e.target).data('section'); + if (section === '') { + settings = TempSettings.find({ group, section: { $exists: false }}, { fields: { _id: 1 }}).fetch(); + } else { + settings = TempSettings.find({ group, section }, { fields: { _id: 1 }}).fetch(); + } + settings.forEach(function(setting) { + const defaultValue = getDefaultSetting(setting._id); + setFieldValue(setting._id, defaultValue.packageValue, defaultValue.type, defaultValue.editor); + TempSettings.update({_id: setting._id }, { + $set: { + value: defaultValue.packageValue, + changed: RocketChat.settings.collectionPrivate.findOne(setting._id).value !== defaultValue.packageValue + } + }); + }); + }, + 'click .submit .save'() { + const group = FlowRouter.getParam('group'); + const query = { group, changed: true }; + const settings = TempSettings.find(query, { fields: { _id: 1, value: 1, editor: 1 }}).fetch(); + if (!_.isEmpty(settings)) { + RocketChat.settings.batchSet(settings, function(err) { + if (err) { + return handleError(err); + } + TempSettings.update({ changed: true }, { $unset: { changed: 1 }}); + toastr.success(TAPi18n.__('Settings_updated')); + }); + } + }, + 'click .submit .refresh-clients'() { + Meteor.call('refreshClients', function() { + toastr.success(TAPi18n.__('Clients_will_refresh_in_a_few_seconds')); + }); + }, + 'click .submit .add-custom-oauth'() { + const config = { + title: TAPi18n.__('Add_custom_oauth'), + text: TAPi18n.__('Give_a_unique_name_for_the_custom_oauth'), + type: 'input', + showCancelButton: true, + closeOnConfirm: true, + inputPlaceholder: TAPi18n.__('Custom_oauth_unique_name') + }; + swal(config, function(inputValue) { + if (inputValue === false) { + return false; + } + if (inputValue === '') { + swal.showInputError(TAPi18n.__('Name_cant_be_empty')); + return false; + } + Meteor.call('addOAuthService', inputValue, function(err) { + if (err) { + handleError(err); + } + }); + }); + }, + 'click .submit .refresh-oauth'() { + toastr.info(TAPi18n.__('Refreshing')); + return Meteor.call('refreshOAuthService', function(err) { + if (err) { + return handleError(err); + } else { + return toastr.success(TAPi18n.__('Done')); + } + }); + }, + 'click .submit .remove-custom-oauth'() { + const name = this.section.replace('Custom OAuth: ', ''); + const config = { + title: TAPi18n.__('Are_you_sure'), + type: 'warning', + showCancelButton: true, + confirmButtonColor: '#DD6B55', + confirmButtonText: TAPi18n.__('Yes_delete_it'), + cancelButtonText: TAPi18n.__('Cancel'), + closeOnConfirm: true + }; + swal(config, function() { + Meteor.call('removeOAuthService', name); + }); + }, + 'click .delete-asset'() { + Meteor.call('unsetAsset', this.asset); + }, + 'change input[type=file]'(ev) { + const e = ev.originalEvent || ev; + let files = e.target.files; + if (!files || files.length === 0) { + if (e.dataTransfer && e.dataTransfer.files) { + files = e.dataTransfer.files; + } else { + files = []; + } + } + + Object.keys(files).forEach(key => { + const blob = files[key]; + toastr.info(TAPi18n.__('Uploading_file')); + const reader = new FileReader(); + reader.readAsBinaryString(blob); + reader.onloadend = () => { + return Meteor.call('setAsset', reader.result, blob.type, this.asset, function(err) { + if (err != null) { + handleError(err); + console.log(err); + return; + } + return toastr.success(TAPi18n.__('File_uploaded')); + }); + }; + }); + }, + 'click .expand'(e) { + $(e.currentTarget).closest('.section').removeClass('section-collapsed'); + $(e.currentTarget).closest('button').removeClass('expand').addClass('collapse').find('span').text(TAPi18n.__('Collapse')); + $('.CodeMirror').each(function(index, codeMirror) { + codeMirror.CodeMirror.refresh(); + }); + }, + 'click .collapse'(e) { + $(e.currentTarget).closest('.section').addClass('section-collapsed'); + $(e.currentTarget).closest('button').addClass('expand').removeClass('collapse').find('span').text(TAPi18n.__('Expand')); + }, + 'click button.action'() { + if (this.type !== 'action') { + return; + } + Meteor.call(this.value, function(err, data) { + if (err != null) { + err.details = _.extend(err.details || {}, { + errorTitle: 'Error' + }); + handleError(err); + return; + } + const args = [data.message].concat(data.params); + toastr.success(TAPi18n.__.apply(TAPi18n, args), TAPi18n.__('Success')); + }); + }, + 'click .button-fullscreen'() { + const codeMirrorBox = $(`.code-mirror-box[data-editor-id="${ this._id }"]`); + codeMirrorBox.addClass('code-mirror-box-fullscreen content-background-color'); + codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh(); + }, + 'click .button-restore'() { + const codeMirrorBox = $(`.code-mirror-box[data-editor-id="${ this._id }"]`); + codeMirrorBox.removeClass('code-mirror-box-fullscreen content-background-color'); + codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh(); + }, + 'autocompleteselect .autocomplete'(event, instance, doc) { + const selectedRooms = instance.selectedRooms.get(); + selectedRooms[this.id] = (selectedRooms[this.id] || []).concat(doc); + instance.selectedRooms.set(selectedRooms); + const value = selectedRooms[this.id]; + TempSettings.update({ _id: this.id }, { $set: { value, changed: RocketChat.settings.collectionPrivate.findOne(this.id).value !== value }}); + event.currentTarget.value = ''; + event.currentTarget.focus(); + }, + 'click .remove-room'(event, instance) { + const docId = this._id; + const settingId = event.currentTarget.getAttribute('data-setting'); + const selectedRooms = instance.selectedRooms.get(); + selectedRooms[settingId] = _.reject(selectedRooms[settingId] || [], function(setting) { + return setting._id === docId; + }); + instance.selectedRooms.set(selectedRooms); + const value = selectedRooms[settingId]; + TempSettings.update({ _id: settingId }, { + $set: { + value, + changed: RocketChat.settings.collectionPrivate.findOne(settingId).value !== value + } + }); + } +}); + +Template.admin.onRendered(function() { + Tracker.afterFlush(function() { + SideNav.setFlex('adminFlex'); + SideNav.openFlex(); + }); + Tracker.autorun(function() { + const hasColor = TempSettings.findOne({ + group: FlowRouter.getParam('group'), + type: 'color' + }, { fields: { _id: 1 }}); + if (hasColor) { + Meteor.setTimeout(function() { + $('.colorpicker-input').each(function(index, el) { + new jscolor(el); //eslint-disable-line + }); + }, 400); + } + }); +}); diff --git a/packages/rocketchat-ui-admin/client/adminFlex.coffee b/packages/rocketchat-ui-admin/client/adminFlex.coffee deleted file mode 100644 index a7f30f2acfd397d33c646b3b2e26148b98470a2e..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-admin/client/adminFlex.coffee +++ /dev/null @@ -1,62 +0,0 @@ -Template.adminFlex.onCreated -> - @settingsFilter = new ReactiveVar('') - - if not RocketChat.settings.cachedCollectionPrivate? - RocketChat.settings.cachedCollectionPrivate = new RocketChat.CachedCollection({ name: 'private-settings', eventType: 'onLogged' }) - RocketChat.settings.collectionPrivate = RocketChat.settings.cachedCollectionPrivate.collection - RocketChat.settings.cachedCollectionPrivate.init() - -label = -> - return TAPi18n.__(@i18nLabel or @_id) - -Template.adminFlex.helpers - groups: -> - filter = Template.instance().settingsFilter.get() - - query = - type: 'group' - - if filter - filterRegex = new RegExp(_.escapeRegExp(filter), 'i') - - records = RocketChat.settings.collectionPrivate.find().fetch() - groups = [] - records = records.forEach (record) -> - if filterRegex.test(TAPi18n.__(record.i18nLabel or record._id)) - groups.push(record.group or record._id) - - groups = _.unique(groups) - if groups.length > 0 - query._id = - $in: groups - RocketChat.settings.collectionPrivate.find(query).fetch() - .map (el) => - el.label = label.apply(el) - return el - .sort (a, b) => - if a.label.toLowerCase() >= b.label.toLowerCase() then 1 else -1 - - label: label - - adminBoxOptions: -> - return RocketChat.AdminBox.getOptions() - - -Template.adminFlex.events - 'mouseenter header': -> - SideNav.overArrow() - - 'mouseleave header': -> - SideNav.leaveArrow() - - 'click header': -> - SideNav.closeFlex() - - 'click .cancel-settings': -> - SideNav.closeFlex() - - 'click .admin-link': -> - menu.close() - - 'keyup [name=settings-search]': (e, t) -> - t.settingsFilter.set(e.target.value) diff --git a/packages/rocketchat-ui-admin/client/adminFlex.js b/packages/rocketchat-ui-admin/client/adminFlex.js new file mode 100644 index 0000000000000000000000000000000000000000..ce1d6bce5b7f517e2cd3e78e72c9a389e0d8a14d --- /dev/null +++ b/packages/rocketchat-ui-admin/client/adminFlex.js @@ -0,0 +1,76 @@ +/* globals menu */ +Template.adminFlex.onCreated(function() { + this.settingsFilter = new ReactiveVar(''); + if (RocketChat.settings.cachedCollectionPrivate == null) { + RocketChat.settings.cachedCollectionPrivate = new RocketChat.CachedCollection({ + name: 'private-settings', + eventType: 'onLogged' + }); + RocketChat.settings.collectionPrivate = RocketChat.settings.cachedCollectionPrivate.collection; + RocketChat.settings.cachedCollectionPrivate.init(); + } +}); + +const label = function() { + return TAPi18n.__(this.i18nLabel || this._id); +}; + +Template.adminFlex.helpers({ + groups() { + const filter = Template.instance().settingsFilter.get(); + const query = { + type: 'group' + }; + if (filter) { + const filterRegex = new RegExp(_.escapeRegExp(filter), 'i'); + const records = RocketChat.settings.collectionPrivate.find().fetch(); + let groups = []; + records.forEach(function(record) { + if (filterRegex.test(TAPi18n.__(record.i18nLabel || record._id))) { + groups.push(record.group || record._id); + } + }); + groups = _.unique(groups); + if (groups.length > 0) { + query._id = { + $in: groups + }; + } + } + return RocketChat.settings.collectionPrivate.find(query).fetch().map(function(el) { + el.label = label.apply(el); + return el; + }).sort(function(a, b) { + if (a.label.toLowerCase() >= b.label.toLowerCase()) { + return 1; + } else { + return -1; + } + }); + }, + label, + adminBoxOptions() { + return RocketChat.AdminBox.getOptions(); + } +}); + +Template.adminFlex.events({ + 'mouseenter header'() { + SideNav.overArrow(); + }, + 'mouseleave header'() { + SideNav.leaveArrow(); + }, + 'click header'() { + SideNav.closeFlex(); + }, + 'click .cancel-settings'() { + SideNav.closeFlex(); + }, + 'click .admin-link'() { + menu.close(); + }, + 'keyup [name=settings-search]'(e, t) { + t.settingsFilter.set(e.target.value); + } +}); diff --git a/packages/rocketchat-ui-admin/client/adminInfo.coffee b/packages/rocketchat-ui-admin/client/adminInfo.coffee deleted file mode 100644 index 51cea0151143e0ea030a56660898f75a0c0f4049..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-admin/client/adminInfo.coffee +++ /dev/null @@ -1,64 +0,0 @@ -import moment from 'moment' - -Template.adminInfo.helpers - isReady: -> - return Template.instance().ready.get() - statistics: -> - return Template.instance().statistics.get() - inGB: (size) -> - if size > 1073741824 - return _.numberFormat(size / 1024 / 1024 / 1024, 2) + ' GB' - return _.numberFormat(size / 1024 / 1024, 2) + ' MB' - humanReadableTime: (time) -> - days = Math.floor time / 86400 - hours = Math.floor (time % 86400) / 3600 - minutes = Math.floor ((time % 86400) % 3600) / 60 - seconds = Math.floor ((time % 86400) % 3600) % 60 - out = "" - if days > 0 - out += "#{days} #{TAPi18n.__ 'days'}, " - if hours > 0 - out += "#{hours} #{TAPi18n.__ 'hours'}, " - if minutes > 0 - out += "#{minutes} #{TAPi18n.__ 'minutes'}, " - if seconds > 0 - out += "#{seconds} #{TAPi18n.__ 'seconds'}" - return out - formatDate: (date) -> - if date - return moment(date).format("LLL") - numFormat: (number) -> - return _.numberFormat(number, 2) - info: -> - return RocketChat.Info - build: -> - return RocketChat.Info?.compile || RocketChat.Info?.build - -Template.adminInfo.events - 'click .refresh': (e, instance) -> - instance.ready.set false - Meteor.call 'getStatistics', true, (error, statistics) -> - instance.ready.set true - if error - handleError(error) - else - instance.statistics.set statistics - -Template.adminInfo.onRendered -> - Tracker.afterFlush -> - SideNav.setFlex "adminFlex" - SideNav.openFlex() - -Template.adminInfo.onCreated -> - instance = @ - @statistics = new ReactiveVar {} - @ready = new ReactiveVar false - - if RocketChat.authz.hasAllPermission('view-statistics') - Meteor.call 'getStatistics', (error, statistics) -> - instance.ready.set true - if error - handleError(error) - else - instance.statistics.set statistics - diff --git a/packages/rocketchat-ui-admin/client/adminInfo.html b/packages/rocketchat-ui-admin/client/adminInfo.html index e3d5358d08ebf8ff3b1168349e6ea421d504f01a..c900a7c6dac682b4b03ce3be339b1d69a33a759b 100644 --- a/packages/rocketchat-ui-admin/client/adminInfo.html +++ b/packages/rocketchat-ui-admin/client/adminInfo.html @@ -226,6 +226,50 @@ </tr> </table> + {{#if instances}} + <h3>{{_ "Broadcast_Connected_Instances"}}</h3> + {{#each instances}} + <table class="statistics-table secondary-background-color"> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Address"}}</th> + <td class="border-component-color">{{address}}</td> + </tr> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Auth"}}</th> + <td class="border-component-color">{{broadcastAuth}}</td> + </tr> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Current_Status"}} > {{_ "Connected"}}</th> + <td class="border-component-color">{{currentStatus.connected}}</td> + </tr> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Current_Status"}} > {{_ "Retry_Count"}}</th> + <td class="border-component-color">{{currentStatus.retryCount}}</td> + </tr> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Current_Status"}} > {{_ "Status"}}</th> + <td class="border-component-color">{{currentStatus.status}}</td> + </tr> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Instance_Record"}} > {{_ "ID"}}</th> + <td class="border-component-color">{{instanceRecord._id}}</td> + </tr> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Instance_Record"}} > {{_ "PID"}}</th> + <td class="border-component-color">{{instanceRecord.pid}}</td> + </tr> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Instance_Record"}} > {{_ "Created_at"}}</th> + <td class="border-component-color">{{formatDate instanceRecord._createdAt}}</td> + </tr> + <tr class="admin-table-row"> + <th class="content-background-color border-component-color">{{_ "Instance_Record"}} > {{_ "Updated_at"}}</th> + <td class="border-component-color">{{formatDate instanceRecord._updatedAt}}</td> + </tr> + </table> + {{/each}} + {{/if}} + <button type="button" class="button primary refresh">Refresh</button> {{else}} {{_ "Loading..."}} diff --git a/packages/rocketchat-ui-admin/client/adminInfo.js b/packages/rocketchat-ui-admin/client/adminInfo.js new file mode 100644 index 0000000000000000000000000000000000000000..a3a2874ed278c3cf7d7c381869f35c9b6bee4418 --- /dev/null +++ b/packages/rocketchat-ui-admin/client/adminInfo.js @@ -0,0 +1,100 @@ +import moment from 'moment'; + +Template.adminInfo.helpers({ + isReady() { + return Template.instance().ready.get(); + }, + statistics() { + return Template.instance().statistics.get(); + }, + instances() { + return Template.instance().instances.get(); + }, + inGB(size) { + if (size > 1073741824) { + return `${ _.numberFormat(size / 1024 / 1024 / 1024, 2) } GB`; + } + return `${ _.numberFormat(size / 1024 / 1024, 2) } MB`; + }, + humanReadableTime(time) { + const days = Math.floor(time / 86400); + const hours = Math.floor((time % 86400) / 3600); + const minutes = Math.floor(((time % 86400) % 3600) / 60); + const seconds = Math.floor(((time % 86400) % 3600) % 60); + let out = ''; + if (days > 0) { + out += `${ days } ${ TAPi18n.__('days') }, `; + } + if (hours > 0) { + out += `${ hours } ${ TAPi18n.__('hours') }, `; + } + if (minutes > 0) { + out += `${ minutes } ${ TAPi18n.__('minutes') }, `; + } + if (seconds > 0) { + out += `${ seconds } ${ TAPi18n.__('seconds') }`; + } + return out; + }, + formatDate(date) { + if (date) { + return moment(date).format('LLL'); + } + }, + numFormat(number) { + return _.numberFormat(number, 2); + }, + info() { + return RocketChat.Info; + }, + build() { + return RocketChat.Info && RocketChat.Info.compile || RocketChat.Info && RocketChat.Info.build; + } +}); + +Template.adminInfo.events({ + 'click .refresh'(e, instance) { + instance.ready.set(false); + return Meteor.call('getStatistics', true, function(error, statistics) { + instance.ready.set(true); + if (error) { + return handleError(error); + } else { + return instance.statistics.set(statistics); + } + }); + } +}); + +Template.adminInfo.onRendered(function() { + return Tracker.afterFlush(function() { + SideNav.setFlex('adminFlex'); + return SideNav.openFlex(); + }); +}); + +Template.adminInfo.onCreated(function() { + const instance = this; + this.statistics = new ReactiveVar({}); + this.instances = new ReactiveVar({}); + this.ready = new ReactiveVar(false); + if (RocketChat.authz.hasAllPermission('view-statistics')) { + Meteor.call('getStatistics', function(error, statistics) { + instance.ready.set(true); + if (error) { + handleError(error); + } else { + instance.statistics.set(statistics); + } + }); + + Meteor.call('instances/get', function(error, instances) { + instance.ready.set(true); + if (error) { + handleError(error); + } else { + instance.instances.set(instances); + } + }); + } +}); diff --git a/packages/rocketchat-ui-admin/client/rooms/adminRoomInfo.coffee b/packages/rocketchat-ui-admin/client/rooms/adminRoomInfo.coffee deleted file mode 100644 index 9667b356681d7e4ac6a1d98e7b58bb8de9ebc934..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-admin/client/rooms/adminRoomInfo.coffee +++ /dev/null @@ -1,186 +0,0 @@ -import toastr from 'toastr' -Template.adminRoomInfo.helpers - selectedRoom: -> - return Session.get 'adminRoomsSelected' - canEdit: -> - return RocketChat.authz.hasAllPermission('edit-room', @rid) - editing: (field) -> - return Template.instance().editing.get() is field - notDirect: -> - return AdminChatRoom.findOne(@rid, { fields: { t: 1 }})?.t isnt 'd' - roomType: -> - return AdminChatRoom.findOne(@rid, { fields: { t: 1 }})?.t - channelSettings: -> - return RocketChat.ChannelSettings.getOptions(null, 'admin-room') - roomTypeDescription: -> - roomType = AdminChatRoom.findOne(@rid, { fields: { t: 1 }})?.t - if roomType is 'c' - return t('Channel') - else if roomType is 'p' - return t('Private_Group') - roomName: -> - return AdminChatRoom.findOne(@rid, { fields: { name: 1 }})?.name - roomTopic: -> - return AdminChatRoom.findOne(@rid, { fields: { topic: 1 }})?.topic - archivationState: -> - return AdminChatRoom.findOne(@rid, { fields: { archived: 1 }})?.archived - archivationStateDescription: -> - archivationState = AdminChatRoom.findOne(@rid, { fields: { archived: 1 }})?.archived - if archivationState is true - return t('Room_archivation_state_true') - else - return t('Room_archivation_state_false') - canDeleteRoom: -> - roomType = AdminChatRoom.findOne(@rid, { fields: { t: 1 }})?.t - return roomType? and RocketChat.authz.hasAtLeastOnePermission("delete-#{roomType}") - readOnly: -> - room = AdminChatRoom.findOne(@rid, { fields: { ro: 1 }}) - return room?.ro - readOnlyDescription: -> - room = AdminChatRoom.findOne(@rid, { fields: { ro: 1 }}) - readOnly = room?.ro - if readOnly is true - return t('True') - else - return t('False') - -Template.adminRoomInfo.events - 'click .delete': -> - swal { - title: t('Are_you_sure') - text: t('Delete_Room_Warning') - type: 'warning' - showCancelButton: true - confirmButtonColor: '#DD6B55' - confirmButtonText: t('Yes_delete_it') - cancelButtonText: t('Cancel') - closeOnConfirm: false - html: false - }, => - swal.disableButtons() - - Meteor.call 'eraseRoom', @rid, (error, result) -> - if error - handleError(error) - swal.enableButtons() - else - swal - title: t('Deleted') - text: t('Room_has_been_deleted') - type: 'success' - timer: 2000 - showConfirmButton: false - - 'keydown input[type=text]': (e, t) -> - if e.keyCode is 13 - e.preventDefault() - t.saveSetting(@rid) - - 'click [data-edit]': (e, t) -> - e.preventDefault() - t.editing.set($(e.currentTarget).data('edit')) - setTimeout (-> t.$('input.editing').focus().select()), 100 - - 'click .cancel': (e, t) -> - e.preventDefault() - t.editing.set() - - 'click .save': (e, t) -> - e.preventDefault() - t.saveSetting(@rid) - -Template.adminRoomInfo.onCreated -> - @editing = new ReactiveVar - - @validateRoomType = (rid) => - type = @$('input[name=roomType]:checked').val() - if type not in ['c', 'p'] - toastr.error t('error-invalid-room-type', { type: type }) - return true - - @validateRoomName = (rid) => - room = AdminChatRoom.findOne rid - - if not RocketChat.authz.hasAllPermission('edit-room', rid) or room.t not in ['c', 'p'] - toastr.error t('error-not-allowed') - return false - - name = $('input[name=roomName]').val() - - try - nameValidation = new RegExp '^' + RocketChat.settings.get('UTF8_Names_Validation') + '$' - catch - nameValidation = new RegExp '^[0-9a-zA-Z-_.]+$' - - if not nameValidation.test name - toastr.error t('error-invalid-room-name', { room_name: name }) - return false - - return true - - @validateRoomTopic = (rid) => - return true - - @saveSetting = (rid) => - switch @editing.get() - when 'roomName' - if @validateRoomName(rid) - RocketChat.callbacks.run 'roomNameChanged', AdminChatRoom.findOne(rid) - Meteor.call 'saveRoomSettings', rid, 'roomName', @$('input[name=roomName]').val(), (err, result) -> - if err - return handleError(err) - toastr.success TAPi18n.__ 'Room_name_changed_successfully' - when 'roomTopic' - if @validateRoomTopic(rid) - Meteor.call 'saveRoomSettings', rid, 'roomTopic', @$('input[name=roomTopic]').val(), (err, result) -> - if err - return handleError(err) - toastr.success TAPi18n.__ 'Room_topic_changed_successfully' - RocketChat.callbacks.run 'roomTopicChanged', AdminChatRoom.findOne(rid) - when 'roomAnnouncement' - if @validateRoomTopic(rid) - Meteor.call 'saveRoomSettings', rid, 'roomAnnouncement', @$('input[name=roomAnnouncement]').val(), (err, result) -> - if err - return handleError(err) - toastr.success TAPi18n.__ 'Room_announcement_changed_successfully' - RocketChat.callbacks.run 'roomAnnouncementChanged', AdminChatRoom.findOne(rid) - when 'roomType' - val = @$('input[name=roomType]:checked').val() - if @validateRoomType(rid) - RocketChat.callbacks.run 'roomTypeChanged', AdminChatRoom.findOne(rid) - saveRoomSettings = => - Meteor.call 'saveRoomSettings', rid, 'roomType', val, (err, result) -> - if err - return handleError(err) - toastr.success TAPi18n.__ 'Room_type_changed_successfully' - unless AdminChatRoom.findOne(rid, { fields: { default: 1 }}).default - return saveRoomSettings() - swal - title: t('Room_default_change_to_private_will_be_default_no_more') - type: 'warning' - showCancelButton: true - confirmButtonColor: '#DD6B55' - confirmButtonText: t('Yes') - cancelButtonText: t('Cancel') - closeOnConfirm: true - html: false - (confirmed) => - return !confirmed || saveRoomSettings() - when 'archivationState' - if @$('input[name=archivationState]:checked').val() is 'true' - if AdminChatRoom.findOne(rid)?.archived isnt true - Meteor.call 'archiveRoom', rid, (err, results) -> - return handleError(err) if err - toastr.success TAPi18n.__ 'Room_archived' - RocketChat.callbacks.run 'archiveRoom', AdminChatRoom.findOne(rid) - else - if AdminChatRoom.findOne(rid)?.archived is true - Meteor.call 'unarchiveRoom', rid, (err, results) -> - return handleError(err) if err - toastr.success TAPi18n.__ 'Room_unarchived' - RocketChat.callbacks.run 'unarchiveRoom', AdminChatRoom.findOne(rid) - when 'readOnly' - Meteor.call 'saveRoomSettings', rid, 'readOnly', @$('input[name=readOnly]:checked').val() is 'true', (err, result) -> - return handleError err if err - toastr.success TAPi18n.__ 'Read_only_changed_successfully' - @editing.set() diff --git a/packages/rocketchat-ui-admin/client/rooms/adminRoomInfo.js b/packages/rocketchat-ui-admin/client/rooms/adminRoomInfo.js new file mode 100644 index 0000000000000000000000000000000000000000..dd2ca493ca4e1d5baa44c00fdf1b7d86016b41f1 --- /dev/null +++ b/packages/rocketchat-ui-admin/client/rooms/adminRoomInfo.js @@ -0,0 +1,258 @@ +/*globals AdminChatRoom */ +import toastr from 'toastr'; +Template.adminRoomInfo.helpers({ + selectedRoom() { + return Session.get('adminRoomsSelected'); + }, + canEdit() { + return RocketChat.authz.hasAllPermission('edit-room', this.rid); + }, + editing(field) { + return Template.instance().editing.get() === field; + }, + notDirect() { + const room = AdminChatRoom.findOne(this.rid, { fields: { t: 1 } }); + return room && room.t !== 'd'; + }, + roomType() { + const room = AdminChatRoom.findOne(this.rid, { fields: { t: 1 } }); + return room && room.t; + }, + channelSettings() { + return RocketChat.ChannelSettings.getOptions(null, 'admin-room'); + }, + roomTypeDescription() { + const room = AdminChatRoom.findOne(this.rid, { fields: { t: 1 } }); + const roomType = room && room.t; + if (roomType === 'c') { + return t('Channel'); + } else if (roomType === 'p') { + return t('Private_Group'); + } + }, + roomName() { + const room = AdminChatRoom.findOne(this.rid, { fields: { name: 1 } }); + return room && room.name; + }, + roomTopic() { + const room = AdminChatRoom.findOne(this.rid, { fields: { topic: 1 } }); + return room && room.topic; + }, + archivationState() { + const room = AdminChatRoom.findOne(this.rid, { fields: { archived: 1 } }); + return room && room.archived; + }, + archivationStateDescription() { + const room = AdminChatRoom.findOne(this.rid, { fields: { archived: 1 } }); + const archivationState = room && room.archived; + if (archivationState === true) { + return t('Room_archivation_state_true'); + } else { + return t('Room_archivation_state_false'); + } + }, + canDeleteRoom() { + const room = AdminChatRoom.findOne(this.rid, { fields: { t: 1 } }); + const roomType = room && room.t; + return (roomType != null) && RocketChat.authz.hasAtLeastOnePermission(`delete-${ roomType }`); + }, + readOnly() { + const room = AdminChatRoom.findOne(this.rid, { fields: { ro: 1 } }); + return room && room.ro; + }, + readOnlyDescription() { + const room = AdminChatRoom.findOne(this.rid, { fields: { ro: 1 } }); + const readOnly = room && room.ro; + + if (readOnly === true) { + return t('True'); + } else { + return t('False'); + } + } +}); + +Template.adminRoomInfo.events({ + 'click .delete'() { + swal({ + title: t('Are_you_sure'), + text: t('Delete_Room_Warning'), + type: 'warning', + showCancelButton: true, + confirmButtonColor: '#DD6B55', + confirmButtonText: t('Yes_delete_it'), + cancelButtonText: t('Cancel'), + closeOnConfirm: false, + html: false + }, () => { + swal.disableButtons(); + Meteor.call('eraseRoom', this.rid, function(error) { + if (error) { + handleError(error); + swal.enableButtons(); + } else { + swal({ + title: t('Deleted'), + text: t('Room_has_been_deleted'), + type: 'success', + timer: 2000, + showConfirmButton: false + }); + } + }); + }); + }, + 'keydown input[type=text]'(e, t) { + if (e.keyCode === 13) { + e.preventDefault(); + t.saveSetting(this.rid); + } + }, + 'click [data-edit]'(e, t) { + e.preventDefault(); + t.editing.set($(e.currentTarget).data('edit')); + return setTimeout((function() { + t.$('input.editing').focus().select(); + }), 100); + }, + 'click .cancel'(e, t) { + e.preventDefault(); + t.editing.set(); + }, + 'click .save'(e, t) { + e.preventDefault(); + t.saveSetting(this.rid); + } +}); + +Template.adminRoomInfo.onCreated(function() { + this.editing = new ReactiveVar; + this.validateRoomType = () => { + const type = this.$('input[name=roomType]:checked').val(); + if (type !== 'c' && type !== 'p') { + toastr.error(t('error-invalid-room-type', { type })); + } + return true; + }; + this.validateRoomName = (rid) => { + const room = AdminChatRoom.findOne(rid); + let nameValidation; + if (!RocketChat.authz.hasAllPermission('edit-room', rid) || (room.t !== 'c' && room.t !== 'p')) { + toastr.error(t('error-not-allowed')); + return false; + } + name = $('input[name=roomName]').val(); + try { + nameValidation = new RegExp(`^${ RocketChat.settings.get('UTF8_Names_Validation') }$`); + } catch (_error) { + nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); + } + if (!nameValidation.test(name)) { + toastr.error(t('error-invalid-room-name', { + room_name: name + })); + return false; + } + return true; + }; + this.validateRoomTopic = () => { + return true; + }; + this.saveSetting = (rid) => { + switch (this.editing.get()) { + case 'roomName': + if (this.validateRoomName(rid)) { + RocketChat.callbacks.run('roomNameChanged', AdminChatRoom.findOne(rid)); + Meteor.call('saveRoomSettings', rid, 'roomName', this.$('input[name=roomName]').val(), function(err) { + if (err) { + return handleError(err); + } + toastr.success(TAPi18n.__('Room_name_changed_successfully')); + }); + } + break; + case 'roomTopic': + if (this.validateRoomTopic(rid)) { + Meteor.call('saveRoomSettings', rid, 'roomTopic', this.$('input[name=roomTopic]').val(), function(err) { + if (err) { + return handleError(err); + } + toastr.success(TAPi18n.__('Room_topic_changed_successfully')); + RocketChat.callbacks.run('roomTopicChanged', AdminChatRoom.findOne(rid)); + }); + } + break; + case 'roomAnnouncement': + if (this.validateRoomTopic(rid)) { + Meteor.call('saveRoomSettings', rid, 'roomAnnouncement', this.$('input[name=roomAnnouncement]').val(), function(err) { + if (err) { + return handleError(err); + } + toastr.success(TAPi18n.__('Room_announcement_changed_successfully')); + RocketChat.callbacks.run('roomAnnouncementChanged', AdminChatRoom.findOne(rid)); + }); + } + break; + case 'roomType': + const val = this.$('input[name=roomType]:checked').val(); + if (this.validateRoomType(rid)) { + RocketChat.callbacks.run('roomTypeChanged', AdminChatRoom.findOne(rid)); + const saveRoomSettings = function() { + Meteor.call('saveRoomSettings', rid, 'roomType', val, function(err) { + if (err) { + return handleError(err); + } else { + toastr.success(TAPi18n.__('Room_type_changed_successfully')); + } + }); + }; + if (!AdminChatRoom.findOne(rid, { fields: { 'default': 1 }})['default']) { + return saveRoomSettings(); + } + swal({ + title: t('Room_default_change_to_private_will_be_default_no_more'), + type: 'warning', + showCancelButton: true, + confirmButtonColor: '#DD6B55', + confirmButtonText: t('Yes'), + cancelButtonText: t('Cancel'), + closeOnConfirm: true, + html: false + }, function(confirmed) { + return !confirmed || saveRoomSettings(); + }); + } + break; + case 'archivationState': + const room = AdminChatRoom.findOne(rid); + if (this.$('input[name=archivationState]:checked').val() === 'true') { + if (room && room.archived !== true) { + Meteor.call('archiveRoom', rid, function(err) { + if (err) { + return handleError(err); + } + toastr.success(TAPi18n.__('Room_archived')); + RocketChat.callbacks.run('archiveRoom', AdminChatRoom.findOne(rid)); + }); + } + } else if ((room && room.archived) === true) { + Meteor.call('unarchiveRoom', rid, function(err) { + if (err) { + return handleError(err); + } + toastr.success(TAPi18n.__('Room_unarchived')); + RocketChat.callbacks.run('unarchiveRoom', AdminChatRoom.findOne(rid)); + }); + } + break; + case 'readOnly': + Meteor.call('saveRoomSettings', rid, 'readOnly', this.$('input[name=readOnly]:checked').val() === 'true', function(err) { + if (err) { + return handleError(err); + } + toastr.success(TAPi18n.__('Read_only_changed_successfully')); + }); + } + this.editing.set(); + }; +}); diff --git a/packages/rocketchat-ui-admin/client/rooms/adminRooms.coffee b/packages/rocketchat-ui-admin/client/rooms/adminRooms.coffee deleted file mode 100644 index 1be4c06b44e52255cd4c879bb584526588da2b2f..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-admin/client/rooms/adminRooms.coffee +++ /dev/null @@ -1,126 +0,0 @@ -@AdminChatRoom = new Mongo.Collection('rocketchat_room') - -Template.adminRooms.helpers - isReady: -> - return Template.instance().ready?.get() - rooms: -> - return Template.instance().rooms() - isLoading: -> - return 'btn-loading' unless Template.instance().ready?.get() - hasMore: -> - return Template.instance().limit?.get() is Template.instance().rooms?().count() - roomCount: -> - return Template.instance().rooms?().count() - name: -> - if @t is 'c' or @t is 'p' - return @name - else if @t is 'd' - return @usernames.join ' x ' - type: -> - if @t is 'c' - return TAPi18n.__ 'Channel' - else if @t is 'd' - return TAPi18n.__ 'Direct Message' - if @t is 'p' - return TAPi18n.__ 'Private Group' - default: -> - if this.default - return t('True') - else - return t('False') - flexData: -> - return { - tabBar: Template.instance().tabBar - } - -Template.adminRooms.onCreated -> - instance = @ - @limit = new ReactiveVar 50 - @filter = new ReactiveVar '' - @types = new ReactiveVar [] - @ready = new ReactiveVar true - - @tabBar = new RocketChatTabBar(); - @tabBar.showGroup(FlowRouter.current().route.name); - - RocketChat.TabBar.addButton({ - groups: ['admin-rooms'], - id: 'admin-room', - i18nTitle: 'Room_Info', - icon: 'icon-info-circled', - template: 'adminRoomInfo', - order: 1 - }); - - RocketChat.ChannelSettings.addOption - group: ['admin-room'] - id: 'make-default' - template: 'channelSettingsDefault' - data: -> - return Session.get('adminRoomsSelected') - validation: -> - return RocketChat.authz.hasAllPermission('view-room-administration') - - @autorun -> - filter = instance.filter.get() - types = instance.types.get() - - if types.length is 0 - types = ['c', 'd', 'p'] - - limit = instance.limit.get() - subscription = instance.subscribe 'adminRooms', filter, types, limit - instance.ready.set subscription.ready() - - @rooms = -> - filter = _.trim instance.filter?.get() - types = instance.types?.get() - - unless _.isArray types - types = [] - - query = {} - - filter = _.trim filter - if filter - filterReg = new RegExp s.escapeRegExp(filter), "i" - query = { $or: [ { name: filterReg }, { t: 'd', usernames: filterReg } ] } - - if types.length - query['t'] = { $in: types } - - return AdminChatRoom.find(query, { limit: instance.limit?.get(), sort: { default: -1, name: 1 } }) - - @getSearchTypes = -> - return _.map $('[name=room-type]:checked'), (input) -> return $(input).val() - -Template.adminRooms.onRendered -> - Tracker.afterFlush -> - SideNav.setFlex "adminFlex" - SideNav.openFlex() - -Template.adminRooms.events - 'keydown #rooms-filter': (e) -> - if e.which is 13 - e.stopPropagation() - e.preventDefault() - - 'keyup #rooms-filter': (e, t) -> - e.stopPropagation() - e.preventDefault() - t.filter.set e.currentTarget.value - - 'click .room-info': (e, instance) -> - e.preventDefault() - - Session.set('adminRoomsSelected', { rid: @_id }); - - instance.tabBar.open('admin-room') - - 'click .load-more': (e, t) -> - e.preventDefault() - e.stopPropagation() - t.limit.set t.limit.get() + 50 - - 'change [name=room-type]': (e, t) -> - t.types.set t.getSearchTypes() diff --git a/packages/rocketchat-ui-admin/client/rooms/adminRooms.js b/packages/rocketchat-ui-admin/client/rooms/adminRooms.js new file mode 100644 index 0000000000000000000000000000000000000000..878eb4d69c390c29090e936512e00428549f4c0a --- /dev/null +++ b/packages/rocketchat-ui-admin/client/rooms/adminRooms.js @@ -0,0 +1,159 @@ +/*globals RocketChatTabBar, AdminChatRoom */ + +this.AdminChatRoom = new Mongo.Collection('rocketchat_room'); + +Template.adminRooms.helpers({ + isReady() { + const instance = Template.instance(); + return instance.ready && instance.ready.get(); + }, + rooms() { + return Template.instance().rooms(); + }, + isLoading() { + const instance = Template.instance(); + if (!(instance.ready && instance.ready.get())) { + return 'btn-loading'; + } + }, + hasMore() { + const instance = Template.instance(); + if (instance.limit && instance.limit.get() && instance.rooms() && instance.rooms().count()) { + return instance.limit.get() === instance.rooms().count(); + } + }, + roomCount() { + const rooms = Template.instance().rooms(); + return rooms && rooms.count(); + }, + name() { + if (this.t === 'c' || this.t === 'p') { + return this.name; + } else if (this.t === 'd') { + return this.usernames.join(' x '); + } + }, + type() { + if (this.t === 'c') { + return TAPi18n.__('Channel'); + } else if (this.t === 'd') { + return TAPi18n.__('Direct Message'); + } + if (this.t === 'p') { + return TAPi18n.__('Private Group'); + } + }, + 'default'() { + if (this['default']) { + return t('True'); + } else { + return t('False'); + } + }, + flexData() { + return { + tabBar: Template.instance().tabBar + }; + } +}); + +Template.adminRooms.onCreated(function() { + const instance = this; + this.limit = new ReactiveVar(50); + this.filter = new ReactiveVar(''); + this.types = new ReactiveVar([]); + this.ready = new ReactiveVar(true); + this.tabBar = new RocketChatTabBar(); + this.tabBar.showGroup(FlowRouter.current().route.name); + RocketChat.TabBar.addButton({ + groups: ['admin-rooms'], + id: 'admin-room', + i18nTitle: 'Room_Info', + icon: 'icon-info-circled', + template: 'adminRoomInfo', + order: 1 + }); + RocketChat.ChannelSettings.addOption({ + group: ['admin-room'], + id: 'make-default', + template: 'channelSettingsDefault', + data() { + return Session.get('adminRoomsSelected'); + }, + validation() { + return RocketChat.authz.hasAllPermission('view-room-administration'); + } + }); + this.autorun(function() { + const filter = instance.filter.get(); + let types = instance.types.get(); + if (types.length === 0) { + types = ['c', 'd', 'p']; + } + const limit = instance.limit.get(); + const subscription = instance.subscribe('adminRooms', filter, types, limit); + instance.ready.set(subscription.ready()); + }); + this.rooms = function() { + let filter; + if (instance.filter && instance.filter.get()) { + filter = _.trim(instance.filter.get()); + } + let types = instance.types && instance.types.get(); + if (!_.isArray(types)) { + types = []; + } + let query = {}; + filter = _.trim(filter); + if (filter) { + const filterReg = new RegExp(s.escapeRegExp(filter), 'i'); + query = { $or: [{ name: filterReg }, { t: 'd', usernames: filterReg } ]}; + } + if (types.length) { + query['t'] = { $in: types }; + } + const limit = instance.limit && instance.limit.get(); + return AdminChatRoom.find(query, { limit, sort: { 'default': -1, name: 1}}); + }; + this.getSearchTypes = function() { + return _.map($('[name=room-type]:checked'), function(input) { + return $(input).val(); + }); + }; +}); + +Template.adminRooms.onRendered(function() { + Tracker.afterFlush(function() { + SideNav.setFlex('adminFlex'); + SideNav.openFlex(); + }); +}); + +Template.adminRooms.events({ + 'keydown #rooms-filter'(e) { + if (e.which === 13) { + e.stopPropagation(); + e.preventDefault(); + } + }, + 'keyup #rooms-filter'(e, t) { + e.stopPropagation(); + e.preventDefault(); + t.filter.set(e.currentTarget.value); + }, + 'click .room-info'(e, instance) { + e.preventDefault(); + Session.set('adminRoomsSelected', { + rid: this._id + }); + instance.tabBar.open('admin-room'); + }, + 'click .load-more'(e, t) { + e.preventDefault(); + e.stopPropagation(); + t.limit.set(t.limit.get() + 50); + }, + 'change [name=room-type]'(e, t) { + t.types.set(t.getSearchTypes()); + } +}); diff --git a/packages/rocketchat-ui-admin/client/users/adminInviteUser.coffee b/packages/rocketchat-ui-admin/client/users/adminInviteUser.coffee deleted file mode 100644 index f58c99014966483a6ec264c02514b8b29705a887..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-admin/client/users/adminInviteUser.coffee +++ /dev/null @@ -1,31 +0,0 @@ -import toastr from 'toastr' -Template.adminInviteUser.helpers - isAdmin: -> - return RocketChat.authz.hasRole(Meteor.userId(), 'admin') - inviteEmails: -> - return Template.instance().inviteEmails.get() - -Template.adminInviteUser.events - 'click .send': (e, instance) -> - emails = $('#inviteEmails').val().split /[\s,;]/ - rfcMailPattern = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ - validEmails = _.compact _.map emails, (email) -> return email if rfcMailPattern.test email - if validEmails.length - Meteor.call 'sendInvitationEmail', validEmails, (error, result) -> - if result - instance.clearForm() - instance.inviteEmails.set validEmails - if error - handleError(error) - else - toastr.error t('Send_invitation_email_error') - - 'click .cancel': (e, instance) -> - instance.clearForm() - instance.inviteEmails.set [] - Template.currentData().tabBar.close() - -Template.adminInviteUser.onCreated -> - @inviteEmails = new ReactiveVar [] - @clearForm = -> - $('#inviteEmails').val('') diff --git a/packages/rocketchat-ui-admin/client/users/adminInviteUser.js b/packages/rocketchat-ui-admin/client/users/adminInviteUser.js new file mode 100644 index 0000000000000000000000000000000000000000..c39c766a0e4e1ddab183c232759c7784fd665073 --- /dev/null +++ b/packages/rocketchat-ui-admin/client/users/adminInviteUser.js @@ -0,0 +1,46 @@ +import toastr from 'toastr'; +Template.adminInviteUser.helpers({ + isAdmin() { + return RocketChat.authz.hasRole(Meteor.userId(), 'admin'); + }, + inviteEmails() { + return Template.instance().inviteEmails.get(); + } +}); + +Template.adminInviteUser.events({ + 'click .send'(e, instance) { + const emails = $('#inviteEmails').val().split(/[\s,;]/); + const rfcMailPattern = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + const validEmails = _.compact(_.map(emails, function(email) { + if (rfcMailPattern.test(email)) { + return email; + } + })); + if (validEmails.length) { + Meteor.call('sendInvitationEmail', validEmails, function(error, result) { + if (result) { + instance.clearForm(); + instance.inviteEmails.set(validEmails); + } + if (error) { + handleError(error); + } + }); + } else { + toastr.error(t('Send_invitation_email_error')); + } + }, + 'click .cancel'(e, instance) { + instance.clearForm(); + instance.inviteEmails.set([]); + Template.currentData().tabBar.close(); + } +}); + +Template.adminInviteUser.onCreated(function() { + this.inviteEmails = new ReactiveVar([]); + this.clearForm = function() { + $('#inviteEmails').val(''); + }; +}); diff --git a/packages/rocketchat-ui-admin/client/users/adminUserChannels.coffee b/packages/rocketchat-ui-admin/client/users/adminUserChannels.coffee deleted file mode 100644 index f7f5520e92150d830f65093256af8a038760774b..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-admin/client/users/adminUserChannels.coffee +++ /dev/null @@ -1,11 +0,0 @@ -Template.adminUserChannels.helpers - type: -> - return if @t is 'd' then 'at' else if @t is 'p' then 'lock' else 'hash' - route: -> - return switch @t - when 'd' - FlowRouter.path('direct', {username: @name}) - when 'p' - FlowRouter.path('group', {name: @name}) - when 'c' - FlowRouter.path('channel', {name: @name}) diff --git a/packages/rocketchat-ui-admin/client/users/adminUserChannels.js b/packages/rocketchat-ui-admin/client/users/adminUserChannels.js new file mode 100644 index 0000000000000000000000000000000000000000..65e3022c0b8c7186fd33c82d1afb1291dfa5d321 --- /dev/null +++ b/packages/rocketchat-ui-admin/client/users/adminUserChannels.js @@ -0,0 +1,27 @@ +Template.adminUserChannels.helpers({ + type() { + if (this.t === 'd') { + return 'at'; + } else if (this.t === 'p') { + return 'lock'; + } else { + return 'hash'; + } + }, + route() { + switch (this.t) { + case 'd': + return FlowRouter.path('direct', { + username: this.name + }); + case 'p': + return FlowRouter.path('group', { + name: this.name + }); + case 'c': + return FlowRouter.path('channel', { + name: this.name + }); + } + } +}); diff --git a/packages/rocketchat-ui-admin/client/users/adminUsers.coffee b/packages/rocketchat-ui-admin/client/users/adminUsers.coffee deleted file mode 100644 index ad8e36d3203477cd5f99f36f431c0b02754a7d37..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-admin/client/users/adminUsers.coffee +++ /dev/null @@ -1,108 +0,0 @@ -Template.adminUsers.helpers - isReady: -> - return Template.instance().ready?.get() - users: -> - return Template.instance().users() - isLoading: -> - return 'btn-loading' unless Template.instance().ready?.get() - hasMore: -> - return Template.instance().limit?.get() is Template.instance().users?().length - emailAddress: -> - return _.map(@emails, (e) -> e.address).join(', ') - flexData: -> - return { - tabBar: Template.instance().tabBar - data: Template.instance().tabBarData.get() - } - -Template.adminUsers.onCreated -> - instance = @ - @limit = new ReactiveVar 50 - @filter = new ReactiveVar '' - @ready = new ReactiveVar true - - @tabBar = new RocketChatTabBar(); - @tabBar.showGroup(FlowRouter.current().route.name); - - @tabBarData = new ReactiveVar - - RocketChat.TabBar.addButton({ - groups: ['admin-users'], - id: 'invite-user', - i18nTitle: 'Invite_Users', - icon: 'icon-paper-plane', - template: 'adminInviteUser', - order: 1 - }) - - RocketChat.TabBar.addButton({ - groups: ['admin-users'], - id: 'add-user', - i18nTitle: 'Add_User', - icon: 'icon-plus', - template: 'adminUserEdit', - order: 2 - }) - - RocketChat.TabBar.addButton({ - groups: ['admin-users'] - id: 'admin-user-info', - i18nTitle: 'User_Info', - icon: 'icon-user', - template: 'adminUserInfo', - order: 3 - }) - - @autorun -> - filter = instance.filter.get() - limit = instance.limit.get() - subscription = instance.subscribe 'fullUserData', filter, limit - instance.ready.set subscription.ready() - - @users = -> - filter = _.trim instance.filter?.get() - if filter - filterReg = new RegExp s.escapeRegExp(filter), "i" - query = { $or: [ { username: filterReg }, { name: filterReg }, { "emails.address": filterReg } ] } - else - query = {} - - query.type = - $in: ['user', 'bot'] - - return Meteor.users.find(query, { limit: instance.limit?.get(), sort: { username: 1, name: 1 } }).fetch() - -Template.adminUsers.onRendered -> - Tracker.afterFlush -> - SideNav.setFlex "adminFlex" - SideNav.openFlex() - -Template.adminUsers.events - 'keydown #users-filter': (e) -> - if e.which is 13 - e.stopPropagation() - e.preventDefault() - - 'keyup #users-filter': (e, t) -> - e.stopPropagation() - e.preventDefault() - t.filter.set e.currentTarget.value - - 'click .user-info': (e, instance) -> - e.preventDefault() - - instance.tabBarData.set Meteor.users.findOne @_id - instance.tabBar.open('admin-user-info') - - 'click .info-tabs button': (e) -> - e.preventDefault() - $('.info-tabs button').removeClass 'active' - $(e.currentTarget).addClass 'active' - - $('.user-info-content').hide() - $($(e.currentTarget).attr('href')).show() - - 'click .load-more': (e, t) -> - e.preventDefault() - e.stopPropagation() - t.limit.set t.limit.get() + 50 diff --git a/packages/rocketchat-ui-admin/client/users/adminUsers.js b/packages/rocketchat-ui-admin/client/users/adminUsers.js new file mode 100644 index 0000000000000000000000000000000000000000..1562e6a095c8380a0bf7f5774f57c0c54804aa02 --- /dev/null +++ b/packages/rocketchat-ui-admin/client/users/adminUsers.js @@ -0,0 +1,131 @@ +/* globals RocketChatTabBar */ +Template.adminUsers.helpers({ + isReady() { + const instance = Template.instance(); + return instance.ready && instance.ready.get(); + }, + users() { + return Template.instance().users(); + }, + isLoading() { + const instance = Template.instance(); + if (!(instance.ready && instance.ready.get())) { + return 'btn-loading'; + } + }, + hasMore() { + const instance = Template.instance(); + const users = instance.users(); + if (instance.limit && instance.limit.get() && users && users.length) { + return instance.limit.get() === users.length; + } + }, + emailAddress() { + return _.map(this.emails, function(e) { e.address; }).join(', '); + }, + flexData() { + return { + tabBar: Template.instance().tabBar, + data: Template.instance().tabBarData.get() + }; + } +}); + +Template.adminUsers.onCreated(function() { + const instance = this; + this.limit = new ReactiveVar(50); + this.filter = new ReactiveVar(''); + this.ready = new ReactiveVar(true); + this.tabBar = new RocketChatTabBar(); + this.tabBar.showGroup(FlowRouter.current().route.name); + this.tabBarData = new ReactiveVar; + RocketChat.TabBar.addButton({ + groups: ['admin-users'], + id: 'invite-user', + i18nTitle: 'Invite_Users', + icon: 'icon-paper-plane', + template: 'adminInviteUser', + order: 1 + }); + RocketChat.TabBar.addButton({ + groups: ['admin-users'], + id: 'add-user', + i18nTitle: 'Add_User', + icon: 'icon-plus', + template: 'adminUserEdit', + order: 2 + }); + RocketChat.TabBar.addButton({ + groups: ['admin-users'], + id: 'admin-user-info', + i18nTitle: 'User_Info', + icon: 'icon-user', + template: 'adminUserInfo', + order: 3 + }); + this.autorun(function() { + const filter = instance.filter.get(); + const limit = instance.limit.get(); + const subscription = instance.subscribe('fullUserData', filter, limit); + instance.ready.set(subscription.ready()); + }); + this.users = function() { + let filter; + let query; + + if (instance.filter && instance.filter.get()) { + filter = _.trim(instance.filter.get()); + } + + if (filter) { + const filterReg = new RegExp(s.escapeRegExp(filter), 'i'); + query = {$or: [{ username: filterReg }, { name: filterReg}, { 'emails.address': filterReg }]}; + } else { + query = {}; + } + query.type = { + $in: ['user', 'bot'] + }; + + const limit = instance.limit && instance.limit.get(); + return Meteor.users.find(query, { limit, sort: { username: 1, name: 1 } }).fetch(); + }; +}); + +Template.adminUsers.onRendered(function() { + Tracker.afterFlush(function() { + SideNav.setFlex('adminFlex'); + SideNav.openFlex(); + }); +}); + +Template.adminUsers.events({ + 'keydown #users-filter'(e) { + if (e.which === 13) { + e.stopPropagation(); + e.preventDefault(); + } + }, + 'keyup #users-filter'(e, t) { + e.stopPropagation(); + e.preventDefault(); + t.filter.set(e.currentTarget.value); + }, + 'click .user-info'(e, instance) { + e.preventDefault(); + instance.tabBarData.set(Meteor.users.findOne(this._id)); + instance.tabBar.open('admin-user-info'); + }, + 'click .info-tabs button'(e) { + e.preventDefault(); + $('.info-tabs button').removeClass('active'); + $(e.currentTarget).addClass('active'); + $('.user-info-content').hide(); + $($(e.currentTarget).attr('href')).show(); + }, + 'click .load-more'(e, t) { + e.preventDefault(); + e.stopPropagation(); + t.limit.set(t.limit.get() + 50); + } +}); diff --git a/packages/rocketchat-ui-admin/package.js b/packages/rocketchat-ui-admin/package.js index a7c27911375bd1660990fb53de7e17cc08dbdb98..619e158cbdb1ce896a999e5fd0c505e67bd55e51 100644 --- a/packages/rocketchat-ui-admin/package.js +++ b/packages/rocketchat-ui-admin/package.js @@ -15,7 +15,6 @@ Package.onUse(function(api) { 'mongo', 'ecmascript', 'templating', - 'coffeescript', 'underscore', 'rocketchat:lib' ]); @@ -27,7 +26,7 @@ Package.onUse(function(api) { api.addFiles('client/rooms/adminRooms.html', 'client'); api.addFiles('client/rooms/adminRoomInfo.html', 'client'); - api.addFiles('client/rooms/adminRoomInfo.coffee', 'client'); + api.addFiles('client/rooms/adminRoomInfo.js', 'client'); api.addFiles('client/rooms/channelSettingsDefault.html', 'client'); api.addFiles('client/rooms/channelSettingsDefault.js', 'client'); @@ -37,16 +36,15 @@ Package.onUse(function(api) { api.addFiles('client/users/adminUserInfo.html', 'client'); api.addFiles('client/users/adminUsers.html', 'client'); - // coffee files - api.addFiles('client/admin.coffee', 'client'); - api.addFiles('client/adminFlex.coffee', 'client'); - api.addFiles('client/adminInfo.coffee', 'client'); + api.addFiles('client/admin.js', 'client'); + api.addFiles('client/adminFlex.js', 'client'); + api.addFiles('client/adminInfo.js', 'client'); - api.addFiles('client/rooms/adminRooms.coffee', 'client'); + api.addFiles('client/rooms/adminRooms.js', 'client'); - api.addFiles('client/users/adminInviteUser.coffee', 'client'); - api.addFiles('client/users/adminUserChannels.coffee', 'client'); - api.addFiles('client/users/adminUsers.coffee', 'client'); + api.addFiles('client/users/adminInviteUser.js', 'client'); + api.addFiles('client/users/adminUserChannels.js', 'client'); + api.addFiles('client/users/adminUsers.js', 'client'); api.addFiles('publications/adminRooms.js', 'server'); diff --git a/packages/rocketchat-ui-message/client/message.coffee b/packages/rocketchat-ui-message/client/message.coffee deleted file mode 100644 index 2684dfd9c0a3b4f60855e444d31b5198775f7672..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-message/client/message.coffee +++ /dev/null @@ -1,254 +0,0 @@ -import moment from 'moment' - -Template.message.helpers - encodeURI: (text) -> - return encodeURI(text) - isBot: -> - return 'bot' if this.bot? - roleTags: -> - if not RocketChat.settings.get('UI_DisplayRoles') or Meteor.user()?.settings?.preferences?.hideRoles - return [] - roles = _.union(UserRoles.findOne(this.u?._id)?.roles, RoomRoles.findOne({'u._id': this.u?._id, rid: this.rid })?.roles) - return RocketChat.models.Roles.find({ _id: { $in: roles }, description: { $exists: 1, $ne: '' } }, { fields: { description: 1 } }) - isGroupable: -> - return 'false' if this.groupable is false - isSequential: -> - return 'sequential' if this.groupable isnt false - avatarFromUsername: -> - if this.avatar? and this.avatar[0] is '@' - return this.avatar.replace(/^@/, '') - getEmoji: (emoji) -> - return renderEmoji emoji - getName: -> - if this.alias - return this.alias - if RocketChat.settings.get('UI_Use_Real_Name') and this.u?.name - return this.u.name - return this.u?.username - showUsername: -> - return this.alias or (RocketChat.settings.get('UI_Use_Real_Name') and this.u?.name) - own: -> - return 'own' if this.u?._id is Meteor.userId() - timestamp: -> - return +this.ts - chatops: -> - return 'chatops-message' if this.u?.username is RocketChat.settings.get('Chatops_Username') - time: -> - return moment(this.ts).format(RocketChat.settings.get('Message_TimeFormat')) - date: -> - return moment(this.ts).format(RocketChat.settings.get('Message_DateFormat')) - isTemp: -> - if @temp is true - return 'temp' - body: -> - return Template.instance().body - system: (returnClass) -> - if RocketChat.MessageTypes.isSystemMessage(this) - if returnClass - return 'color-info-font-color' - - return 'system' - - showTranslated: -> - if RocketChat.settings.get('AutoTranslate_Enabled') and this.u?._id isnt Meteor.userId() and !RocketChat.MessageTypes.isSystemMessage(this) - subscription = RocketChat.models.Subscriptions.findOne({ rid: this.rid, 'u._id': Meteor.userId() }, { fields: { autoTranslate: 1, autoTranslateLanguage: 1 } }); - language = RocketChat.AutoTranslate.getLanguage(this.rid); - return this.autoTranslateFetching || (subscription?.autoTranslate isnt this.autoTranslateShowInverse && this.translations && this.translations[language]) # || _.find(this.attachments, (attachment) -> attachment.translations && attachment.translations[language] && attachment.author_name isnt Meteor.user().username ) - - edited: -> - return Template.instance().wasEdited - - editTime: -> - if Template.instance().wasEdited - return moment(@editedAt).format(RocketChat.settings.get('Message_DateFormat') + ' ' + RocketChat.settings.get('Message_TimeFormat')) - editedBy: -> - return "" unless Template.instance().wasEdited - # try to return the username of the editor, - # otherwise a special "?" character that will be - # rendered as a special avatar - return @editedBy?.username or "?" - canEdit: -> - hasPermission = RocketChat.authz.hasAtLeastOnePermission('edit-message', this.rid) - isEditAllowed = RocketChat.settings.get 'Message_AllowEditing' - editOwn = this.u?._id is Meteor.userId() - - return unless hasPermission or (isEditAllowed and editOwn) - - blockEditInMinutes = RocketChat.settings.get 'Message_AllowEditing_BlockEditInMinutes' - if blockEditInMinutes? and blockEditInMinutes isnt 0 - msgTs = moment(this.ts) if this.ts? - currentTsDiff = moment().diff(msgTs, 'minutes') if msgTs? - return currentTsDiff < blockEditInMinutes - else - return true - - canDelete: -> - hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', this.rid ) - isDeleteAllowed = RocketChat.settings.get('Message_AllowDeleting') - deleteOwn = this.u?._id is Meteor.userId() - - return unless hasPermission or (isDeleteAllowed and deleteOwn) - - blockDeleteInMinutes = RocketChat.settings.get 'Message_AllowDeleting_BlockDeleteInMinutes' - if blockDeleteInMinutes? and blockDeleteInMinutes isnt 0 - msgTs = moment(this.ts) if this.ts? - currentTsDiff = moment().diff(msgTs, 'minutes') if msgTs? - return currentTsDiff < blockDeleteInMinutes - else - return true - - showEditedStatus: -> - return RocketChat.settings.get 'Message_ShowEditedStatus' - label: -> - if @i18nLabel - return t(@i18nLabel) - else if @label - return @label - - hasOembed: -> - return false unless this.urls?.length > 0 and Template.oembedBaseWidget? and RocketChat.settings.get 'API_Embed' - - return false unless this.u?.username not in RocketChat.settings.get('API_EmbedDisabledFor')?.split(',').map (username) -> username.trim() - - return true - - reactions: -> - msgReactions = [] - userUsername = Meteor.user()?.username - - for emoji, reaction of @reactions - total = reaction.usernames.length - usernames = '@' + reaction.usernames.slice(0, 15).join(', @') - - usernames = usernames.replace('@'+userUsername, t('You').toLowerCase()) - - if total > 15 - usernames = usernames + ' ' + t('And_more', { length: total - 15 }).toLowerCase() - else - usernames = usernames.replace(/,([^,]+)$/, ' '+t('and')+'$1') - - if usernames[0] isnt '@' - usernames = usernames[0].toUpperCase() + usernames.substr(1) - - msgReactions.push - emoji: emoji - count: reaction.usernames.length - usernames: usernames - reaction: ' ' + t('Reacted_with').toLowerCase() + ' ' + emoji - userReacted: reaction.usernames.indexOf(userUsername) > -1 - - return msgReactions - - markUserReaction: (reaction) -> - if reaction.userReacted - return { - class: 'selected' - } - - hideReactions: -> - return 'hidden' if _.isEmpty(@reactions) - - actionLinks: -> - # remove 'method_id' and 'params' properties - return _.map(@actionLinks, (actionLink, key) -> _.extend({ id: key }, _.omit(actionLink, 'method_id', 'params'))) - - hideActionLinks: -> - return 'hidden' if _.isEmpty(@actionLinks) - - injectIndex: (data, index) -> - data.index = index - return - - hideCog: -> - subscription = RocketChat.models.Subscriptions.findOne({ rid: this.rid }); - return 'hidden' if not subscription? - - hideUsernames: -> - prefs = Meteor.user()?.settings?.preferences - return if prefs?.hideUsernames - -Template.message.onCreated -> - msg = Template.currentData() - - @wasEdited = msg.editedAt? and not RocketChat.MessageTypes.isSystemMessage(msg) - - @body = do -> - isSystemMessage = RocketChat.MessageTypes.isSystemMessage(msg) - messageType = RocketChat.MessageTypes.getType(msg) - if messageType?.render? - msg = messageType.render(msg) - else if messageType?.template? - # render template - else if messageType?.message? - if messageType.data?(msg)? - msg = TAPi18n.__(messageType.message, messageType.data(msg)) - else - msg = TAPi18n.__(messageType.message) - else - if msg.u?.username is RocketChat.settings.get('Chatops_Username') - msg.html = msg.msg - msg = RocketChat.callbacks.run 'renderMentions', msg - # console.log JSON.stringify message - msg = msg.html - else - msg = renderMessageBody msg - - if isSystemMessage - msg.html = RocketChat.Markdown.parse msg.html - - return msg - -Template.message.onViewRendered = (context) -> - view = this - this._domrange.onAttached (domRange) -> - currentNode = domRange.lastNode() - currentDataset = currentNode.dataset - previousNode = currentNode.previousElementSibling - nextNode = currentNode.nextElementSibling - $currentNode = $(currentNode) - $nextNode = $(nextNode) - - unless previousNode? - $currentNode.addClass('new-day').removeClass('sequential') - - else if previousNode?.dataset? - previousDataset = previousNode.dataset - previousMessageDate = new Date(parseInt(previousDataset.timestamp)) - currentMessageDate = new Date(parseInt(currentDataset.timestamp)) - - if previousMessageDate.toDateString() isnt currentMessageDate.toDateString() - $currentNode.addClass('new-day').removeClass('sequential') - else - $currentNode.removeClass('new-day') - - if previousDataset.groupable is 'false' or currentDataset.groupable is 'false' - $currentNode.removeClass('sequential') - else - if previousDataset.username isnt currentDataset.username or parseInt(currentDataset.timestamp) - parseInt(previousDataset.timestamp) > RocketChat.settings.get('Message_GroupingPeriod') * 1000 - $currentNode.removeClass('sequential') - else if not $currentNode.hasClass 'new-day' - $currentNode.addClass('sequential') - - if nextNode?.dataset? - nextDataset = nextNode.dataset - - if nextDataset.date isnt currentDataset.date - $nextNode.addClass('new-day').removeClass('sequential') - else - $nextNode.removeClass('new-day') - - if nextDataset.groupable isnt 'false' - if nextDataset.username isnt currentDataset.username or parseInt(nextDataset.timestamp) - parseInt(currentDataset.timestamp) > RocketChat.settings.get('Message_GroupingPeriod') * 1000 - $nextNode.removeClass('sequential') - else if not $nextNode.hasClass 'new-day' - $nextNode.addClass('sequential') - - if not nextNode? - templateInstance = if $('#chat-window-' + context.rid)[0] then Blaze.getView($('#chat-window-' + context.rid)[0])?.templateInstance() else null - - if currentNode.classList.contains('own') is true - templateInstance?.atBottom = true - else - if templateInstance?.firstNode && templateInstance?.atBottom is false - newMessage = templateInstance?.find(".new-message") - newMessage?.className = "new-message background-primary-action-color color-content-background-color " diff --git a/packages/rocketchat-ui-message/client/message.js b/packages/rocketchat-ui-message/client/message.js new file mode 100644 index 0000000000000000000000000000000000000000..92d1db91acec117e365867f207c781a3922aa210 --- /dev/null +++ b/packages/rocketchat-ui-message/client/message.js @@ -0,0 +1,358 @@ +/* globals renderEmoji renderMessageBody*/ +import moment from 'moment'; + +Template.message.helpers({ + encodeURI(text) { + return encodeURI(text); + }, + isBot() { + if (this.bot != null) { + return 'bot'; + } + }, + roleTags() { + const user = Meteor.user(); + // test user -> settings -> preferences -> hideRoles + if (!RocketChat.settings.get('UI_DisplayRoles') || ['settings', 'preferences', 'hideRoles'].reduce((obj, field) => typeof obj !== 'undefined' && obj[field], user)) { + return []; + } + + if (!this.u || !this.u._id) { + return []; + } + /* globals UserRoles RoomRoles */ + const userRoles = UserRoles.findOne(this.u._id); + const roomRoles = RoomRoles.findOne({ + 'u._id': this.u._id, + rid: this.rid + }); + const roles = [...(userRoles && userRoles.roles) || [], ...(roomRoles && roomRoles.roles) || []]; + return RocketChat.models.Roles.find({ + _id: { + $in: roles + }, + description: { + $exists: 1, + $ne: '' + } + }, { + fields: { + description: 1 + } + }); + }, + isGroupable() { + if (this.groupable === false) { + return 'false'; + } + }, + isSequential() { + if (this.groupable !== false) { + return 'sequential'; + } + }, + avatarFromUsername() { + if ((this.avatar != null) && this.avatar[0] === '@') { + return this.avatar.replace(/^@/, ''); + } + }, + getEmoji(emoji) { + return renderEmoji(emoji); + }, + getName() { + if (this.alias) { + return this.alias; + } + if (!this.u) { + return ''; + } + return (RocketChat.settings.get('UI_Use_Real_Name') && this.u.name) || this.u.username; + }, + showUsername() { + return this.alias || RocketChat.settings.get('UI_Use_Real_Name') && this.u && this.u.name; + }, + own() { + if (this.u && this.u._id === Meteor.userId()) { + return 'own'; + } + }, + timestamp() { + return +this.ts; + }, + chatops() { + if (this.u && this.u.username === RocketChat.settings.get('Chatops_Username')) { + return 'chatops-message'; + } + }, + time() { + return moment(this.ts).format(RocketChat.settings.get('Message_TimeFormat')); + }, + date() { + return moment(this.ts).format(RocketChat.settings.get('Message_DateFormat')); + }, + isTemp() { + if (this.temp === true) { + return 'temp'; + } + }, + body() { + return Template.instance().body; + }, + system(returnClass) { + if (RocketChat.MessageTypes.isSystemMessage(this)) { + if (returnClass) { + return 'color-info-font-color'; + } + return 'system'; + } + }, + showTranslated() { + if (RocketChat.settings.get('AutoTranslate_Enabled') && this.u && this.u._id !== Meteor.userId() && !RocketChat.MessageTypes.isSystemMessage(this)) { + const subscription = RocketChat.models.Subscriptions.findOne({ + rid: this.rid, + 'u._id': Meteor.userId() + }, { + fields: { + autoTranslate: 1, + autoTranslateLanguage: 1 + } + }); + const language = RocketChat.AutoTranslate.getLanguage(this.rid); + return this.autoTranslateFetching || subscription && subscription.autoTranslate !== this.autoTranslateShowInverse && this.translations && this.translations[language]; + } + }, + edited() { + return Template.instance().wasEdited; + }, + editTime() { + if (Template.instance().wasEdited) { + return moment(this.editedAt).format(`${ RocketChat.settings.get('Message_DateFormat') } ${ RocketChat.settings.get('Message_TimeFormat') }`); + } + }, + editedBy() { + if (!Template.instance().wasEdited) { + return ''; + } + // try to return the username of the editor, + // otherwise a special "?" character that will be + // rendered as a special avatar + return (this.editedBy && this.editedBy.username) || '?'; + }, + canEdit() { + const hasPermission = RocketChat.authz.hasAtLeastOnePermission('edit-message', this.rid); + const isEditAllowed = RocketChat.settings.get('Message_AllowEditing'); + const editOwn = this.u && this.u._id === Meteor.userId(); + if (!(hasPermission || (isEditAllowed && editOwn))) { + return; + } + const blockEditInMinutes = RocketChat.settings.get('Message_AllowEditing_BlockEditInMinutes'); + if (blockEditInMinutes) { + let msgTs; + if (this.ts != null) { + msgTs = moment(this.ts); + } + let currentTsDiff; + if (msgTs != null) { + currentTsDiff = moment().diff(msgTs, 'minutes'); + } + return currentTsDiff < blockEditInMinutes; + } else { + return true; + } + }, + canDelete() { + const hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', this.rid); + const isDeleteAllowed = RocketChat.settings.get('Message_AllowDeleting'); + const deleteOwn = this.u && this.u._id === Meteor.userId(); + if (!(hasPermission || (isDeleteAllowed && deleteOwn))) { + return; + } + const blockDeleteInMinutes = RocketChat.settings.get('Message_AllowDeleting_BlockDeleteInMinutes'); + if (blockDeleteInMinutes) { + let msgTs; + if (this.ts != null) { + msgTs = moment(this.ts); + } + let currentTsDiff; + if (msgTs != null) { + currentTsDiff = moment().diff(msgTs, 'minutes'); + } + return currentTsDiff < blockDeleteInMinutes; + } else { + return true; + } + }, + showEditedStatus() { + return RocketChat.settings.get('Message_ShowEditedStatus'); + }, + label() { + if (this.i18nLabel) { + return t(this.i18nLabel); + } else if (this.label) { + return this.label; + } + }, + hasOembed() { + if (!(this.urls && this.urls.length > 0 && Template.oembedBaseWidget != null && RocketChat.settings.get('API_Embed'))) { + return false; + } + if (!(RocketChat.settings.get('API_EmbedDisabledFor')||'').split(',').map(username => username.trim()).includes(this.u && this.u.username)) { + return false; + } + return true; + }, + reactions() { + const userUsername = Meteor.user().username; + return Object.keys(this.reactions||{}).map(emoji => { + const reaction = this.reactions[emoji]; + const total = reaction.usernames.length; + let usernames = reaction.usernames.slice(0, 15).map(username => username === userUsername ? t('You').toLowerCase() : `@${ userUsername }`).join(', '); + if (total > 15) { + usernames = `${ usernames } ${ t('And_more', { + length: total - 15 + }).toLowerCase() }`; + } else { + usernames = usernames.replace(/,([^,]+)$/, ` ${ t('and') }$1`); + } + if (usernames[0] !== '@') { + usernames = usernames[0].toUpperCase() + usernames.substr(1); + } + return { + emoji, + count: reaction.usernames.length, + usernames, + reaction: ` ${ t('Reacted_with').toLowerCase() } ${ emoji }`, + userReacted: reaction.usernames.indexOf(userUsername) > -1 + }; + }); + }, + markUserReaction(reaction) { + if (reaction.userReacted) { + return { + 'class': 'selected' + }; + } + }, + hideReactions() { + if (_.isEmpty(this.reactions)) { + return 'hidden'; + } + }, + actionLinks() { + // remove 'method_id' and 'params' properties + return _.map(this.actionLinks, function(actionLink, key) { + return _.extend({ + id: key + }, _.omit(actionLink, 'method_id', 'params')); + }); + }, + hideActionLinks() { + if (_.isEmpty(this.actionLinks)) { + return 'hidden'; + } + }, + injectIndex(data, index) { + data.index = index; + }, + hideCog() { + const subscription = RocketChat.models.Subscriptions.findOne({ + rid: this.rid + }); + if (subscription == null) { + return 'hidden'; + } + } +}); + +Template.message.onCreated(function() { + let msg = Template.currentData(); + + this.wasEdited = (msg.editedAt != null) && !RocketChat.MessageTypes.isSystemMessage(msg); + + return this.body = (() => { + const isSystemMessage = RocketChat.MessageTypes.isSystemMessage(msg); + const messageType = RocketChat.MessageTypes.getType(msg)||{}; + if (messageType.render) { + msg = messageType.render(msg); + } else if (messageType.template) { + // render template + } else if (messageType.message) { + if (typeof messageType.data === 'function' && messageType.data(msg)) { + msg = TAPi18n.__(messageType.message, messageType.data(msg)); + } else { + msg = TAPi18n.__(messageType.message); + } + } else if (msg.u && msg.u.username === RocketChat.settings.get('Chatops_Username')) { + msg.html = msg.msg; + msg = RocketChat.callbacks.run('renderMentions', msg); + // console.log JSON.stringify message + msg = msg.html; + } else { + msg = renderMessageBody(msg); + } + + if (isSystemMessage) { + msg.html = RocketChat.Markdown.parse(msg.html); + } + return msg; + })(); +}); + +Template.message.onViewRendered = function(context) { + return this._domrange.onAttached(function(domRange) { + const currentNode = domRange.lastNode(); + const currentDataset = currentNode.dataset; + const previousNode = currentNode.previousElementSibling; + const nextNode = currentNode.nextElementSibling; + const $currentNode = $(currentNode); + const $nextNode = $(nextNode); + if (previousNode == null) { + $currentNode.addClass('new-day').removeClass('sequential'); + } else if (previousNode.dataset) { + const previousDataset = previousNode.dataset; + const previousMessageDate = new Date(parseInt(previousDataset.timestamp)); + const currentMessageDate = new Date(parseInt(currentDataset.timestamp)); + if (previousMessageDate.toDateString() !== currentMessageDate.toDateString()) { + $currentNode.addClass('new-day').removeClass('sequential'); + } else { + $currentNode.removeClass('new-day'); + } + if (previousDataset.groupable === 'false' || currentDataset.groupable === 'false') { + $currentNode.removeClass('sequential'); + } else if (previousDataset.username !== currentDataset.username || parseInt(currentDataset.timestamp) - parseInt(previousDataset.timestamp) > RocketChat.settings.get('Message_GroupingPeriod') * 1000) { + $currentNode.removeClass('sequential'); + } else if (!$currentNode.hasClass('new-day')) { + $currentNode.addClass('sequential'); + } + } + if (nextNode && nextNode.dataset) { + const nextDataset = nextNode.dataset; + if (nextDataset.date !== currentDataset.date) { + $nextNode.addClass('new-day').removeClass('sequential'); + } else { + $nextNode.removeClass('new-day'); + } + if (nextDataset.groupable !== 'false') { + if (nextDataset.username !== currentDataset.username || parseInt(nextDataset.timestamp) - parseInt(currentDataset.timestamp) > RocketChat.settings.get('Message_GroupingPeriod') * 1000) { + $nextNode.removeClass('sequential'); + } else if (!$nextNode.hasClass('new-day')) { + $nextNode.addClass('sequential'); + } + } + } + if (nextNode == null) { + const [el] = $(`#chat-window-${ context.rid }`); + const view = el && Blaze.getView(el); + const templateInstance = view && view.templateInstance(); + if (!templateInstance) { + return; + } + if (currentNode.classList.contains('own') === true) { + return (templateInstance.atBottom = true); + } else if (templateInstance.firstNode && templateInstance.atBottom === false) { + const newMessage = templateInstance.find('.new-message'); + return newMessage && (newMessage.className = 'new-message background-primary-action-color color-content-background-color '); + } + } + }); +}; diff --git a/packages/rocketchat-ui-message/client/messageBox.coffee b/packages/rocketchat-ui-message/client/messageBox.coffee deleted file mode 100644 index 2587fda0c301ecbd128e41861c4d0fd4e683bed1..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-message/client/messageBox.coffee +++ /dev/null @@ -1,391 +0,0 @@ -import toastr from 'toastr' -import mime from 'mime-type/with-db' -import moment from 'moment' -import {VRecDialog} from 'meteor/rocketchat:ui-vrecord' - -katexSyntax = -> - if RocketChat.katex.katex_enabled() - return "$$KaTeX$$" if RocketChat.katex.dollar_syntax_enabled() - return "\\[KaTeX\\]" if RocketChat.katex.parenthesis_syntax_enabled() - - return false - -Template.messageBox.helpers - roomName: -> - roomData = Session.get('roomData' + this._id) - return '' unless roomData - - if roomData.t is 'd' - return ChatSubscription.findOne({ rid: this._id }, { fields: { name: 1 } })?.name - else - return roomData.name - showMarkdown: -> - return RocketChat.Markdown - showMarkdownCode: -> - return RocketChat.MarkdownCode - showKatex: -> - return RocketChat.katex - katexSyntax: -> - return katexSyntax() - showFormattingTips: -> - return RocketChat.settings.get('Message_ShowFormattingTips') and (RocketChat.Markdown or RocketChat.MarkdownCode or katexSyntax()) - canJoin: -> - return Meteor.userId()? and RocketChat.roomTypes.verifyShowJoinLink @_id - joinCodeRequired: -> - return Session.get('roomData' + this._id)?.joinCodeRequired - subscribed: -> - return RocketChat.roomTypes.verifyCanSendMessage @_id - allowedToSend: -> - if RocketChat.roomTypes.readOnly @_id, Meteor.user() - return false - - if RocketChat.roomTypes.archived @_id - return false - - roomData = Session.get('roomData' + this._id) - if roomData?.t is 'd' - subscription = ChatSubscription.findOne({ rid: this._id }, { fields: { archived: 1, blocked: 1, blocker: 1 } }) - if subscription and (subscription.archived or subscription.blocked or subscription.blocker) - return false - - return true - isBlockedOrBlocker: -> - roomData = Session.get('roomData' + this._id) - if roomData?.t is 'd' - subscription = ChatSubscription.findOne({ rid: this._id }, { fields: { blocked: 1, blocker: 1 } }) - if subscription and (subscription.blocked or subscription.blocker) - return true - - getPopupConfig: -> - template = Template.instance() - return { - getInput: -> - return template.find('.input-message') - } - usersTyping: -> - users = MsgTyping.get @_id - if users.length is 0 - return - if users.length is 1 - return { - multi: false - selfTyping: MsgTyping.selfTyping.get() - users: users[0] - } - # usernames = _.map messages, (message) -> return message.u.username - last = users.pop() - if users.length > 4 - last = t('others') - # else - usernames = users.join(', ') - usernames = [usernames, last] - return { - multi: true - selfTyping: MsgTyping.selfTyping.get() - users: usernames.join " #{t 'and'} " - } - - groupAttachHidden: -> - return 'hidden' if RocketChat.settings.get('Message_Attachments_GroupAttach') - - fileUploadEnabled: -> - return RocketChat.settings.get('FileUpload_Enabled') - - fileUploadAllowedMediaTypes: -> - return RocketChat.settings.get('FileUpload_MediaTypeWhiteList') - - showFileUpload: -> - if (RocketChat.settings.get('FileUpload_Enabled')) - roomData = Session.get('roomData' + this._id) - if roomData?.t is 'd' - return RocketChat.settings.get('FileUpload_Enabled_Direct') - else - return true - else - return RocketChat.settings.get('FileUpload_Enabled') - - - showMic: -> - return Template.instance().showMicButton.get() - - showVRec: -> - return Template.instance().showVideoRec.get() - - showSend: -> - if not Template.instance().isMessageFieldEmpty.get() - return 'show-send' - - showLocation: -> - return RocketChat.Geolocation.get() isnt false - - notSubscribedTpl: -> - return RocketChat.roomTypes.getNotSubscribedTpl @_id - - showSandstorm: -> - return Meteor.settings.public.sandstorm && !Meteor.isCordova - - anonymousRead: -> - return not Meteor.userId()? and RocketChat.settings.get('Accounts_AllowAnonymousRead') is true - - anonymousWrite: -> - return not Meteor.userId()? and RocketChat.settings.get('Accounts_AllowAnonymousRead') is true and RocketChat.settings.get('Accounts_AllowAnonymousWrite') is true - -firefoxPasteUpload = (fn) -> - user = navigator.userAgent.match(/Firefox\/(\d+)\.\d/) - if !user or user[1] > 49 - return fn - return (event, instance) -> - if (event.originalEvent.ctrlKey or event.originalEvent.metaKey) and (event.keyCode == 86) - textarea = instance.find("textarea") - selectionStart = textarea.selectionStart - selectionEnd = textarea.selectionEnd - contentEditableDiv = instance.find('#msg_contenteditable') - contentEditableDiv.focus() - Meteor.setTimeout -> - pastedImg = contentEditableDiv.querySelector 'img' - textareaContent = textarea.value - startContent = textareaContent.substring(0, selectionStart) - endContent = textareaContent.substring(selectionEnd) - restoreSelection = (pastedText) -> - textarea.value = startContent + pastedText + endContent - textarea.selectionStart = selectionStart + pastedText.length - textarea.selectionEnd = textarea.selectionStart - contentEditableDiv.innerHTML = '' if pastedImg - textarea.focus - return if (!pastedImg || contentEditableDiv.innerHTML.length > 0) - [].slice.call(contentEditableDiv.querySelectorAll("br")).forEach (el) -> - contentEditableDiv.replaceChild(new Text("\n") , el) - restoreSelection(contentEditableDiv.innerText) - imageSrc = pastedImg.getAttribute("src") - if imageSrc.match(/^data:image/) - fetch(imageSrc) - .then((img)-> - return img.blob()) - .then (blob)-> - fileUpload [{ - file: blob - name: 'Clipboard' - }] - , 150 - fn?.apply @, arguments - - -Template.messageBox.events - 'click .join': (event) -> - event.stopPropagation() - event.preventDefault() - Meteor.call 'joinRoom', @_id, Template.instance().$('[name=joinCode]').val(), (err) => - if err? - toastr.error t(err.reason) - - if RocketChat.authz.hasAllPermission('preview-c-room') is false and RoomHistoryManager.getRoom(@_id).loaded is 0 - RoomManager.getOpenedRoomByRid(@_id).streamActive = false - RoomManager.getOpenedRoomByRid(@_id).ready = false - RoomHistoryManager.getRoom(@_id).loaded = undefined - RoomManager.computation.invalidate() - - 'click .register': (event) -> - event.stopPropagation() - event.preventDefault() - Session.set('forceLogin', true) - - 'click .register-anonymous': (event) -> - event.stopPropagation() - event.preventDefault() - - Meteor.call 'registerUser', {}, (error, loginData) -> - if loginData && loginData.token - Meteor.loginWithToken loginData.token - - - 'focus .input-message': (event, instance) -> - KonchatNotification.removeRoomNotification @_id - chatMessages[@_id].input = instance.find('.input-message') - - 'click .send-button': (event, instance) -> - input = instance.find('.input-message') - chatMessages[@_id].send(@_id, input, => - # fixes https://github.com/RocketChat/Rocket.Chat/issues/3037 - # at this point, the input is cleared and ready for autogrow - input.updateAutogrow() - instance.isMessageFieldEmpty.set(chatMessages[@_id].isEmpty()) - ) - input.focus() - - 'keyup .input-message': (event, instance) -> - chatMessages[@_id].keyup(@_id, event, instance) - instance.isMessageFieldEmpty.set(chatMessages[@_id].isEmpty()) - - 'paste .input-message': (e, instance) -> - Meteor.setTimeout -> - input = instance.find('.input-message') - input.updateAutogrow?() - , 50 - - if not e.originalEvent.clipboardData? - return - items = e.originalEvent.clipboardData.items - files = [] - for item in items - if item.kind is 'file' and item.type.indexOf('image/') isnt -1 - e.preventDefault() - files.push - file: item.getAsFile() - name: 'Clipboard - ' + moment().format(RocketChat.settings.get('Message_TimeAndDateFormat')) - - if files.length - fileUpload files - else - instance.isMessageFieldEmpty.set(false) - - 'keydown .input-message': firefoxPasteUpload((event, instance) -> - chatMessages[@_id].keydown(@_id, event, Template.instance())) - - 'input .input-message': (event) -> - chatMessages[@_id].valueChanged(@_id, event, Template.instance()) - - 'propertychange .input-message': (event) -> - if event.originalEvent.propertyName is 'value' - chatMessages[@_id].valueChanged(@_id, event, Template.instance()) - - "click .editing-commands-cancel > button": (e) -> - chatMessages[@_id].clearEditing() - - "click .editing-commands-save > button": (e) -> - chatMessages[@_id].send(@_id, chatMessages[@_id].input) - - 'change .message-form input[type=file]': (event, template) -> - e = event.originalEvent or event - files = e.target.files - if not files or files.length is 0 - files = e.dataTransfer?.files or [] - - filesToUpload = [] - for file in files - # `file.type = mime.lookup(file.name)` does not work. - Object.defineProperty(file, 'type', { value: mime.lookup(file.name) }) - filesToUpload.push - file: file - name: file.name - - fileUpload filesToUpload - - "click .message-buttons.share": (e, t) -> - t.$('.share-items').toggleClass('hidden') - t.$('.message-buttons.share').toggleClass('active') - - 'click .message-form .message-buttons.location': (event, instance) -> - roomId = @_id - - position = RocketChat.Geolocation.get() - - latitude = position.coords.latitude - longitude = position.coords.longitude - - text = """ - <div class="location-preview"> - <img style="height: 250px; width: 250px;" src="https://maps.googleapis.com/maps/api/staticmap?zoom=14&size=250x250&markers=color:gray%7Clabel:%7C#{latitude},#{longitude}&key=#{RocketChat.settings.get('MapView_GMapsAPIKey')}" /> - </div> - """ - - swal - title: t('Share_Location_Title') - text: text - showCancelButton: true - closeOnConfirm: true - closeOnCancel: true - html: true - , (isConfirm) -> - if isConfirm isnt true - return - - Meteor.call "sendMessage", - _id: Random.id() - rid: roomId - msg: "" - location: - type: 'Point' - coordinates: [ longitude, latitude ] - - - 'click .message-form .mic': (e, t) -> - AudioRecorder.start -> - t.$('.stop-mic').removeClass('hidden') - t.$('.mic').addClass('hidden') - - 'click .message-form .video-button': (e, t) -> - if VRecDialog.opened - VRecDialog.close() - else - VRecDialog.open(e.currentTarget) - - 'click .message-form .stop-mic': (e, t) -> - AudioRecorder.stop (blob) -> - fileUpload [{ - file: blob - type: 'audio' - name: TAPi18n.__('Audio record') + '.wav' - }] - - t.$('.stop-mic').addClass('hidden') - t.$('.mic').removeClass('hidden') - - 'click .sandstorm-offer': (e, t) -> - roomId = @_id - RocketChat.Sandstorm.request "uiView", (err, data) => - if err or !data.token - console.error err - return - Meteor.call "sandstormClaimRequest", data.token, data.descriptor, (err, viewInfo) => - if err - console.error err - return - - Meteor.call "sendMessage", { - _id: Random.id() - rid: roomId - msg: "" - urls: [{ url: "grain://sandstorm", sandstormViewInfo: viewInfo }] - } - -Template.messageBox.onCreated -> - @isMessageFieldEmpty = new ReactiveVar true - @showMicButton = new ReactiveVar false - @showVideoRec = new ReactiveVar false - - @autorun => - videoRegex = /video\/webm|video\/\*/i - videoEnabled = !RocketChat.settings.get("FileUpload_MediaTypeWhiteList") || RocketChat.settings.get("FileUpload_MediaTypeWhiteList").match(videoRegex) - if RocketChat.settings.get('Message_VideoRecorderEnabled') and (navigator.getUserMedia? or navigator.webkitGetUserMedia?) and videoEnabled and RocketChat.settings.get('FileUpload_Enabled') - @showVideoRec.set true - else - @showVideoRec.set false - - wavRegex = /audio\/wav|audio\/\*/i - wavEnabled = !RocketChat.settings.get("FileUpload_MediaTypeWhiteList") || RocketChat.settings.get("FileUpload_MediaTypeWhiteList").match(wavRegex) - if RocketChat.settings.get('Message_AudioRecorderEnabled') and (navigator.getUserMedia? or navigator.webkitGetUserMedia?) and wavEnabled and RocketChat.settings.get('FileUpload_Enabled') - @showMicButton.set true - else - @showMicButton.set false - - -Meteor.startup -> - RocketChat.Geolocation = new ReactiveVar false - - Tracker.autorun -> - if RocketChat.settings.get('MapView_Enabled') is true and RocketChat.settings.get('MapView_GMapsAPIKey')?.length and navigator.geolocation?.getCurrentPosition? - success = (position) => - RocketChat.Geolocation.set position - - error = (error) => - console.log 'Error getting your geolocation', error - RocketChat.Geolocation.set false - - options = - enableHighAccuracy: true - maximumAge: 0 - timeout: 10000 - - navigator.geolocation.watchPosition success, error - else - RocketChat.Geolocation.set false diff --git a/packages/rocketchat-ui-message/client/messageBox.js b/packages/rocketchat-ui-message/client/messageBox.js new file mode 100644 index 0000000000000000000000000000000000000000..add30a122142c3afacbf892c932821e54e346d0a --- /dev/null +++ b/packages/rocketchat-ui-message/client/messageBox.js @@ -0,0 +1,454 @@ +/* globals fileUpload AudioRecorder KonchatNotification chatMessages */ +import toastr from 'toastr'; + +import mime from 'mime-type/with-db'; + +import moment from 'moment'; + +import {VRecDialog} from 'meteor/rocketchat:ui-vrecord'; + +function katexSyntax() { + if (RocketChat.katex.katex_enabled()) { + if (RocketChat.katex.dollar_syntax_enabled()) { + return '$$KaTeX$$'; + } + if (RocketChat.katex.parenthesis_syntax_enabled()) { + return '\\[KaTeX\\]'; + } + } + return false; +} + +Template.messageBox.helpers({ + roomName() { + const roomData = Session.get(`roomData${ this._id }`); + if (!roomData) { + return ''; + } + if (roomData.t === 'd') { + const chat = ChatSubscription.findOne({ + rid: this._id + }, { + fields: { + name: 1 + } + }); + return chat && chat.name; + } else { + return roomData.name; + } + }, + showMarkdown() { + return RocketChat.Markdown; + }, + showMarkdownCode() { + return RocketChat.MarkdownCode; + }, + showKatex() { + return RocketChat.katex; + }, + katexSyntax() { + return katexSyntax(); + }, + showFormattingTips() { + return RocketChat.settings.get('Message_ShowFormattingTips') && (RocketChat.Markdown || RocketChat.MarkdownCode || katexSyntax()); + }, + canJoin() { + return RocketChat.roomTypes.verifyShowJoinLink(this._id); + }, + joinCodeRequired() { + const code = Session.get(`roomData${ this._id }`); + return code && code.joinCodeRequired; + }, + subscribed() { + return RocketChat.roomTypes.verifyCanSendMessage(this._id); + }, + allowedToSend() { + if (RocketChat.roomTypes.readOnly(this._id, Meteor.user())) { + return false; + } + if (RocketChat.roomTypes.archived(this._id)) { + return false; + } + const roomData = Session.get(`roomData${ this._id }`); + if (roomData && roomData.t === 'd') { + const subscription = ChatSubscription.findOne({ + rid: this._id + }, { + fields: { + archived: 1, + blocked: 1, + blocker: 1 + } + }); + if (subscription && (subscription.archived || subscription.blocked || subscription.blocker)) { + return false; + } + } + return true; + }, + isBlockedOrBlocker() { + const roomData = Session.get(`roomData${ this._id }`); + if (roomData && roomData.t === 'd') { + const subscription = ChatSubscription.findOne({ + rid: this._id + }, { + fields: { + blocked: 1, + blocker: 1 + } + }); + if (subscription && (subscription.blocked || subscription.blocker)) { + return true; + } + } + }, + getPopupConfig() { + const template = Template.instance(); + return { + getInput() { + return template.find('.input-message'); + } + }; + }, + /* globals MsgTyping*/ + usersTyping() { + const users = MsgTyping.get(this._id); + if (users.length === 0) { + return; + } + if (users.length === 1) { + return { + multi: false, + selfTyping: MsgTyping.selfTyping.get(), + users: users[0] + }; + } + let last = users.pop(); + if (users.length > 4) { + last = t('others'); + } + let usernames = users.join(', '); + usernames = [usernames, last]; + return { + multi: true, + selfTyping: MsgTyping.selfTyping.get(), + users: usernames.join(` ${ t('and') } `) + }; + }, + groupAttachHidden() { + if (RocketChat.settings.get('Message_Attachments_GroupAttach')) { + return 'hidden'; + } + }, + fileUploadEnabled() { + return RocketChat.settings.get('FileUpload_Enabled'); + }, + fileUploadAllowedMediaTypes() { + return RocketChat.settings.get('FileUpload_MediaTypeWhiteList'); + }, + showFileUpload() { + let roomData; + if (RocketChat.settings.get('FileUpload_Enabled')) { + roomData = Session.get(`roomData${ this._id }`); + if (roomData && roomData.t === 'd') { + return RocketChat.settings.get('FileUpload_Enabled_Direct'); + } else { + return true; + } + } else { + return RocketChat.settings.get('FileUpload_Enabled'); + } + }, + showMic() { + return Template.instance().showMicButton.get(); + }, + showVRec() { + return Template.instance().showVideoRec.get(); + }, + showSend() { + if (!Template.instance().isMessageFieldEmpty.get()) { + return 'show-send'; + } + }, + showLocation() { + return RocketChat.Geolocation.get() !== false; + }, + notSubscribedTpl() { + return RocketChat.roomTypes.getNotSubscribedTpl(this._id); + }, + showSandstorm() { + return Meteor.settings['public'].sandstorm && !Meteor.isCordova; + } +}); + +function firefoxPasteUpload(fn) { + const user = navigator.userAgent.match(/Firefox\/(\d+)\.\d/); + if (!user || user[1] > 49) { + return fn; + } + return function(event, instance) { + if ((event.originalEvent.ctrlKey || event.originalEvent.metaKey) && (event.keyCode === 86)) { + const textarea = instance.find('textarea'); + const {selectionStart, selectionEnd} = textarea; + const contentEditableDiv = instance.find('#msg_contenteditable'); + contentEditableDiv.focus(); + Meteor.setTimeout(function() { + const pastedImg = contentEditableDiv.querySelector('img'); + const textareaContent = textarea.value; + const startContent = textareaContent.substring(0, selectionStart); + const endContent = textareaContent.substring(selectionEnd); + const restoreSelection = function(pastedText) { + textarea.value = startContent + pastedText + endContent; + textarea.selectionStart = selectionStart + pastedText.length; + return textarea.selectionEnd = textarea.selectionStart; + }; + if (pastedImg) { + contentEditableDiv.innerHTML = ''; + } + textarea.focus; + if (!pastedImg || contentEditableDiv.innerHTML.length > 0) { + return [].slice.call(contentEditableDiv.querySelectorAll('br')).forEach(function(el) { + contentEditableDiv.replaceChild(new Text('\n'), el); + return restoreSelection(contentEditableDiv.innerText); + }); + } + const imageSrc = pastedImg.getAttribute('src'); + if (imageSrc.match(/^data:image/)) { + return fetch(imageSrc).then(function(img) { + return img.blob(); + }).then(function(blob) { + return fileUpload([ + { + file: blob, + name: 'Clipboard' + } + ]); + }); + } + }, 150); + } + return fn && fn.apply(this, arguments); + }; +} + +Template.messageBox.events({ + 'click .join'(event) { + event.stopPropagation(); + event.preventDefault(); + Meteor.call('joinRoom', this._id, Template.instance().$('[name=joinCode]').val(), (err) => { + if (err != null) { + toastr.error(t(err.reason)); + } + if (RocketChat.authz.hasAllPermission('preview-c-room') === false && RoomHistoryManager.getRoom(this._id).loaded === 0) { + RoomManager.getOpenedRoomByRid(this._id).streamActive = false; + RoomManager.getOpenedRoomByRid(this._id).ready = false; + RoomHistoryManager.getRoom(this._id).loaded = null; + RoomManager.computation.invalidate(); + } + }); + }, + 'focus .input-message'(event, instance) { + KonchatNotification.removeRoomNotification(this._id); + chatMessages[this._id].input = instance.find('.input-message'); + }, + 'click .send-button'(event, instance) { + const input = instance.find('.input-message'); + chatMessages[this._id].send(this._id, input, () => { + // fixes https://github.com/RocketChat/Rocket.Chat/issues/3037 + // at this point, the input is cleared and ready for autogrow + input.updateAutogrow(); + return instance.isMessageFieldEmpty.set(chatMessages[this._id].isEmpty()); + }); + return input.focus(); + }, + 'keyup .input-message'(event, instance) { + chatMessages[this._id].keyup(this._id, event, instance); + return instance.isMessageFieldEmpty.set(chatMessages[this._id].isEmpty()); + }, + 'paste .input-message'(e, instance) { + Meteor.setTimeout(function() { + const input = instance.find('.input-message'); + return typeof input.updateAutogrow === 'function' && input.updateAutogrow(); + }, 50); + if (e.originalEvent.clipboardData == null) { + return; + } + const items = [...e.originalEvent.clipboardData.items]; + const files = items.map(item => { + if (item.kind === 'file' && item.type.indexOf('image/') !== -1) { + e.preventDefault(); + return { + file: item.getAsFile(), + name: `Clipboard - ${ moment().format(RocketChat.settings.get('Message_TimeAndDateFormat')) }` + }; + } + }).filter(e => e); + if (files.length) { + return fileUpload(files); + } else { + return instance.isMessageFieldEmpty.set(false); + } + }, + 'keydown .input-message': firefoxPasteUpload(function(event) { + return chatMessages[this._id].keydown(this._id, event, Template.instance()); + }), + 'input .input-message'(event) { + return chatMessages[this._id].valueChanged(this._id, event, Template.instance()); + }, + 'propertychange .input-message'(event) { + if (event.originalEvent.propertyName === 'value') { + return chatMessages[this._id].valueChanged(this._id, event, Template.instance()); + } + }, + 'click .editing-commands-cancel > button'() { + return chatMessages[this._id].clearEditing(); + }, + 'click .editing-commands-save > button'() { + return chatMessages[this._id].send(this._id, chatMessages[this._id].input); + }, + 'change .message-form input[type=file]'(event) { + const e = event.originalEvent || event; + let files = e.target.files; + if (!files || files.length === 0) { + files = (e.dataTransfer && e.dataTransfer.files) || []; + } + const filesToUpload = [...files].map(file => { + // `file.type = mime.lookup(file.name)` does not work. + Object.defineProperty(file, 'type', { + value: mime.lookup(file.name) + }); + return { + file, + name: file.name + }; + }); + return fileUpload(filesToUpload); + }, + 'click .message-buttons.share'(e, t) { + t.$('.share-items').toggleClass('hidden'); + return t.$('.message-buttons.share').toggleClass('active'); + }, + 'click .message-form .message-buttons.location'() { + const roomId = this._id; + const position = RocketChat.Geolocation.get(); + const latitude = position.coords.latitude; + const longitude = position.coords.longitude; + const text = `<div class="location-preview">\n <img style="height: 250px; width: 250px;" src="https://maps.googleapis.com/maps/api/staticmap?zoom=14&size=250x250&markers=color:gray%7Clabel:%7C${ latitude },${ longitude }&key=${ RocketChat.settings.get('MapView_GMapsAPIKey') }" />\n</div>`; + return swal({ + title: t('Share_Location_Title'), + text, + showCancelButton: true, + closeOnConfirm: true, + closeOnCancel: true, + html: true + }, function(isConfirm) { + if (isConfirm !== true) { + return; + } + return Meteor.call('sendMessage', { + _id: Random.id(), + rid: roomId, + msg: '', + location: { + type: 'Point', + coordinates: [longitude, latitude] + } + }); + }); + }, + 'click .message-form .mic'(e, t) { + return AudioRecorder.start(function() { + t.$('.stop-mic').removeClass('hidden'); + return t.$('.mic').addClass('hidden'); + }); + }, + 'click .message-form .video-button'(e) { + return VRecDialog.opened ? VRecDialog.close() : VRecDialog.open(e.currentTarget); + }, + 'click .message-form .stop-mic'(e, t) { + AudioRecorder.stop(function(blob) { + return fileUpload([ + { + file: blob, + type: 'audio', + name: `${ TAPi18n.__('Audio record') }.wav` + } + ]); + }); + t.$('.stop-mic').addClass('hidden'); + return t.$('.mic').removeClass('hidden'); + }, + 'click .sandstorm-offer'() { + const roomId = this._id; + return RocketChat.Sandstorm.request('uiView', (err, data) => { + if (err || !data.token) { + console.error(err); + return; + } + return Meteor.call('sandstormClaimRequest', data.token, data.descriptor, function(err, viewInfo) { + if (err) { + console.error(err); + return; + } + Meteor.call('sendMessage', { + _id: Random.id(), + rid: roomId, + msg: '', + urls: [ + { + url: 'grain://sandstorm', + sandstormViewInfo: viewInfo + } + ] + }); + }); + }); + } +}); + +Template.messageBox.onCreated(function() { + this.isMessageFieldEmpty = new ReactiveVar(true); + this.showMicButton = new ReactiveVar(false); + this.showVideoRec = new ReactiveVar(false); + return this.autorun(() => { + const videoRegex = /video\/webm|video\/\*/i; + const videoEnabled = !RocketChat.settings.get('FileUpload_MediaTypeWhiteList') || RocketChat.settings.get('FileUpload_MediaTypeWhiteList').match(videoRegex); + if (RocketChat.settings.get('Message_VideoRecorderEnabled') && ((navigator.getUserMedia != null) || (navigator.webkitGetUserMedia != null)) && videoEnabled && RocketChat.settings.get('FileUpload_Enabled')) { + this.showVideoRec.set(true); + } else { + this.showVideoRec.set(false); + } + const wavRegex = /audio\/wav|audio\/\*/i; + const wavEnabled = !RocketChat.settings.get('FileUpload_MediaTypeWhiteList') || RocketChat.settings.get('FileUpload_MediaTypeWhiteList').match(wavRegex); + if (RocketChat.settings.get('Message_AudioRecorderEnabled') && ((navigator.getUserMedia != null) || (navigator.webkitGetUserMedia != null)) && wavEnabled && RocketChat.settings.get('FileUpload_Enabled')) { + return this.showMicButton.set(true); + } else { + return this.showMicButton.set(false); + } + }); +}); + +Meteor.startup(function() { + RocketChat.Geolocation = new ReactiveVar(false); + return Tracker.autorun(function() { + const MapView_GMapsAPIKey = RocketChat.settings.get('MapView_GMapsAPIKey'); + if (RocketChat.settings.get('MapView_Enabled') === true && MapView_GMapsAPIKey && MapView_GMapsAPIKey.length && navigator.geolocation && navigator.geolocation.getCurrentPosition) { + const success = (position) => { + return RocketChat.Geolocation.set(position); + }; + const error = (error) => { + console.log('Error getting your geolocation', error); + return RocketChat.Geolocation.set(false); + }; + const options = { + enableHighAccuracy: true, + maximumAge: 0, + timeout: 10000 + }; + return navigator.geolocation.watchPosition(success, error, options); + } else { + return RocketChat.Geolocation.set(false); + } + }); +}); diff --git a/packages/rocketchat-ui-message/client/popup/messagePopup.coffee b/packages/rocketchat-ui-message/client/popup/messagePopup.coffee deleted file mode 100644 index c76fcf1b1711fe501d76781c1c41d858518bbecd..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-message/client/popup/messagePopup.coffee +++ /dev/null @@ -1,282 +0,0 @@ -# This is not supposed to be a complete list -# it is just to improve readability in this file -keys = { - TAB: 9 - ENTER: 13 - ESC: 27 - ARROW_LEFT: 37 - ARROW_UP: 38 - ARROW_RIGHT: 39 - ARROW_DOWN: 40 -} - -getCursorPosition = (input) -> - if not input? then return - if input.selectionStart? - return input.selectionStart - else if document.selection? - input.focus() - sel = document.selection.createRange() - selLen = document.selection.createRange().text.length - sel.moveStart('character', - input.value.length) - return sel.text.length - selLen - -setCursorPosition = (input, caretPos) -> - if not input? then return - if input.selectionStart? - input.focus() - return input.setSelectionRange(caretPos, caretPos) - else if document.selection? - range = input.createTextRange() - range.move('character', caretPos) - range.select() - -val = (v, d) -> - return if v? then v else d - -Template.messagePopup.onCreated -> - template = this - - template.textFilter = new ReactiveVar '' - - template.textFilterDelay = val(template.data.textFilterDelay, 0) - - template.open = val(template.data.open, new ReactiveVar(false)) - - template.hasData = new ReactiveVar false - - template.value = new ReactiveVar - - template.trigger = val(template.data.trigger, '') - - template.triggerAnywhere = val(template.data.triggerAnywhere, true) - - template.closeOnEsc = val(template.data.closeOnEsc, true) - - template.blurOnSelectItem = val(template.data.blurOnSelectItem, false) - - template.prefix = val(template.data.prefix, template.trigger) - - template.suffix = val(template.data.suffix, '') - - if template.triggerAnywhere is true - template.matchSelectorRegex = val(template.data.matchSelectorRegex, new RegExp "(?:^| )#{template.trigger}[^\\s]*$") - else - template.matchSelectorRegex = val(template.data.matchSelectorRegex, new RegExp "(?:^)#{template.trigger}[^\\s]*$") - - template.selectorRegex = val(template.data.selectorRegex, new RegExp "#{template.trigger}([^\\s]*)$") - - template.replaceRegex = val(template.data.replaceRegex, new RegExp "#{template.trigger}[^\\s]*$") - - template.getValue = val template.data.getValue, (_id) -> return _id - - template.up = => - current = template.find('.popup-item.selected') - previous = $(current).prev('.popup-item')[0] or template.find('.popup-item:last-child') - if previous? - current.className = current.className.replace /\sselected/, '' - previous.className += ' selected' - template.value.set previous.getAttribute('data-id') - - template.down = => - current = template.find('.popup-item.selected') - next = $(current).next('.popup-item')[0] or template.find('.popup-item') - if next?.classList.contains('popup-item') - current.className = current.className.replace /\sselected/, '' - next.className += ' selected' - template.value.set next.getAttribute('data-id') - - template.verifySelection = => - current = template.find('.popup-item.selected') - if not current? - first = template.find('.popup-item') - if first? - first.className += ' selected' - template.value.set first.getAttribute('data-id') - else - template.value.set undefined - - template.onInputKeydown = (event) => - if template.open.curValue isnt true or template.hasData.curValue isnt true - return - - if event.which in [keys.ENTER, keys.TAB] - if template.blurOnSelectItem is true - template.input.blur() - else - template.open.set false - - template.enterValue() - - if template.data.cleanOnEnter - template.input.value = '' - - event.preventDefault() - event.stopPropagation() - return - - if event.which is keys.ARROW_UP - template.up() - - event.preventDefault() - event.stopPropagation() - return - - if event.which is keys.ARROW_DOWN - template.down() - - event.preventDefault() - event.stopPropagation() - return - - template.setTextFilter = _.debounce (value) -> - template.textFilter.set(value) - , template.textFilterDelay - - template.onInputKeyup = (event) => - if template.closeOnEsc is true and template.open.curValue is true and event.which is keys.ESC - template.open.set false - event.preventDefault() - event.stopPropagation() - return - - value = template.input.value - value = value.substr 0, getCursorPosition(template.input) - - if template.matchSelectorRegex.test value - template.setTextFilter value.match(template.selectorRegex)[1] - template.open.set true - else - template.open.set false - - if template.open.curValue isnt true - return - - if event.which not in [keys.ARROW_UP, keys.ARROW_DOWN] - Meteor.defer => - template.verifySelection() - - template.onFocus = (event) => - template.clickingItem = false; - - if template.open.curValue is true - return - - value = template.input.value - value = value.substr 0, getCursorPosition(template.input) - - if template.matchSelectorRegex.test value - template.setTextFilter value.match(template.selectorRegex)[1] - template.open.set true - Meteor.defer => - template.verifySelection() - else - template.open.set false - - template.onBlur = (event) => - if template.open.curValue is false - return - - if template.clickingItem is true - return - - template.open.set false - - template.enterValue = -> - if not template.value.curValue? then return - - value = template.input.value - caret = getCursorPosition(template.input) - firstPartValue = value.substr 0, caret - lastPartValue = value.substr caret - getValue = this.getValue(template.value.curValue, template.data.collection, template.records.get(), firstPartValue) - - if not getValue - return - - firstPartValue = firstPartValue.replace(template.selectorRegex, template.prefix + getValue + template.suffix) - - template.input.value = firstPartValue + lastPartValue - - setCursorPosition template.input, firstPartValue.length - - template.records = new ReactiveVar [] - Tracker.autorun -> - if template.data.collection.findOne? - template.data.collection.find().count() - - filter = template.textFilter.get() - if filter? - filterCallback = (result) => - template.hasData.set result?.length > 0 - template.records.set result - - Meteor.defer => - template.verifySelection() - - result = template.data.getFilter(template.data.collection, filter, filterCallback) - if result? - filterCallback result - - -Template.messagePopup.onRendered -> - if this.data.getInput? - this.input = this.data.getInput?() - else if this.data.input - this.input = this.parentTemplate().find(this.data.input) - - if not this.input? - console.error 'Input not found for popup' - - $(this.input).on 'keyup', this.onInputKeyup.bind this - $(this.input).on 'keydown', this.onInputKeydown.bind this - $(this.input).on 'focus', this.onFocus.bind this - $(this.input).on 'blur', this.onBlur.bind this - - -Template.messagePopup.onDestroyed -> - $(this.input).off 'keyup', this.onInputKeyup - $(this.input).off 'keydown', this.onInputKeydown - $(this.input).off 'focus', this.onFocus - $(this.input).off 'blur', this.onBlur - - -Template.messagePopup.events - 'mouseenter .popup-item': (e) -> - if e.currentTarget.className.indexOf('selected') > -1 - return - - template = Template.instance() - - current = template.find('.popup-item.selected') - if current? - current.className = current.className.replace /\sselected/, '' - e.currentTarget.className += ' selected' - template.value.set this._id - - 'mousedown .popup-item, touchstart .popup-item': (e) -> - template = Template.instance() - template.clickingItem = true; - - 'mouseup .popup-item, touchend .popup-item': (e) -> - template = Template.instance() - - template.clickingItem = false; - - template.value.set this._id - - template.enterValue() - - template.open.set false - - toolbarSearch.clear(); - - -Template.messagePopup.helpers - isOpen: -> - Template.instance().open.get() and ((Template.instance().hasData.get() or Template.instance().data.emptyTemplate?) or not Template.instance().parentTemplate(1).subscriptionsReady()) - - data: -> - template = Template.instance() - - return template.records.get() diff --git a/packages/rocketchat-ui-message/client/popup/messagePopup.js b/packages/rocketchat-ui-message/client/popup/messagePopup.js new file mode 100644 index 0000000000000000000000000000000000000000..57a160bc13bb6e75e868ea6c4d4c20f2cf324732 --- /dev/null +++ b/packages/rocketchat-ui-message/client/popup/messagePopup.js @@ -0,0 +1,285 @@ +/* globals toolbarSearch */ +// This is not supposed to be a complete list +// it is just to improve readability in this file +const keys = { + TAB: 9, + ENTER: 13, + ESC: 27, + ARROW_LEFT: 37, + ARROW_UP: 38, + ARROW_RIGHT: 39, + ARROW_DOWN: 40 +}; + +function getCursorPosition(input) { + if (input == null) { + return; + } + if (input.selectionStart != null) { + return input.selectionStart; + } else if (document.selection != null) { + input.focus(); + const sel = document.selection.createRange(); + const selLen = document.selection.createRange().text.length; + sel.moveStart('character', -input.value.length); + return sel.text.length - selLen; + } +} + +function setCursorPosition(input, caretPos) { + if (input == null) { + return; + } + if (input.selectionStart != null) { + input.focus(); + return input.setSelectionRange(caretPos, caretPos); + } else if (document.selection != null) { + const range = input.createTextRange(); + range.move('character', caretPos); + return range.select(); + } +} + +function val(v, d) { + if (v != null) { + return v; + } else { + return d; + } +} + +Template.messagePopup.onCreated(function() { + const template = this; + template.textFilter = new ReactiveVar(''); + template.textFilterDelay = val(template.data.textFilterDelay, 0); + template.open = val(template.data.open, new ReactiveVar(false)); + template.hasData = new ReactiveVar(false); + template.value = new ReactiveVar; + template.trigger = val(template.data.trigger, ''); + template.triggerAnywhere = val(template.data.triggerAnywhere, true); + template.closeOnEsc = val(template.data.closeOnEsc, true); + template.blurOnSelectItem = val(template.data.blurOnSelectItem, false); + template.prefix = val(template.data.prefix, template.trigger); + template.suffix = val(template.data.suffix, ''); + if (template.triggerAnywhere === true) { + template.matchSelectorRegex = val(template.data.matchSelectorRegex, new RegExp(`(?:^| )${ template.trigger }[^\\s]*$`)); + } else { + template.matchSelectorRegex = val(template.data.matchSelectorRegex, new RegExp(`(?:^)${ template.trigger }[^\\s]*$`)); + } + template.selectorRegex = val(template.data.selectorRegex, new RegExp(`${ template.trigger }([^\\s]*)$`)); + template.replaceRegex = val(template.data.replaceRegex, new RegExp(`${ template.trigger }[^\\s]*$`)); + template.getValue = val(template.data.getValue, function(_id) { + return _id; + }); + template.up = () => { + const current = template.find('.popup-item.selected'); + const previous = $(current).prev('.popup-item')[0] || template.find('.popup-item:last-child'); + if (previous != null) { + current.className = current.className.replace(/\sselected/, ''); + previous.className += ' selected'; + return template.value.set(previous.getAttribute('data-id')); + } + }; + template.down = () => { + const current = template.find('.popup-item.selected'); + const next = $(current).next('.popup-item')[0] || template.find('.popup-item'); + if (next && next.classList.contains('popup-item')) { + current.className = current.className.replace(/\sselected/, ''); + next.className += ' selected'; + return template.value.set(next.getAttribute('data-id')); + } + }; + template.verifySelection = () => { + const current = template.find('.popup-item.selected'); + if (current == null) { + const first = template.find('.popup-item'); + if (first != null) { + first.className += ' selected'; + return template.value.set(first.getAttribute('data-id')); + } else { + return template.value.set(null); + } + } + }; + template.onInputKeydown = (event) => { + if (template.open.curValue !== true || template.hasData.curValue !== true) { + return; + } + if (event.which === keys.ENTER || event.which === keys.TAB) { + if (template.blurOnSelectItem === true) { + template.input.blur(); + } else { + template.open.set(false); + } + template.enterValue(); + if (template.data.cleanOnEnter) { + template.input.value = ''; + } + event.preventDefault(); + event.stopPropagation(); + return; + } + if (event.which === keys.ARROW_UP) { + template.up(); + event.preventDefault(); + event.stopPropagation(); + return; + } + if (event.which === keys.ARROW_DOWN) { + template.down(); + event.preventDefault(); + event.stopPropagation(); + } + }; + + template.setTextFilter = _.debounce(function(value) { + return template.textFilter.set(value); + }, template.textFilterDelay); + + template.onInputKeyup = (event) => { + if (template.closeOnEsc === true && template.open.curValue === true && event.which === keys.ESC) { + template.open.set(false); + event.preventDefault(); + event.stopPropagation(); + return; + } + const value = template.input.value.substr(0, getCursorPosition(template.input)); + + if (template.matchSelectorRegex.test(value)) { + template.setTextFilter(value.match(template.selectorRegex)[1]); + template.open.set(true); + } else { + template.open.set(false); + } + if (template.open.curValue !== true) { + return; + } + if (event.which !== keys.ARROW_UP && event.which !== keys.ARROW_DOWN) { + return Meteor.defer(function() { + template.verifySelection(); + }); + } + }; + template.onFocus = () => { + template.clickingItem = false; + if (template.open.curValue === true) { + return; + } + const value = template.input.value.substr(0, getCursorPosition(template.input)); + if (template.matchSelectorRegex.test(value)) { + template.setTextFilter(value.match(template.selectorRegex)[1]); + template.open.set(true); + return Meteor.defer(function() { + return template.verifySelection(); + }); + } else { + return template.open.set(false); + } + }; + + template.onBlur = () => { + if (template.open.curValue === false) { + return; + } + if (template.clickingItem === true) { + return; + } + return template.open.set(false); + }; + + template.enterValue = function() { + if (template.value.curValue == null) { + return; + } + const value = template.input.value; + const caret = getCursorPosition(template.input); + let firstPartValue = value.substr(0, caret); + const lastPartValue = value.substr(caret); + const getValue = this.getValue(template.value.curValue, template.data.collection, template.records.get(), firstPartValue); + if (!getValue) { + return; + } + firstPartValue = firstPartValue.replace(template.selectorRegex, template.prefix + getValue + template.suffix); + template.input.value = firstPartValue + lastPartValue; + return setCursorPosition(template.input, firstPartValue.length); + }; + template.records = new ReactiveVar([]); + Tracker.autorun(function() { + if (template.data.collection.findOne != null) { + template.data.collection.find().count(); + } + const filter = template.textFilter.get(); + if (filter != null) { + const filterCallback = (result) => { + template.hasData.set(result && result.length > 0); + template.records.set(result); + return Meteor.defer(function() { + return template.verifySelection(); + }); + }; + const result = template.data.getFilter(template.data.collection, filter, filterCallback); + if (result != null) { + return filterCallback(result); + } + } + }); +}); + +Template.messagePopup.onRendered(function() { + if (this.data.getInput != null) { + this.input = typeof this.data.getInput === 'function' && this.data.getInput(); + } else if (this.data.input) { + this.input = this.parentTemplate().find(this.data.input); + } + if (this.input == null) { + console.error('Input not found for popup'); + } + $(this.input).on('keyup', this.onInputKeyup.bind(this)); + $(this.input).on('keydown', this.onInputKeydown.bind(this)); + $(this.input).on('focus', this.onFocus.bind(this)); + return $(this.input).on('blur', this.onBlur.bind(this)); +}); + +Template.messagePopup.onDestroyed(function() { + $(this.input).off('keyup', this.onInputKeyup); + $(this.input).off('keydown', this.onInputKeydown); + $(this.input).off('focus', this.onFocus); + return $(this.input).off('blur', this.onBlur); +}); + +Template.messagePopup.events({ + 'mouseenter .popup-item'(e) { + if (e.currentTarget.className.indexOf('selected') > -1) { + return; + } + const template = Template.instance(); + const current = template.find('.popup-item.selected'); + if (current != null) { + current.className = current.className.replace(/\sselected/, ''); + } + e.currentTarget.className += ' selected'; + return template.value.set(this._id); + }, + 'mousedown .popup-item, touchstart .popup-item'() { + const template = Template.instance(); + return template.clickingItem = true; + }, + 'mouseup .popup-item, touchend .popup-item'() { + const template = Template.instance(); + template.clickingItem = false; + template.value.set(this._id); + template.enterValue(); + template.open.set(false); + return toolbarSearch.clear(); + } +}); + +Template.messagePopup.helpers({ + isOpen() { + return Template.instance().open.get() && ((Template.instance().hasData.get() || (Template.instance().data.emptyTemplate != null)) || !Template.instance().parentTemplate(1).subscriptionsReady()); + }, + data() { + const template = Template.instance(); + return template.records.get(); + } +}); diff --git a/packages/rocketchat-ui-message/client/popup/messagePopupConfig.coffee b/packages/rocketchat-ui-message/client/popup/messagePopupConfig.coffee deleted file mode 100644 index a5bc17cc33fc8ed2fd76ebf284b1ac7c42028ec2..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-message/client/popup/messagePopupConfig.coffee +++ /dev/null @@ -1,235 +0,0 @@ -@filteredUsersMemory = new Mongo.Collection null - -Meteor.startup -> - Tracker.autorun -> - if not Meteor.user()? or not Session.get('openedRoom')? - return - - filteredUsersMemory.remove({}) - messageUsers = RocketChat.models.Messages.find({rid: Session.get('openedRoom'), 'u.username': {$ne: Meteor.user().username}}, {fields: {'u.username': 1, 'u.name': 1, ts: 1}, sort: {ts: -1}}).fetch() - uniqueMessageUsersControl = {} - messageUsers.forEach (messageUser) -> - if not uniqueMessageUsersControl[messageUser.u.username]? - uniqueMessageUsersControl[messageUser.u.username] = true - filteredUsersMemory.upsert messageUser.u.username, - _id: messageUser.u.username - username: messageUser.u.username - name: messageUser.u.name - status: Session.get('user_' + messageUser.u.username + '_status') or 'offline' - ts: messageUser.ts - - -getUsersFromServer = (filter, records, cb) => - messageUsers = _.pluck(records, 'username') - Meteor.call 'spotlight', filter, messageUsers, { users: true }, (err, results) -> - if err? - return console.error err - - if results.users.length > 0 - for result in results.users - if records.length < 5 - records.push - _id: result.username - username: result.username - status: 'offline' - sort: 3 - - records = _.sortBy(records, 'sort') - - cb(records) - -getRoomsFromServer = (filter, records, cb) => - Meteor.call 'spotlight', filter, null, { rooms: true }, (err, results) -> - if err? - return console.error err - - if results.rooms.length > 0 - for room in results.rooms - if records.length < 5 - records.push room - - cb(records) - -getUsersFromServerDelayed = _.throttle getUsersFromServer, 500 -getRoomsFromServerDelayed = _.throttle getRoomsFromServer, 500 - - -Template.messagePopupConfig.helpers - popupUserConfig: -> - self = this - template = Template.instance() - - config = - title: t('People') - collection: filteredUsersMemory - template: 'messagePopupUser' - getInput: self.getInput - textFilterDelay: 200 - trigger: '@' - suffix: ' ' - getFilter: (collection, filter, cb) -> - exp = new RegExp("#{RegExp.escape filter}", 'i') - - # Get users from messages - items = filteredUsersMemory.find({ts: {$exists: true}, $or: [{username: exp}, {name: exp}]}, {limit: 5, sort: {ts: -1}}).fetch() - - # Get online users - if items.length < 5 and filter?.trim() isnt '' - messageUsers = _.pluck(items, 'username') - Meteor.users.find({$and: [{$or:[{username: exp}, {name: exp}]}, {username: {$nin: [Meteor.user()?.username].concat(messageUsers)}}]}, {limit: 5 - messageUsers.length}).fetch().forEach (item) -> - items.push - _id: item.username - username: item.username - name: item.name - status: item.status - sort: 1 - - # # Get users of room - # if items.length < 5 and filter?.trim() isnt '' - # messageUsers = _.pluck(items, 'username') - # Tracker.nonreactive -> - # roomUsernames = RocketChat.models.Rooms.findOne(Session.get('openedRoom')).usernames - # for roomUsername in roomUsernames - # if messageUsers.indexOf(roomUsername) is -1 and exp.test(roomUsername) - # items.push - # _id: roomUsername - # username: roomUsername - # status: Session.get('user_' + roomUsername + '_status') or 'offline' - # sort: 2 - - # if items.length >= 5 - # break - - # Get users from db - if items.length < 5 and filter?.trim() isnt '' - getUsersFromServerDelayed filter, items, cb - - all = - _id: 'all' - username: 'all' - system: true - name: t 'Notify_all_in_this_room' - compatibility: 'channel group' - sort: 4 - - exp = new RegExp("(^|\\s)#{RegExp.escape filter}", 'i') - if exp.test(all.username) or exp.test(all.compatibility) - items.push all - - here = - _id: 'here' - username: 'here' - system: true - name: t 'Notify_active_in_this_room' - compatibility: 'channel group' - sort: 4 - - if exp.test(here.username) or exp.test(here.compatibility) - items.push here - - return items - - getValue: (_id) -> - return _id - - return config - - popupChannelConfig: -> - self = this - template = Template.instance() - - config = - title: t('Channels') - collection: RocketChat.models.Subscriptions - trigger: '#' - suffix: ' ' - template: 'messagePopupChannel' - getInput: self.getInput - getFilter: (collection, filter, cb) -> - exp = new RegExp(filter, 'i') - - records = collection.find({name: exp, t: {$in: ['c', 'p']}}, {limit: 5, sort: {ls: -1}}).fetch() - - if records.length < 5 and filter?.trim() isnt '' - getRoomsFromServerDelayed filter, records, cb - - return records - - getValue: (_id, collection, records) -> - return _.findWhere(records, {_id: _id})?.name - - return config - - popupSlashCommandsConfig: -> - self = this - template = Template.instance() - - config = - title: t('Commands') - collection: RocketChat.slashCommands.commands - trigger: '/' - suffix: ' ' - triggerAnywhere: false - template: 'messagePopupSlashCommand' - getInput: self.getInput - getFilter: (collection, filter) -> - commands = [] - for command, item of collection - if command.indexOf(filter) > -1 - commands.push - _id: command - params: if item.params then TAPi18n.__ item.params else '' - description: TAPi18n.__ item.description - - commands = commands.sort (a, b) -> - return a._id > b._id - - commands = commands[0..10] - - return commands - - return config - - emojiEnabled: -> - return RocketChat.emoji? - - popupEmojiConfig: -> - if RocketChat.emoji? - self = this - template = Template.instance() - config = - title: t('Emoji') - collection: RocketChat.emoji.list - template: 'messagePopupEmoji' - trigger: ':' - prefix: '' - suffix: ' ' - getInput: self.getInput - getFilter: (collection, filter, cb) -> - results = [] - key = ':' + filter - - if RocketChat.emoji.packages.emojione?.asciiList[key] or filter.length < 2 - return [] - - regExp = new RegExp('^' + RegExp.escape(key), 'i') - - for key, value of collection - if results.length > 10 - break - - if regExp.test(key) - results.push - _id: key - data: value - - results.sort (a, b) -> - if a._id < b._id - return -1 - if a._id > b._id - return 1 - return 0 - - return results - - return config diff --git a/packages/rocketchat-ui-message/client/popup/messagePopupConfig.js b/packages/rocketchat-ui-message/client/popup/messagePopupConfig.js new file mode 100644 index 0000000000000000000000000000000000000000..797e77fa9ca4dbdd6c993f1228f88d0e96b2f9b4 --- /dev/null +++ b/packages/rocketchat-ui-message/client/popup/messagePopupConfig.js @@ -0,0 +1,295 @@ +/* globals filteredUsersMemory */ +filteredUsersMemory = new Mongo.Collection(null); + +Meteor.startup(function() { + Tracker.autorun(function() { + if (Meteor.user() == null || Session.get('openedRoom') == null) { + return; + } + filteredUsersMemory.remove({}); + const messageUsers = RocketChat.models.Messages.find({ + rid: Session.get('openedRoom'), + 'u.username': { + $ne: Meteor.user().username + } + }, { + fields: { + 'u.username': 1, + 'u.name': 1, + ts: 1 + }, + sort: { + ts: -1 + } + }).fetch(); + const uniqueMessageUsersControl = {}; + return messageUsers.forEach(function(messageUser) { + if (uniqueMessageUsersControl[messageUser.u.username] == null) { + uniqueMessageUsersControl[messageUser.u.username] = true; + return filteredUsersMemory.upsert(messageUser.u.username, { + _id: messageUser.u.username, + username: messageUser.u.username, + name: messageUser.u.name, + status: Session.get(`user_${ messageUser.u.username }_status`) || 'offline', + ts: messageUser.ts + }); + } + }); + }); +}); + +const getUsersFromServer = (filter, records, cb) => { + const messageUsers = _.pluck(records, 'username'); + return Meteor.call('spotlight', filter, messageUsers, { + users: true + }, function(err, results) { + if (err != null) { + return console.error(err); + } + if (results.users.length > 0) { + results.users.forEach(result => { + if (records.length < 5) { + records.push({ + _id: result.username, + username: result.username, + status: 'offline', + sort: 3 + }); + } + }); + records = _.sortBy(records, 'sort'); + return cb(records); + } + }); +}; + +const getRoomsFromServer = (filter, records, cb) => { + return Meteor.call('spotlight', filter, null, { + rooms: true + }, function(err, results) { + if (err != null) { + return console.error(err); + } + if (results.rooms.length > 0) { + results.rooms.forEach(room => { + if (records.length < 5) { + records.push(room); + } + }); + return cb(records); + } + }); +}; + +const getUsersFromServerDelayed = _.throttle(getUsersFromServer, 500); + +const getRoomsFromServerDelayed = _.throttle(getRoomsFromServer, 500); + +Template.messagePopupConfig.helpers({ + popupUserConfig() { + const self = this; + const config = { + title: t('People'), + collection: filteredUsersMemory, + template: 'messagePopupUser', + getInput: self.getInput, + textFilterDelay: 200, + trigger: '@', + suffix: ' ', + getFilter(collection, filter, cb) { + let exp = new RegExp(`${ RegExp.escape(filter) }`, 'i'); + // Get users from messages + const items = filteredUsersMemory.find({ + ts: { + $exists: true + }, + $or: [ + { + username: exp + }, { + name: exp + } + ] + }, { + limit: 5, + sort: { + ts: -1 + } + }).fetch(); + // Get online users + if (items.length < 5 && filter && filter.trim() !== '') { + const messageUsers = _.pluck(items, 'username'); + const user = Meteor.user(); + items.push(...Meteor.users.find({ + $and: [ + { + $or: [ + { + username: exp + }, { + name: exp + } + ] + }, { + username: { + $nin: [(user && user.username), ...messageUsers] + } + } + ] + }, { + limit: 5 - messageUsers.length + }).fetch().map(function(item) { + return { + _id: item.username, + username: item.username, + name: item.name, + status: item.status, + sort: 1 + }; + })); + } + // Get users from db + if (items.length < 5 && filter && filter.trim() !== '') { + getUsersFromServerDelayed(filter, items, cb); + } + const all = { + _id: 'all', + username: 'all', + system: true, + name: t('Notify_all_in_this_room'), + compatibility: 'channel group', + sort: 4 + }; + exp = new RegExp(`(^|\\s)${ RegExp.escape(filter) }`, 'i'); + if (exp.test(all.username) || exp.test(all.compatibility)) { + items.push(all); + } + const here = { + _id: 'here', + username: 'here', + system: true, + name: t('Notify_active_in_this_room'), + compatibility: 'channel group', + sort: 4 + }; + if (exp.test(here.username) || exp.test(here.compatibility)) { + items.push(here); + } + return items; + }, + getValue(_id) { + return _id; + } + }; + return config; + }, + popupChannelConfig() { + const self = this; + const config = { + title: t('Channels'), + collection: RocketChat.models.Subscriptions, + trigger: '#', + suffix: ' ', + template: 'messagePopupChannel', + getInput: self.getInput, + getFilter(collection, filter, cb) { + const exp = new RegExp(filter, 'i'); + const records = collection.find({ + name: exp, + t: { + $in: ['c', 'p'] + } + }, { + limit: 5, + sort: { + ls: -1 + } + }).fetch(); + + if (records.length < 5 && filter && filter.trim() !== '') { + getRoomsFromServerDelayed(filter, records, cb); + } + return records; + }, + getValue(_id, collection, records) { + const record = _.findWhere(records, { + _id + }); + return record && record.name; + } + }; + return config; + }, + popupSlashCommandsConfig() { + const self = this; + const config = { + title: t('Commands'), + collection: RocketChat.slashCommands.commands, + trigger: '/', + suffix: ' ', + triggerAnywhere: false, + template: 'messagePopupSlashCommand', + getInput: self.getInput, + getFilter(collection, filter) { + return Object.keys(collection).map(command => { + const item = collection[command]; + return { + _id: command, + params: item.params ? TAPi18n.__(item.params) : '', + description: TAPi18n.__(item.description) + }; + }) + .filter(command => command._id.indexOf(filter) > -1) + .sort(function(a, b) { + return a._id > b._id; + }) + .slice(0, 11); + } + }; + return config; + }, + emojiEnabled() { + return RocketChat.emoji != null; + }, + popupEmojiConfig() { + if (RocketChat.emoji != null) { + const self = this; + return { + title: t('Emoji'), + collection: RocketChat.emoji.list, + template: 'messagePopupEmoji', + trigger: ':', + prefix: '', + suffix: ' ', + getInput: self.getInput, + getFilter(collection, filter) { + const key = `:${ filter }`; + + if (!RocketChat.emoji.packages.emojione || RocketChat.emoji.packages.emojione.asciiList[key] || filter.length < 2) { + return []; + } + + const regExp = new RegExp(`^${ RegExp.escape(key) }`, 'i'); + return Object.keys(collection).map(key => { + const value = collection[key]; + return { + _id: key, + data: value + }; + }) + .filter(obj => regExp.test(obj._id)) + .slice(0, 10) + .sort(function(a, b) { + if (a._id < b._id) { + return -1; + } + if (a._id > b._id) { + return 1; + } + return 0; + }); + } + }; + } + } +}); diff --git a/packages/rocketchat-ui-message/client/popup/messagePopupEmoji.coffee b/packages/rocketchat-ui-message/client/popup/messagePopupEmoji.coffee deleted file mode 100644 index d50a31f2c236aa0a71d4bc5144f84d2101e4a68d..0000000000000000000000000000000000000000 --- a/packages/rocketchat-ui-message/client/popup/messagePopupEmoji.coffee +++ /dev/null @@ -1,4 +0,0 @@ -Template.messagePopupEmoji.helpers - value: -> - length = this.data.length - return this.data[length - 1] diff --git a/packages/rocketchat-ui-message/client/popup/messagePopupEmoji.js b/packages/rocketchat-ui-message/client/popup/messagePopupEmoji.js new file mode 100644 index 0000000000000000000000000000000000000000..934067a235e72a70cbf29af8cd85cb262cd640c8 --- /dev/null +++ b/packages/rocketchat-ui-message/client/popup/messagePopupEmoji.js @@ -0,0 +1,6 @@ +Template.messagePopupEmoji.helpers({ + value() { + const length = this.data.length; + return this.data[length - 1]; + } +}); diff --git a/packages/rocketchat-ui-message/package.js b/packages/rocketchat-ui-message/package.js index 4d26d912832fbb5d6ff45c6ab902d02cabf39663..bcf2b9e3ddfd66b221c62bd5091f7ecab70fdcd1 100644 --- a/packages/rocketchat-ui-message/package.js +++ b/packages/rocketchat-ui-message/package.js @@ -15,7 +15,6 @@ Package.onUse(function(api) { 'mongo', 'ecmascript', 'templating', - 'coffeescript', 'underscore', 'tracker', 'rocketchat:lib', @@ -33,12 +32,12 @@ Package.onUse(function(api) { api.addFiles('client/popup/messagePopupSlashCommand.html', 'client'); api.addFiles('client/popup/messagePopupUser.html', 'client'); - api.addFiles('client/message.coffee', 'client'); - api.addFiles('client/messageBox.coffee', 'client'); - api.addFiles('client/popup/messagePopup.coffee', 'client'); + api.addFiles('client/message.js', 'client'); + api.addFiles('client/messageBox.js', 'client'); + api.addFiles('client/popup/messagePopup.js', 'client'); api.addFiles('client/popup/messagePopupChannel.js', 'client'); - api.addFiles('client/popup/messagePopupConfig.coffee', 'client'); - api.addFiles('client/popup/messagePopupEmoji.coffee', 'client'); + api.addFiles('client/popup/messagePopupConfig.js', 'client'); + api.addFiles('client/popup/messagePopupEmoji.js', 'client'); api.addFiles('client/renderMessageBody.js', 'client'); diff --git a/packages/rocketchat-ui/client/lib/chatMessages.coffee b/packages/rocketchat-ui/client/lib/chatMessages.coffee index 42ddfe99488636ad8e0002168cb30240de0a92c8..bdeb2d72acda382f5cf597b44074848582831723 100644 --- a/packages/rocketchat-ui/client/lib/chatMessages.coffee +++ b/packages/rocketchat-ui/client/lib/chatMessages.coffee @@ -250,8 +250,9 @@ class @ChatMessages $('.sweet-alert').addClass 'visible' deleteMsg: (message) -> + forceDelete = RocketChat.authz.hasAtLeastOnePermission('force-delete-message', message.rid) blockDeleteInMinutes = RocketChat.settings.get 'Message_AllowDeleting_BlockDeleteInMinutes' - if blockDeleteInMinutes? and blockDeleteInMinutes isnt 0 + if blockDeleteInMinutes? and blockDeleteInMinutes isnt 0 and forceDelete is false msgTs = moment(message.ts) if message.ts? currentTsDiff = moment().diff(msgTs, 'minutes') if msgTs? if currentTsDiff > blockDeleteInMinutes diff --git a/packages/rocketchat-ui/client/lib/fileUpload.coffee b/packages/rocketchat-ui/client/lib/fileUpload.coffee index 55507fe06189668c34d6907074167a26b5f58fe6..e4494b7d78683b94332b90b20b9f7b2505f282f0 100644 --- a/packages/rocketchat-ui/client/lib/fileUpload.coffee +++ b/packages/rocketchat-ui/client/lib/fileUpload.coffee @@ -9,6 +9,8 @@ getUploadPreview = (file, callback) -> # If greater then 10MB don't try and show a preview if file.file.size > 10 * 1000000 callback(file, null) + else if not file.file.type? + callback(file, null) else if file.file.type.indexOf('audio') > -1 or file.file.type.indexOf('video') > -1 or file.file.type.indexOf('image') > -1 file.type = file.file.type.split('/')[0] diff --git a/packages/rocketchat-ui/client/lib/fireEvent.js b/packages/rocketchat-ui/client/lib/fireEvent.js index 3a1ae3fe6c102a280980407250817f967bd7c2fa..a3456d3e61a6d235a9b1d180b6c1be4c54ea2718 100644 --- a/packages/rocketchat-ui/client/lib/fireEvent.js +++ b/packages/rocketchat-ui/client/lib/fireEvent.js @@ -1,4 +1,6 @@ window.fireGlobalEvent = function _fireGlobalEvent(eventName, params) { + window.dispatchEvent(new CustomEvent(eventName, {detail: params})); + Tracker.autorun((computation) => { const enabled = RocketChat.settings.get('Iframe_Integration_send_enable'); if (enabled === undefined) { @@ -6,7 +8,6 @@ window.fireGlobalEvent = function _fireGlobalEvent(eventName, params) { } computation.stop(); if (enabled) { - window.dispatchEvent(new CustomEvent(eventName, {detail: params})); parent.postMessage({ eventName, data: params @@ -14,3 +15,4 @@ window.fireGlobalEvent = function _fireGlobalEvent(eventName, params) { } }); }; + diff --git a/packages/rocketchat-webrtc/WebRTCClass.coffee b/packages/rocketchat-webrtc/WebRTCClass.coffee deleted file mode 100644 index a957e411d7cc480832d41bd9cdc3ce66a4715c50..0000000000000000000000000000000000000000 --- a/packages/rocketchat-webrtc/WebRTCClass.coffee +++ /dev/null @@ -1,823 +0,0 @@ -emptyFn = -> - # empty - -class WebRTCTransportClass - debug: false - - log: -> - if @debug is true - console.log.apply(console, arguments) - - constructor: (@webrtcInstance) -> - @callbacks = {} - - RocketChat.Notifications.onRoom @webrtcInstance.room, 'webrtc', (type, data) => - @log 'WebRTCTransportClass - onRoom', type, data - - switch type - when 'status' - if @callbacks['onRemoteStatus']?.length > 0 - fn(data) for fn in @callbacks['onRemoteStatus'] - - onUserStream: (type, data) -> - if data.room isnt @webrtcInstance.room then return - @log 'WebRTCTransportClass - onUser', type, data - - switch type - when 'call' - if @callbacks['onRemoteCall']?.length > 0 - fn(data) for fn in @callbacks['onRemoteCall'] - - when 'join' - if @callbacks['onRemoteJoin']?.length > 0 - fn(data) for fn in @callbacks['onRemoteJoin'] - - when 'candidate' - if @callbacks['onRemoteCandidate']?.length > 0 - fn(data) for fn in @callbacks['onRemoteCandidate'] - - when 'description' - if @callbacks['onRemoteDescription']?.length > 0 - fn(data) for fn in @callbacks['onRemoteDescription'] - - startCall: (data) -> - @log 'WebRTCTransportClass - startCall', @webrtcInstance.room, @webrtcInstance.selfId - RocketChat.Notifications.notifyUsersOfRoom @webrtcInstance.room, 'webrtc', 'call', - from: @webrtcInstance.selfId - room: @webrtcInstance.room - media: data.media - monitor: data.monitor - - joinCall: (data) -> - @log 'WebRTCTransportClass - joinCall', @webrtcInstance.room, @webrtcInstance.selfId - if data.monitor is true - RocketChat.Notifications.notifyUser data.to, 'webrtc', 'join', - from: @webrtcInstance.selfId - room: @webrtcInstance.room - media: data.media - monitor: data.monitor - else - RocketChat.Notifications.notifyUsersOfRoom @webrtcInstance.room, 'webrtc', 'join', - from: @webrtcInstance.selfId - room: @webrtcInstance.room - media: data.media - monitor: data.monitor - - sendCandidate: (data) -> - data.from = @webrtcInstance.selfId - data.room = @webrtcInstance.room - @log 'WebRTCTransportClass - sendCandidate', data - RocketChat.Notifications.notifyUser data.to, 'webrtc', 'candidate', data - - sendDescription: (data) -> - data.from = @webrtcInstance.selfId - data.room = @webrtcInstance.room - @log 'WebRTCTransportClass - sendDescription', data - RocketChat.Notifications.notifyUser data.to, 'webrtc', 'description', data - - sendStatus: (data) -> - @log 'WebRTCTransportClass - sendStatus', data, @webrtcInstance.room - data.from = @webrtcInstance.selfId - RocketChat.Notifications.notifyRoom @webrtcInstance.room, 'webrtc', 'status', data - - onRemoteCall: (fn) -> - @callbacks['onRemoteCall'] ?= [] - @callbacks['onRemoteCall'].push fn - - onRemoteJoin: (fn) -> - @callbacks['onRemoteJoin'] ?= [] - @callbacks['onRemoteJoin'].push fn - - onRemoteCandidate: (fn) -> - @callbacks['onRemoteCandidate'] ?= [] - @callbacks['onRemoteCandidate'].push fn - - onRemoteDescription: (fn) -> - @callbacks['onRemoteDescription'] ?= [] - @callbacks['onRemoteDescription'].push fn - - onRemoteStatus: (fn) -> - @callbacks['onRemoteStatus'] ?= [] - @callbacks['onRemoteStatus'].push fn - - -class WebRTCClass - config: - iceServers: [] - - debug: false - - transportClass: WebRTCTransportClass - - - ### - @param seldId {String} - @param room {String} - ### - constructor: (@selfId, @room) -> - @config.iceServers = [] - - servers = RocketChat.settings.get("WebRTC_Servers") - if servers?.trim() isnt '' - servers = servers.replace /\s/g, '' - servers = servers.split ',' - for server in servers - server = server.split '@' - serverConfig = - urls: server.pop() - - if server.length is 1 - server = server[0].split ':' - serverConfig.username = decodeURIComponent(server[0]) - serverConfig.credential = decodeURIComponent(server[1]) - - @config.iceServers.push serverConfig - - @peerConnections = {} - - @remoteItems = new ReactiveVar [] - @remoteItemsById = new ReactiveVar {} - @callInProgress = new ReactiveVar false - @audioEnabled = new ReactiveVar true - @videoEnabled = new ReactiveVar true - @overlayEnabled = new ReactiveVar false - @screenShareEnabled = new ReactiveVar false - @localUrl = new ReactiveVar - - @active = false - @remoteMonitoring = false - @monitor = false - @autoAccept = false - - @navigator = undefined - userAgent = navigator.userAgent.toLocaleLowerCase(); - if userAgent.indexOf('electron') isnt -1 - @navigator = 'electron' - else if userAgent.indexOf('chrome') isnt -1 - @navigator = 'chrome' - else if userAgent.indexOf('firefox') isnt -1 - @navigator = 'firefox' - else if userAgent.indexOf('safari') isnt -1 - @navigator = 'safari' - - @screenShareAvailable = @navigator in ['chrome', 'firefox', 'electron'] - - @media = - video: false - audio: true - - @transport = new @transportClass @ - - @transport.onRemoteCall @onRemoteCall.bind @ - @transport.onRemoteJoin @onRemoteJoin.bind @ - @transport.onRemoteCandidate @onRemoteCandidate.bind @ - @transport.onRemoteDescription @onRemoteDescription.bind @ - @transport.onRemoteStatus @onRemoteStatus.bind @ - - Meteor.setInterval @checkPeerConnections.bind(@), 1000 - - # Meteor.setInterval @broadcastStatus.bind(@), 1000 - - log: -> - if @debug is true - console.log.apply(console, arguments) - - onError: -> - console.error.apply(console, arguments) - - checkPeerConnections: -> - for id, peerConnection of @peerConnections - if peerConnection.iceConnectionState not in ['connected', 'completed'] and peerConnection.createdAt + 5000 < Date.now() - @stopPeerConnection id - - updateRemoteItems: -> - items = [] - itemsById = {} - - for id, peerConnection of @peerConnections - for remoteStream in peerConnection.getRemoteStreams() - item = - id: id - url: URL.createObjectURL(remoteStream) - state: peerConnection.iceConnectionState - - switch peerConnection.iceConnectionState - when 'checking' - item.stateText = 'Connecting...' - - when 'connected', 'completed' - item.stateText = 'Connected' - item.connected = true - - when 'disconnected' - item.stateText = 'Disconnected' - - when 'failed' - item.stateText = 'Failed' - - when 'closed' - item.stateText = 'Closed' - - items.push item - itemsById[id] = item - - @remoteItems.set items - @remoteItemsById.set itemsById - - resetCallInProgress: -> - @callInProgress.set false - - broadcastStatus: -> - if @active isnt true or @monitor is true or @remoteMonitoring is true then return - - remoteConnections = [] - for id, peerConnection of @peerConnections - remoteConnections.push - id: id - media: peerConnection.remoteMedia - - @transport.sendStatus - media: @media - remoteConnections: remoteConnections - - ### - @param data {Object} - from {String} - media {Object} - remoteConnections {Array[Object]} - id {String} - media {Object} - ### - onRemoteStatus: (data) -> - # @log 'onRemoteStatus', arguments - - @callInProgress.set true - - Meteor.clearTimeout @callInProgressTimeout - @callInProgressTimeout = Meteor.setTimeout @resetCallInProgress.bind(@), 2000 - - if @active isnt true then return - - remoteConnections = [{id: data.from, media: data.media}].concat data.remoteConnections - - for remoteConnection in remoteConnections - if remoteConnection.id isnt @selfId and not @peerConnections[remoteConnection.id]? - @log 'reconnecting with', remoteConnection.id - @onRemoteJoin - from: remoteConnection.id - media: remoteConnection.media - - ### - @param id {String} - ### - getPeerConnection: (id) -> - return @peerConnections[id] if @peerConnections[id]? - - peerConnection = new RTCPeerConnection @config - - peerConnection.createdAt = Date.now() - peerConnection.remoteMedia = {} - - @peerConnections[id] = peerConnection - - eventNames = [ - 'icecandidate' - 'addstream' - 'removestream' - 'iceconnectionstatechange' - 'datachannel' - 'identityresult' - 'idpassertionerror' - 'idpvalidationerror' - 'negotiationneeded' - 'peeridentity' - 'signalingstatechange' - ] - - for eventName in eventNames - peerConnection.addEventListener eventName, (e) => - @log id, e.type, e - - peerConnection.addEventListener 'icecandidate', (e) => - if not e.candidate? - return - - @transport.sendCandidate - to: id - candidate: - candidate: e.candidate.candidate - sdpMLineIndex: e.candidate.sdpMLineIndex - sdpMid: e.candidate.sdpMid - - peerConnection.addEventListener 'addstream', (e) => - @updateRemoteItems() - - peerConnection.addEventListener 'removestream', (e) => - @updateRemoteItems() - - peerConnection.addEventListener 'iceconnectionstatechange', (e) => - if peerConnection.iceConnectionState in ['disconnected', 'closed'] and peerConnection is @peerConnections[id] - @stopPeerConnection id - Meteor.setTimeout => - if Object.keys(@peerConnections).length is 0 - @stop() - , 3000 - - @updateRemoteItems() - - return peerConnection - - _getUserMedia: (media, onSuccess, onError) -> - onSuccessLocal = (stream) -> - if AudioContext? and stream.getAudioTracks().length > 0 - audioContext = new AudioContext - source = audioContext.createMediaStreamSource(stream) - - volume = audioContext.createGain() - source.connect(volume) - peer = audioContext.createMediaStreamDestination() - volume.connect(peer) - volume.gain.value = 0.6 - - stream.removeTrack(stream.getAudioTracks()[0]) - stream.addTrack(peer.stream.getAudioTracks()[0]) - stream.volume = volume - - this.audioContext = audioContext - - onSuccess(stream) - - navigator.getUserMedia media, onSuccessLocal, onError - - - getUserMedia: (media, onSuccess, onError=@onError) -> - if media.desktop isnt true - @_getUserMedia media, onSuccess, onError - return - - if @screenShareAvailable isnt true - console.log 'Screen share is not avaliable' - return - - getScreen = (audioStream) => - if document.cookie.indexOf("rocketchatscreenshare=chrome") is -1 and not window.rocketchatscreenshare? and @navigator isnt 'electron' - refresh = -> - swal - type: "warning" - title: TAPi18n.__ "Refresh_your_page_after_install_to_enable_screen_sharing" - - swal - type: "warning" - title: TAPi18n.__ "Screen_Share" - text: TAPi18n.__ "You_need_install_an_extension_to_allow_screen_sharing" - html: true - showCancelButton: true - confirmButtonText: TAPi18n.__ "Install_Extension" - cancelButtonText: TAPi18n.__ "Cancel" - , (isConfirm) => - if isConfirm - if @navigator is 'chrome' - url = 'https://chrome.google.com/webstore/detail/rocketchat-screen-share/nocfbnnmjnndkbipkabodnheejiegccf' - try - chrome.webstore.install url, refresh, -> - window.open(url) - refresh() - catch e - window.open(url) - refresh() - else if @navigator is 'firefox' - window.open('https://addons.mozilla.org/en-GB/firefox/addon/rocketchat-screen-share/') - refresh() - - return onError(false) - - getScreenSuccess = (stream) => - if audioStream? - stream.addTrack(audioStream.getAudioTracks()[0]) - onSuccess(stream) - - if @navigator is 'firefox' - media = - audio: media.audio - video: - mozMediaSource: 'window' - mediaSource: 'window' - @_getUserMedia media, getScreenSuccess, onError - else - ChromeScreenShare.getSourceId @navigator, (id) => - media = - audio: false - video: - mandatory: - chromeMediaSource: 'desktop' - chromeMediaSourceId: id - maxWidth: 1280 - maxHeight: 720 - - @_getUserMedia media, getScreenSuccess, onError - - if @navigator is 'firefox' or not media.audio? or media.audio is false - getScreen() - else - getAudioSuccess = (audioStream) => - getScreen(audioStream) - - getAudioError = => - getScreen() - - @_getUserMedia {audio: media.audio}, getAudioSuccess, getAudioError - - - ### - @param callback {Function} - ### - getLocalUserMedia: (callback) -> - @log 'getLocalUserMedia', arguments - - if @localStream? - return callback null, @localStream - - onSuccess = (stream) => - @localStream = stream - @localUrl.set URL.createObjectURL(stream) - - @videoEnabled.set @media.video is true - @audioEnabled.set @media.audio is true - - for id, peerConnection of @peerConnections - peerConnection.addStream stream - - callback null, @localStream - - onError = (error) => - callback false - @onError error - - @getUserMedia @media, onSuccess, onError - - - ### - @param id {String} - ### - stopPeerConnection: (id) -> - peerConnection = @peerConnections[id] - if not peerConnection? then return - - delete @peerConnections[id] - peerConnection.close() - - @updateRemoteItems() - - stopAllPeerConnections: -> - for id, peerConnection of @peerConnections - @stopPeerConnection id - window.audioContext?.close() - - setAudioEnabled: (enabled=true) -> - if @localStream? - if enabled is true and @media.audio isnt true - delete @localStream - @media.audio = true - @getLocalUserMedia => - @stopAllPeerConnections() - @joinCall() - else - @localStream.getAudioTracks().forEach (audio) -> audio.enabled = enabled - @audioEnabled.set enabled - - disableAudio: -> - @setAudioEnabled false - - enableAudio: -> - @setAudioEnabled true - - setVideoEnabled: (enabled=true) -> - if @localStream? - if enabled is true and @media.video isnt true - delete @localStream - @media.video = true - @getLocalUserMedia => - @stopAllPeerConnections() - @joinCall() - else - @localStream.getVideoTracks().forEach (video) -> video.enabled = enabled - @videoEnabled.set enabled - - disableScreenShare: -> - @setScreenShareEnabled false - - enableScreenShare: -> - @setScreenShareEnabled true - - setScreenShareEnabled: (enabled=true) -> - if @localStream? - @media.desktop = enabled - delete @localStream - @getLocalUserMedia (err) => - if err? - return - @screenShareEnabled.set enabled - @stopAllPeerConnections() - @joinCall() - - disableVideo: -> - @setVideoEnabled false - - enableVideo: -> - @setVideoEnabled true - - stop: -> - @active = false - @monitor = false - @remoteMonitoring = false - if @localStream? and typeof @localStream isnt 'undefined' - @localStream.getTracks().forEach (track) -> - track.stop() - @localUrl.set undefined - delete @localStream - - @stopAllPeerConnections() - - - ### - @param media {Object} - audio {Boolean} - video {Boolean} - ### - startCall: (media={}) -> - @log 'startCall', arguments - @media = media - @getLocalUserMedia => - @active = true - @transport.startCall - media: @media - - startCallAsMonitor: (media={}) -> - @log 'startCallAsMonitor', arguments - @media = media - @active = true - @monitor = true - @transport.startCall - media: @media - monitor: true - - - ### - @param data {Object} - from {String} - monitor {Boolean} - media {Object} - audio {Boolean} - video {Boolean} - ### - onRemoteCall: (data) -> - if @autoAccept is true - FlowRouter.goToRoomById data.room - Meteor.defer => - @joinCall - to: data.from - monitor: data.monitor - media: data.media - return - - fromUsername = Meteor.users.findOne(data.from)?.username - subscription = ChatSubscription.findOne({rid: data.room}) - - if data.monitor is true - icon = 'eye' - title = "Monitor call from #{fromUsername}" - else if subscription?.t is 'd' - if data.media?.video - icon = 'videocam' - title = "Direct video call from #{fromUsername}" - else - icon = 'phone' - title = "Direct audio call from #{fromUsername}" - else - if data.media?.video - icon = 'videocam' - title = "Group video call from #{subscription.name}" - else - icon = 'phone' - title = "Group audio call from #{subscription.name}" - - swal - title: "<i class='icon-#{icon} alert-icon success-color'></i>#{title}" - text: "Do you want to accept?" - html: true - showCancelButton: true - confirmButtonText: "Yes" - cancelButtonText: "No" - , (isConfirm) => - if isConfirm - FlowRouter.goToRoomById data.room - Meteor.defer => - @joinCall - to: data.from - monitor: data.monitor - media: data.media - else - @stop() - - - ### - @param data {Object} - to {String} - monitor {Boolean} - media {Object} - audio {Boolean} - video {Boolean} - desktop {Boolean} - ### - joinCall: (data={}) -> - if data.media?.audio? - @media.audio = data.media.audio - - if data.media?.video? - @media.video = data.media.video - - data.media = @media - - @log 'joinCall', arguments - @getLocalUserMedia => - @remoteMonitoring = data.monitor - @active = true - @transport.joinCall(data) - - - ### - @param data {Object} - from {String} - monitor {Boolean} - media {Object} - audio {Boolean} - video {Boolean} - desktop {Boolean} - ### - onRemoteJoin: (data) -> - if @active isnt true then return - - @log 'onRemoteJoin', arguments - - peerConnection = @getPeerConnection data.from - - # needsRefresh = false - # if peerConnection.iceConnectionState isnt 'new' - # needsAudio = data.media.audio is true and peerConnection.remoteMedia.audio isnt true - # needsVideo = data.media.video is true and peerConnection.remoteMedia.video isnt true - # needsRefresh = needsAudio or needsVideo or data.media.desktop isnt peerConnection.remoteMedia.desktop - - # if peerConnection.signalingState is "have-local-offer" or needsRefresh - if peerConnection.signalingState isnt "checking" - @stopPeerConnection data.from - peerConnection = @getPeerConnection data.from - - if peerConnection.iceConnectionState isnt 'new' - return - - peerConnection.remoteMedia = data.media - - peerConnection.addStream @localStream if @localStream - - onOffer = (offer) => - onLocalDescription = => - @transport.sendDescription - to: data.from - type: 'offer' - ts: peerConnection.createdAt - media: @media - description: - sdp: offer.sdp - type: offer.type - - peerConnection.setLocalDescription(new RTCSessionDescription(offer), onLocalDescription, @onError) - - if data.monitor is true - peerConnection.createOffer onOffer, @onError, - mandatory: - OfferToReceiveAudio: data.media.audio - OfferToReceiveVideo: data.media.video - else - peerConnection.createOffer(onOffer, @onError) - - - ### - @param data {Object} - from {String} - ts {Integer} - description {String} - ### - onRemoteOffer: (data) -> - if @active isnt true then return - - @log 'onRemoteOffer', arguments - peerConnection = @getPeerConnection data.from - - if peerConnection.signalingState in ["have-local-offer", "stable"] and peerConnection.createdAt < data.ts - @stopPeerConnection data.from - peerConnection = @getPeerConnection data.from - - if peerConnection.iceConnectionState isnt 'new' - return - - peerConnection.setRemoteDescription new RTCSessionDescription(data.description) - - try peerConnection.addStream @localStream if @localStream - - onAnswer = (answer) => - onLocalDescription = => - @transport.sendDescription - to: data.from - type: 'answer' - ts: peerConnection.createdAt - description: - sdp: answer.sdp - type: answer.type - - peerConnection.setLocalDescription(new RTCSessionDescription(answer), onLocalDescription, @onError) - - peerConnection.createAnswer(onAnswer, @onError) - - - ### - @param data {Object} - to {String} - from {String} - candidate {RTCIceCandidate JSON encoded} - ### - onRemoteCandidate: (data) -> - if @active isnt true then return - if data.to isnt @selfId then return - - @log 'onRemoteCandidate', arguments - peerConnection = @getPeerConnection data.from - - if peerConnection.iceConnectionState not in ["closed", "failed", "disconnected", "completed"] - peerConnection.addIceCandidate new RTCIceCandidate(data.candidate) - - - ### - @param data {Object} - to {String} - from {String} - type {String} [offer, answer] - description {RTCSessionDescription JSON encoded} - ts {Integer} - media {Object} - audio {Boolean} - video {Boolean} - desktop {Boolean} - ### - onRemoteDescription: (data) -> - if @active isnt true then return - if data.to isnt @selfId then return - - @log 'onRemoteDescription', arguments - peerConnection = @getPeerConnection data.from - - if data.type is 'offer' - peerConnection.remoteMedia = data.media - @onRemoteOffer - from: data.from - ts: data.ts - description: data.description - else - peerConnection.setRemoteDescription new RTCSessionDescription(data.description) - - -WebRTC = new class - constructor: -> - @instancesByRoomId = {} - - getInstanceByRoomId: (roomId) -> - subscription = ChatSubscription.findOne({rid: roomId}) - if not subscription - return - - enabled = false - switch subscription.t - when 'd' - enabled = RocketChat.settings.get('WebRTC_Enable_Direct') - when 'p' - enabled = RocketChat.settings.get('WebRTC_Enable_Private') - when 'c' - enabled = RocketChat.settings.get('WebRTC_Enable_Channel') - - if enabled is false - return - - if not @instancesByRoomId[roomId]? - @instancesByRoomId[roomId] = new WebRTCClass Meteor.userId(), roomId - - return @instancesByRoomId[roomId] - - -Meteor.startup -> - Tracker.autorun -> - if Meteor.userId() - RocketChat.Notifications.onUser 'webrtc', (type, data) => - if not data.room? then return - - webrtc = WebRTC.getInstanceByRoomId(data.room) - - webrtc.transport.onUserStream type, data diff --git a/packages/rocketchat-webrtc/client/WebRTCClass.js b/packages/rocketchat-webrtc/client/WebRTCClass.js new file mode 100644 index 0000000000000000000000000000000000000000..dfbccbd4391f6c16874c8440c7ebd9e23e8c378b --- /dev/null +++ b/packages/rocketchat-webrtc/client/WebRTCClass.js @@ -0,0 +1,992 @@ +/* globals chrome, ChromeScreenShare */ +class WebRTCTransportClass { + constructor(webrtcInstance) { + this.debug = false; + this.webrtcInstance = webrtcInstance; + this.callbacks = {}; + RocketChat.Notifications.onRoom(this.webrtcInstance.room, 'webrtc', (type, data) => { + const onRemoteStatus = this.callbacks['onRemoteStatus']; + this.log('WebRTCTransportClass - onRoom', type, data); + switch (type) { + case 'status': + if (onRemoteStatus && onRemoteStatus.length) { + onRemoteStatus.forEach(fn => fn(data)); + } + } + }); + } + + log() { + if (this.debug === true) { + console.log.apply(console, arguments); + } + } + + onUserStream(type, data) { + if (data.room !== this.webrtcInstance.room) { + return; + } + this.log('WebRTCTransportClass - onUser', type, data); + const onRemoteCall = this.callbacks['onRemoteCall']; + const onRemoteJoin = this.callbacks['onRemoteJoin']; + const onRemoteCandidate = this.callbacks['onRemoteCandidate']; + const onRemoteDescription = this.callbacks['onRemoteDescription']; + + switch (type) { + case 'call': + if (onRemoteCall && onRemoteCall.length) { + onRemoteCall.forEach(fn => fn(data)); + } + break; + case 'join': + if (onRemoteJoin && onRemoteJoin.length) { + onRemoteJoin.forEach(fn => fn(data)); + } + break; + case 'candidate': + if (onRemoteCandidate && onRemoteCandidate.length) { + onRemoteCandidate.forEach(fn => fn(data)); + } + break; + case 'description': + if (onRemoteDescription && onRemoteDescription.length) { + onRemoteDescription.forEach(fn => fn(data)); + } + } + } + + startCall(data) { + this.log('WebRTCTransportClass - startCall', this.webrtcInstance.room, this.webrtcInstance.selfId); + RocketChat.Notifications.notifyUsersOfRoom(this.webrtcInstance.room, 'webrtc', 'call', { + from: this.webrtcInstance.selfId, + room: this.webrtcInstance.room, + media: data.media, + monitor: data.monitor + }); + } + + joinCall(data) { + this.log('WebRTCTransportClass - joinCall', this.webrtcInstance.room, this.webrtcInstance.selfId); + if (data.monitor === true) { + RocketChat.Notifications.notifyUser(data.to, 'webrtc', 'join', { + from: this.webrtcInstance.selfId, + room: this.webrtcInstance.room, + media: data.media, + monitor: data.monitor + }); + } else { + RocketChat.Notifications.notifyUsersOfRoom(this.webrtcInstance.room, 'webrtc', 'join', { + from: this.webrtcInstance.selfId, + room: this.webrtcInstance.room, + media: data.media, + monitor: data.monitor + }); + } + } + + sendCandidate(data) { + data.from = this.webrtcInstance.selfId; + data.room = this.webrtcInstance.room; + this.log('WebRTCTransportClass - sendCandidate', data); + RocketChat.Notifications.notifyUser(data.to, 'webrtc', 'candidate', data); + } + + sendDescription(data) { + data.from = this.webrtcInstance.selfId; + data.room = this.webrtcInstance.room; + this.log('WebRTCTransportClass - sendDescription', data); + RocketChat.Notifications.notifyUser(data.to, 'webrtc', 'description', data); + } + + sendStatus(data) { + this.log('WebRTCTransportClass - sendStatus', data, this.webrtcInstance.room); + data.from = this.webrtcInstance.selfId; + RocketChat.Notifications.notifyRoom(this.webrtcInstance.room, 'webrtc', 'status', data); + } + + onRemoteCall(fn) { + const callbacks = this.callbacks; + if (callbacks['onRemoteCall'] == null) { + callbacks['onRemoteCall'] = []; + } + callbacks['onRemoteCall'].push(fn); + } + + onRemoteJoin(fn) { + const callbacks = this.callbacks; + if (callbacks['onRemoteJoin'] == null) { + callbacks['onRemoteJoin'] = []; + } + callbacks['onRemoteJoin'].push(fn); + } + + onRemoteCandidate(fn) { + const callbacks = this.callbacks; + if (callbacks['onRemoteCandidate'] == null) { + callbacks['onRemoteCandidate'] = []; + } + callbacks['onRemoteCandidate'].push(fn); + } + + onRemoteDescription(fn) { + const callbacks = this.callbacks; + if (callbacks['onRemoteDescription'] == null) { + callbacks['onRemoteDescription'] = []; + } + callbacks['onRemoteDescription'].push(fn); + } + + onRemoteStatus(fn) { + const callbacks = this.callbacks; + if (callbacks['onRemoteStatus'] == null) { + callbacks['onRemoteStatus'] = []; + } + callbacks['onRemoteStatus'].push(fn); + } + + + +} + +class WebRTCClass { + /* + @param seldId {String} + @param room {String} + */ + + constructor(selfId, room) { + this.config = { + iceServers: [] + }; + this.debug = false; + this.TransportClass = WebRTCTransportClass; + this.selfId = selfId; + this.room = room; + let servers = RocketChat.settings.get('WebRTC_Servers'); + if (servers && servers.trim() !== '') { + servers = servers.replace(/\s/g, ''); + servers = servers.split(','); + + servers.forEach(server => { + server = server.split('@'); + const serverConfig = { + urls: server.pop() + }; + if (server.length === 1) { + server = server[0].split(':'); + serverConfig.username = decodeURIComponent(server[0]); + serverConfig.credential = decodeURIComponent(server[1]); + } + this.config.iceServers.push(serverConfig); + }); + } + this.peerConnections = {}; + this.remoteItems = new ReactiveVar([]); + this.remoteItemsById = new ReactiveVar({}); + this.callInProgress = new ReactiveVar(false); + this.audioEnabled = new ReactiveVar(true); + this.videoEnabled = new ReactiveVar(true); + this.overlayEnabled = new ReactiveVar(false); + this.screenShareEnabled = new ReactiveVar(false); + this.localUrl = new ReactiveVar; + this.active = false; + this.remoteMonitoring = false; + this.monitor = false; + this.autoAccept = false; + this.navigator = undefined; + const userAgent = navigator.userAgent.toLocaleLowerCase(); + + if (userAgent.indexOf('electron') !== -1) { + this.navigator = 'electron'; + } else if (userAgent.indexOf('chrome') !== -1) { + this.navigator = 'chrome'; + } else if (userAgent.indexOf('firefox') !== -1) { + this.navigator = 'firefox'; + } else if (userAgent.indexOf('safari') !== -1) { + this.navigator = 'safari'; + } + const nav = this.navigator; + this.screenShareAvailable = nav === 'chrome' || nav === 'firefox' || nav === 'electron'; + this.media = { + video: false, + audio: true + }; + this.transport = new this.TransportClass(this); + this.transport.onRemoteCall(this.onRemoteCall.bind(this)); + this.transport.onRemoteJoin(this.onRemoteJoin.bind(this)); + this.transport.onRemoteCandidate(this.onRemoteCandidate.bind(this)); + this.transport.onRemoteDescription(this.onRemoteDescription.bind(this)); + this.transport.onRemoteStatus(this.onRemoteStatus.bind(this)); + Meteor.setInterval(this.checkPeerConnections.bind(this), 1000); + + //Meteor.setInterval(this.broadcastStatus.bind(@), 1000); + } + + log() { + if (this.debug === true) { + console.log.apply(console, arguments); + } + } + + onError() { + console.error.apply(console, arguments); + } + + checkPeerConnections() { + const peerConnections = this.peerConnections; + Object.keys(peerConnections).forEach(id => { + const peerConnection = peerConnections[id]; + if (peerConnection.iceConnectionState !== 'connected' && peerConnection.iceConnectionState !== 'completed' && peerConnection.createdAt + 5000 < Date.now()) { + this.stopPeerConnection(id); + } + }); + } + + updateRemoteItems() { + const items = []; + const itemsById = {}; + const peerConnections = this.peerConnections; + + Object.keys(peerConnections).forEach(id => { + const peerConnection = peerConnections[id]; + + peerConnection.getRemoteStreams().forEach(remoteStream => { + const item = { + id, + url: URL.createObjectURL(remoteStream), + state: peerConnection.iceConnectionState + }; + switch (peerConnection.iceConnectionState) { + case 'checking': + item.stateText = 'Connecting...'; + break; + case 'connected': + case 'completed': + item.stateText = 'Connected'; + item.connected = true; + break; + case 'disconnected': + item.stateText = 'Disconnected'; + break; + case 'failed': + item.stateText = 'Failed'; + break; + case 'closed': + item.stateText = 'Closed'; + } + items.push(item); + itemsById[id] = item; + }); + }); + this.remoteItems.set(items); + this.remoteItemsById.set(itemsById); + } + + resetCallInProgress() { + this.callInProgress.set(false); + } + + broadcastStatus() { + if (this.active !== true || this.monitor === true || this.remoteMonitoring === true) { + return; + } + const remoteConnections = []; + const peerConnections = this.peerConnections; + Object.keys(peerConnections).forEach(id => { + const peerConnection = peerConnections[id]; + remoteConnections.push({ + id, + media: peerConnection.remoteMedia + }); + }); + + this.transport.sendStatus({ + media: this.media, + remoteConnections + }); + } + + + /* + @param data {Object} + from {String} + media {Object} + remoteConnections {Array[Object]} + id {String} + media {Object} + */ + + onRemoteStatus(data) { + //this.log(onRemoteStatus, arguments); + this.callInProgress.set(true); + Meteor.clearTimeout(this.callInProgressTimeout); + this.callInProgressTimeout = Meteor.setTimeout(this.resetCallInProgress.bind(this), 2000); + if (this.active !== true) { + return; + } + const remoteConnections = [{ + id: data.from, + media: data.media + }, + ...data.remoteConnections]; + + remoteConnections.forEach(remoteConnection => { + if (remoteConnection.id !== this.selfId && (this.peerConnections[remoteConnection.id] == null)) { + this.log('reconnecting with', remoteConnection.id); + this.onRemoteJoin({ + from: remoteConnection.id, + media: remoteConnection.media + }); + } + }); + } + + + /* + @param id {String} + */ + + getPeerConnection(id) { + if (this.peerConnections[id] != null) { + return this.peerConnections[id]; + } + const peerConnection = new RTCPeerConnection(this.config); + + peerConnection.createdAt = Date.now(); + peerConnection.remoteMedia = {}; + this.peerConnections[id] = peerConnection; + const eventNames = ['icecandidate', 'addstream', 'removestream', 'iceconnectionstatechange', 'datachannel', 'identityresult', 'idpassertionerror', 'idpvalidationerror', 'negotiationneeded', 'peeridentity', 'signalingstatechange']; + + eventNames.forEach(eventName => { + peerConnection.addEventListener(eventName, (e) => { + this.log(id, e.type, e); + }); + }); + + peerConnection.addEventListener('icecandidate', (e) => { + if (e.candidate == null) { + return; + } + this.transport.sendCandidate({ + to: id, + candidate: { + candidate: e.candidate.candidate, + sdpMLineIndex: e.candidate.sdpMLineIndex, + sdpMid: e.candidate.sdpMid + } + }); + }); + peerConnection.addEventListener('addstream', () => { + this.updateRemoteItems(); + }); + peerConnection.addEventListener('removestream', () => { + this.updateRemoteItems(); + }); + peerConnection.addEventListener('iceconnectionstatechange', () => { + if ((peerConnection.iceConnectionState === 'disconnected' || peerConnection.iceConnectionState === 'closed') && peerConnection === this.peerConnections[id]) { + this.stopPeerConnection(id); + Meteor.setTimeout(() => { + if (Object.keys(this.peerConnections).length === 0) { + this.stop(); + } + }, 3000); + } + this.updateRemoteItems(); + }); + return peerConnection; + } + + _getUserMedia(media, onSuccess, onError) { + const onSuccessLocal = function(stream) { + if (AudioContext && stream.getAudioTracks().length > 0) { + const audioContext = new AudioContext; + const source = audioContext.createMediaStreamSource(stream); + const volume = audioContext.createGain(); + source.connect(volume); + const peer = audioContext.createMediaStreamDestination(); + volume.connect(peer); + volume.gain.value = 0.6; + stream.removeTrack(stream.getAudioTracks()[0]); + stream.addTrack(peer.stream.getAudioTracks()[0]); + stream.volume = volume; + this.audioContext = audioContext; + } + onSuccess(stream); + }; + navigator.getUserMedia(media, onSuccessLocal, onError); + } + + getUserMedia(media, onSuccess, onError = this.onError) { + if (media.desktop !== true) { + this._getUserMedia(media, onSuccess, onError); + return; + } + if (this.screenShareAvailable !== true) { + console.log('Screen share is not avaliable'); + return; + } + const getScreen = (audioStream) => { + if (document.cookie.indexOf('rocketchatscreenshare=chrome') === -1 && (window.rocketchatscreenshare == null) && this.navigator !== 'electron') { + const refresh = function() { + swal({ + type: 'warning', + title: TAPi18n.__('Refresh_your_page_after_install_to_enable_screen_sharing') + }); + }; + swal({ + type: 'warning', + title: TAPi18n.__('Screen_Share'), + text: TAPi18n.__('You_need_install_an_extension_to_allow_screen_sharing'), + html: true, + showCancelButton: true, + confirmButtonText: TAPi18n.__('Install_Extension'), + cancelButtonText: TAPi18n.__('Cancel') + }, (isConfirm) => { + if (isConfirm) { + if (this.navigator === 'chrome') { + const url = 'https://chrome.google.com/webstore/detail/rocketchat-screen-share/nocfbnnmjnndkbipkabodnheejiegccf'; + try { + chrome.webstore.install(url, refresh, function() { + window.open(url); + refresh(); + }); + } catch (_error) { + console.log(_error); + } + } else if (this.navigator === 'firefox') { + window.open('https://addons.mozilla.org/en-GB/firefox/addon/rocketchat-screen-share/'); + refresh(); + } + } + }); + return onError(false); + } + const getScreenSuccess = (stream) => { + if (audioStream != null) { + stream.addTrack(audioStream.getAudioTracks()[0]); + } + onSuccess(stream); + }; + if (this.navigator === 'firefox') { + media = { + audio: media.audio, + video: { + mozMediaSource: 'window', + mediaSource: 'window' + } + }; + this._getUserMedia(media, getScreenSuccess, onError); + } else { + ChromeScreenShare.getSourceId(this.navigator, (id) => { + media = { + audio: false, + video: { + mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: id, + maxWidth: 1280, + maxHeight: 720 + } + } + }; + this._getUserMedia(media, getScreenSuccess, onError); + }); + } + }; + if (this.navigator === 'firefox' || (media.audio == null) || media.audio === false) { + getScreen(); + } else { + const getAudioSuccess = (audioStream) => { + getScreen(audioStream); + }; + const getAudioError = () => { + getScreen(); + }; + + this._getUserMedia({ + audio: media.audio + }, getAudioSuccess, getAudioError); + } + } + + + /* + @param callback {Function} + */ + + getLocalUserMedia(callback) { + this.log('getLocalUserMedia', arguments); + if (this.localStream != null) { + return callback(null, this.localStream); + } + const onSuccess = (stream) => { + this.localStream = stream; + this.localUrl.set(URL.createObjectURL(stream)); + this.videoEnabled.set(this.media.video === true); + this.audioEnabled.set(this.media.audio === true); + const peerConnections = this.peerConnections; + Object.keys(peerConnections).forEach(id => { + const peerConnection = peerConnections[id]; + peerConnection.addStream(stream); + }); + callback(null, this.localStream); + }; + const onError = (error) => { + callback(false); + this.onError(error); + }; + this.getUserMedia(this.media, onSuccess, onError); + } + + + /* + @param id {String} + */ + + stopPeerConnection(id) { + const peerConnection = this.peerConnections[id]; + if (peerConnection == null) { + return; + } + delete this.peerConnections[id]; + peerConnection.close(); + this.updateRemoteItems(); + } + + stopAllPeerConnections() { + const peerConnections = this.peerConnections; + + Object.keys(peerConnections).forEach(id => { + this.stopPeerConnection(id); + }); + + window.audioContext && window.audioContext.close(); + } + + setAudioEnabled(enabled = true) { + if (this.localStream != null) { + if (enabled === true && this.media.audio !== true) { + delete this.localStream; + this.media.audio = true; + this.getLocalUserMedia(() => { + this.stopAllPeerConnections(); + this.joinCall(); + }); + } else { + this.localStream.getAudioTracks().forEach(function(audio) { + audio.enabled = enabled; + }); + this.audioEnabled.set(enabled); + } + } + } + + disableAudio() { + this.setAudioEnabled(false); + } + + enableAudio() { + this.setAudioEnabled(true); + } + + setVideoEnabled(enabled = true) { + if (this.localStream != null) { + if (enabled === true && this.media.video !== true) { + delete this.localStream; + this.media.video = true; + this.getLocalUserMedia(() => { + this.stopAllPeerConnections(); + this.joinCall(); + }); + } else { + this.localStream.getVideoTracks().forEach(function(video) { + video.enabled = enabled; + }); + this.videoEnabled.set(enabled); + } + } + } + + disableScreenShare() { + this.setScreenShareEnabled(false); + } + + enableScreenShare() { + this.setScreenShareEnabled(true); + } + + setScreenShareEnabled(enabled = true) { + if (this.localStream != null) { + this.media.desktop = enabled; + delete this.localStream; + this.getLocalUserMedia(err => { + if (err != null) { + return; + } + this.screenShareEnabled.set(enabled); + this.stopAllPeerConnections(); + this.joinCall(); + }); + } + } + + disableVideo() { + this.setVideoEnabled(false); + } + + enableVideo() { + this.setVideoEnabled(true); + } + + stop() { + this.active = false; + this.monitor = false; + this.remoteMonitoring = false; + if (this.localStream != null && typeof this.localStream !== 'undefined') { + this.localStream.getTracks().forEach(track => track.stop()); + } + this.localUrl.set(undefined); + delete this.localStream; + this.stopAllPeerConnections(); + } + + + /* + @param media {Object} + audio {Boolean} + video {Boolean} + */ + + startCall(media = {}) { + this.log('startCall', arguments); + this.media = media; + this.getLocalUserMedia(() => { + this.active = true; + this.transport.startCall({ + media: this.media + }); + }); + } + + startCallAsMonitor(media = {}) { + this.log('startCallAsMonitor', arguments); + this.media = media; + this.active = true; + this.monitor = true; + this.transport.startCall({ + media: this.media, + monitor: true + }); + } + + + /* + @param data {Object} + from {String} + monitor {Boolean} + media {Object} + audio {Boolean} + video {Boolean} + */ + + onRemoteCall(data) { + if (this.autoAccept === true) { + FlowRouter.goToRoomById(data.room); + Meteor.defer(() => { + this.joinCall({ + to: data.from, + monitor: data.monitor, + media: data.media + }); + }); + return; + } + + const user = Meteor.users.findOne(data.from); + let fromUsername = undefined; + if (user && user.username) { + fromUsername = user.username; + } + const subscription = ChatSubscription.findOne({ + rid: data.room + }); + + let icon; + let title; + if (data.monitor === true) { + icon = 'eye'; + title = `Monitor call from ${ fromUsername }`; + } else if (subscription && subscription.t === 'd') { + if (data.media && data.media.video) { + icon = 'videocam'; + title = `Direct video call from ${ fromUsername }`; + } else { + icon = 'phone'; + title = `Direct audio call from ${ fromUsername }`; + } + } else if (data.media && data.media.video) { + icon = 'videocam'; + title = `Group video call from ${ subscription.name }`; + } else { + icon = 'phone'; + title = `Group audio call from ${ subscription.name }`; + } + swal({ + title: `<i class='icon-${ icon } alert-icon success-color'></i>${ title }`, + text: 'Do you want to accept?', + html: true, + showCancelButton: true, + confirmButtonText: 'Yes', + cancelButtonText: 'No' + }, (isConfirm) => { + if (isConfirm) { + FlowRouter.goToRoomById(data.room); + Meteor.defer(() => { + this.joinCall({ + to: data.from, + monitor: data.monitor, + media: data.media + }); + }); + } else { + this.stop(); + } + }); + } + + + /* + @param data {Object} + to {String} + monitor {Boolean} + media {Object} + audio {Boolean} + video {Boolean} + desktop {Boolean} + */ + + joinCall(data = {}) { + if (data.media && data.media.audio) { + this.media.audio = data.media.audio; + } + if (data.media && data.media.video) { + this.media.video = data.media.video; + } + data.media = this.media; + this.log('joinCall', arguments); + this.getLocalUserMedia(() => { + this.remoteMonitoring = data.monitor; + this.active = true; + this.transport.joinCall(data); + }); + } + + + onRemoteJoin(data) { + if (this.active !== true) { + return; + } + this.log('onRemoteJoin', arguments); + let peerConnection = this.getPeerConnection(data.from); + + // needsRefresh = false + // if peerConnection.iceConnectionState isnt 'new' + // needsAudio = data.media.audio is true and peerConnection.remoteMedia.audio isnt true + // needsVideo = data.media.video is true and peerConnection.remoteMedia.video isnt true + // needsRefresh = needsAudio or needsVideo or data.media.desktop isnt peerConnection.remoteMedia.desktop + + // # if peerConnection.signalingState is "have-local-offer" or needsRefresh + + if (peerConnection.signalingState !== 'checking') { + this.stopPeerConnection(data.from); + peerConnection = this.getPeerConnection(data.from); + } + if (peerConnection.iceConnectionState !== 'new') { + return; + } + peerConnection.remoteMedia = data.media; + if (this.localStream) { + peerConnection.addStream(this.localStream); + } + const onOffer = offer => { + const onLocalDescription = () => { + this.transport.sendDescription({ + to: data.from, + type: 'offer', + ts: peerConnection.createdAt, + media: this.media, + description: { + sdp: offer.sdp, + type: offer.type + } + }); + }; + + peerConnection.setLocalDescription(new RTCSessionDescription(offer), onLocalDescription, this.onError); + }; + + if (data.monitor === true) { + peerConnection.createOffer(onOffer, this.onError, { + mandatory: { + OfferToReceiveAudio: data.media.audio, + OfferToReceiveVideo: data.media.video + } + }); + } else { + peerConnection.createOffer(onOffer, this.onError); + } + } + + + + onRemoteOffer(data) { + if (this.active !== true) { + return; + } + + this.log('onRemoteOffer', arguments); + let peerConnection = this.getPeerConnection(data.from); + + if (['have-local-offer', 'stable'].includes(peerConnection.signalingState) && (peerConnection.createdAt < data.ts)) { + this.stopPeerConnection(data.from); + peerConnection = this.getPeerConnection(data.from); + } + + if (peerConnection.iceConnectionState !== 'new') { + return; + } + + peerConnection.setRemoteDescription(new RTCSessionDescription(data.description)); + + try { + if (this.localStream) { + peerConnection.addStream(this.localStream); + } + } catch (error) { + console.log(error); + } + + const onAnswer = answer => { + const onLocalDescription = () => { + this.transport.sendDescription({ + to: data.from, + type: 'answer', + ts: peerConnection.createdAt, + description: { + sdp: answer.sdp, + type: answer.type + } + }); + }; + + peerConnection.setLocalDescription(new RTCSessionDescription(answer), onLocalDescription, this.onError); + }; + + peerConnection.createAnswer(onAnswer, this.onError); + } + + + /* + @param data {Object} + to {String} + from {String} + candidate {RTCIceCandidate JSON encoded} + */ + + onRemoteCandidate(data) { + if (this.active !== true) { + return; + } + if (data.to !== this.selfId) { + return; + } + this.log('onRemoteCandidate', arguments); + const peerConnection = this.getPeerConnection(data.from); + if (peerConnection.iceConnectionState !== 'closed' && peerConnection.iceConnectionState !== 'failed' && peerConnection.iceConnectionState !== 'disconnected' && peerConnection.iceConnectionState !== 'completed') { + peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate)); + } + } + + + /* + @param data {Object} + to {String} + from {String} + type {String} [offer, answer] + description {RTCSessionDescription JSON encoded} + ts {Integer} + media {Object} + audio {Boolean} + video {Boolean} + desktop {Boolean} + */ + + onRemoteDescription(data) { + if (this.active !== true) { + return; + } + if (data.to !== this.selfId) { + return; + } + this.log('onRemoteDescription', arguments); + const peerConnection = this.getPeerConnection(data.from); + if (data.type === 'offer') { + peerConnection.remoteMedia = data.media; + this.onRemoteOffer({ + from: data.from, + ts: data.ts, + description: data.description + }); + } else { + peerConnection.setRemoteDescription(new RTCSessionDescription(data.description)); + } + } + +} + +const WebRTC = new class { + constructor() { + this.instancesByRoomId = {}; + } + + getInstanceByRoomId(roomId) { + const subscription = ChatSubscription.findOne({ rid: roomId }); + if (!subscription) { + return; + } + let enabled = false; + switch (subscription.t) { + case 'd': + enabled = RocketChat.settings.get('WebRTC_Enable_Direct'); + break; + case 'p': + enabled = RocketChat.settings.get('WebRTC_Enable_Private'); + break; + case 'c': + enabled = RocketChat.settings.get('WebRTC_Enable_Channel'); + } + if (enabled === false) { + return; + } + if (this.instancesByRoomId[roomId] == null) { + this.instancesByRoomId[roomId] = new WebRTCClass(Meteor.userId(), roomId); + } + return this.instancesByRoomId[roomId]; + } +}; + +Meteor.startup(function() { + Tracker.autorun(function() { + if (Meteor.userId()) { + RocketChat.Notifications.onUser('webrtc', (type, data) => { + if (data.room == null) { + return; + } + const webrtc = WebRTC.getInstanceByRoomId(data.room); + webrtc.transport.onUserStream(type, data); + }); + } + }); +}); + +export {WebRTC}; diff --git a/packages/rocketchat-webrtc/adapter.js b/packages/rocketchat-webrtc/client/adapter.js similarity index 100% rename from packages/rocketchat-webrtc/adapter.js rename to packages/rocketchat-webrtc/client/adapter.js diff --git a/packages/rocketchat-webrtc/client/screenShare.js b/packages/rocketchat-webrtc/client/screenShare.js new file mode 100644 index 0000000000000000000000000000000000000000..22f9ba2dc951e3b506f2003f104ea44499d67e4a --- /dev/null +++ b/packages/rocketchat-webrtc/client/screenShare.js @@ -0,0 +1,29 @@ +/* globals ChromeScreenShare, fireGlobalEvent */ +this.ChromeScreenShare = { + screenCallback: undefined, + getSourceId(navigator, callback) { + if (callback == null) { + throw '"callback" parameter is mandatory.'; + } + ChromeScreenShare.screenCallback = callback; + if (navigator === 'electron') { + return fireGlobalEvent('get-sourceId', '*'); + } + return window.postMessage('get-sourceId', '*'); + } +}; + +window.addEventListener('message', function(e) { + if (e.origin !== window.location.origin) { + return; + } + if (e.data === 'PermissionDeniedError') { + if (ChromeScreenShare.screenCallback != null) { + return ChromeScreenShare.screenCallback('PermissionDeniedError'); + } + throw new Error('PermissionDeniedError'); + } + if (e.data.sourceId != null) { + return typeof ChromeScreenShare.screenCallback === 'function' && ChromeScreenShare.screenCallback(e.data.sourceId); + } +}); diff --git a/packages/rocketchat-webrtc/package.js b/packages/rocketchat-webrtc/package.js index 53d56d1cde7020237cae4eecaf24cea53909d350..608f3d91768ef973a71a229d4fea6089851e4a18 100644 --- a/packages/rocketchat-webrtc/package.js +++ b/packages/rocketchat-webrtc/package.js @@ -7,16 +7,15 @@ Package.describe({ Package.onUse(function(api) { api.use('rocketchat:lib'); - api.use('coffeescript'); api.use('ecmascript'); api.use('templating', 'client'); + api.mainModule('client/WebRTCClass.js', 'client'); + api.addFiles('client/adapter.js', 'client'); + // api.addFiles('); + api.addFiles('client/screenShare.js', 'client'); - api.addFiles('adapter.js', 'client'); - api.addFiles('WebRTCClass.coffee', 'client'); - api.addFiles('screenShare.coffee', 'client'); + api.addFiles('server/settings.js', 'server'); - api.addFiles('server/settings.coffee', 'server'); - - api.export('WebRTC'); + api.export('WebRTC', 'client'); }); diff --git a/packages/rocketchat-webrtc/screenShare.coffee b/packages/rocketchat-webrtc/screenShare.coffee deleted file mode 100644 index c157e7c045b3eaa5c44e28f8c7541bbf3073e764..0000000000000000000000000000000000000000 --- a/packages/rocketchat-webrtc/screenShare.coffee +++ /dev/null @@ -1,27 +0,0 @@ -@ChromeScreenShare = - screenCallback: undefined - - getSourceId: (navigator, callback) -> - if not callback? then throw '"callback" parameter is mandatory.' - - ChromeScreenShare.screenCallback = callback - - if navigator is 'electron' - fireGlobalEvent('get-sourceId', '*') - else - window.postMessage('get-sourceId', '*') - -window.addEventListener 'message', (e) -> - if e.origin isnt window.location.origin - return - - # "cancel" button was clicked - if e.data is 'PermissionDeniedError' - if ChromeScreenShare.screenCallback? - return ChromeScreenShare.screenCallback('PermissionDeniedError') - else - throw new Error('PermissionDeniedError') - - # extension shared temp sourceId - if e.data.sourceId? - ChromeScreenShare.screenCallback?(e.data.sourceId) diff --git a/packages/rocketchat-webrtc/server/settings.coffee b/packages/rocketchat-webrtc/server/settings.coffee deleted file mode 100644 index 84337ffbc49c9bab26aeaee854b463f412ec6ac7..0000000000000000000000000000000000000000 --- a/packages/rocketchat-webrtc/server/settings.coffee +++ /dev/null @@ -1,5 +0,0 @@ -RocketChat.settings.addGroup 'WebRTC', -> - @add 'WebRTC_Enable_Channel', false, { type: 'boolean', group: 'WebRTC', public: true} - @add 'WebRTC_Enable_Private', true , { type: 'boolean', group: 'WebRTC', public: true} - @add 'WebRTC_Enable_Direct' , true , { type: 'boolean', group: 'WebRTC', public: true} - @add 'WebRTC_Servers', 'stun:stun.l.google.com:19302, stun:23.21.150.121, team%40rocket.chat:demo@turn:numb.viagenie.ca:3478', { type: 'string', group: 'WebRTC', public: true} diff --git a/packages/rocketchat-webrtc/server/settings.js b/packages/rocketchat-webrtc/server/settings.js new file mode 100644 index 0000000000000000000000000000000000000000..c32f3103acaf04b62876fa0d16f37d36e19f9490 --- /dev/null +++ b/packages/rocketchat-webrtc/server/settings.js @@ -0,0 +1,22 @@ +RocketChat.settings.addGroup('WebRTC', function() { + this.add('WebRTC_Enable_Channel', false, { + type: 'boolean', + group: 'WebRTC', + 'public': true + }); + this.add('WebRTC_Enable_Private', true, { + type: 'boolean', + group: 'WebRTC', + 'public': true + }); + this.add('WebRTC_Enable_Direct', true, { + type: 'boolean', + group: 'WebRTC', + 'public': true + }); + return this.add('WebRTC_Servers', 'stun:stun.l.google.com:19302, stun:23.21.150.121, team%40rocket.chat:demo@turn:numb.viagenie.ca:3478', { + type: 'string', + group: 'WebRTC', + 'public': true + }); +}); diff --git a/server/configuration/accounts_meld.js b/server/configuration/accounts_meld.js index 1848635c4a5ea9c1d3633014842e19a1db472bfc..8eee62d4f85589ad7cc5e3826f8555dc01bfafd7 100644 --- a/server/configuration/accounts_meld.js +++ b/server/configuration/accounts_meld.js @@ -18,7 +18,8 @@ Accounts.updateOrCreateUserFromExternalService = function(serviceName, serviceDa if (serviceName === 'meteor-developer') { if (Array.isArray(serviceData.emails)) { - serviceData.email = serviceData.emails.sort(a => a.primary !== true).filter(item => item.verified === true)[0]; + const primaryEmail = serviceData.emails.sort(a => a.primary !== true).filter(item => item.verified === true)[0]; + serviceData.email = primaryEmail && primaryEmail.address; } } diff --git a/server/startup/migrations/v094.js b/server/startup/migrations/v094.js new file mode 100644 index 0000000000000000000000000000000000000000..4cc141cb8d8d81ce17fc5b0cc7ccd12def3a2999 --- /dev/null +++ b/server/startup/migrations/v094.js @@ -0,0 +1,26 @@ +RocketChat.Migrations.add({ + version: 94, + up() { + const query = { + 'emails.address.address': { $exists: true } + }; + + RocketChat.models.Users.find(query, {'emails.address.address': 1}).forEach((user) => { + let emailAddress; + user.emails.some(email => { + if (email.address && email.address.address) { + emailAddress = email.address.address; + return true; + } + }); + RocketChat.models.Users.update({ + _id: user._id, + 'emails.address.address': emailAddress + }, { + $set: { + 'emails.$.address': emailAddress + } + }); + }); + } +}); diff --git a/server/startup/presence.js b/server/startup/presence.js index f8248e9c3d7b77e31db3412a58d37edfe805e538..59d2a3c94a069a488f4b46114cd8752d7d43f21a 100644 --- a/server/startup/presence.js +++ b/server/startup/presence.js @@ -3,11 +3,11 @@ Meteor.startup(function() { const instance = { host: 'localhost', - port: process.env.PORT + port: String(process.env.PORT).trim() }; if (process.env.INSTANCE_IP) { - instance.host = process.env.INSTANCE_IP; + instance.host = String(process.env.INSTANCE_IP).trim(); } InstanceStatus.registerInstance('rocket.chat', instance); diff --git a/server/stream/streamBroadcast.js b/server/stream/streamBroadcast.js index 6de334e34b95565b97435f040812b28d5fcf95fc..321bdc72b54047182e1ebf6afc0b5153083cd401 100644 --- a/server/stream/streamBroadcast.js +++ b/server/stream/streamBroadcast.js @@ -2,6 +2,9 @@ import {DDPCommon} from 'meteor/ddp-common'; +process.env.PORT = String(process.env.PORT).trim(); +process.env.INSTANCE_IP = String(process.env.INSTANCE_IP).trim(); + const connections = {}; this.connections = connections; @@ -257,3 +260,18 @@ function startStreamBroadcast() { Meteor.startup(function() { return startStreamBroadcast(); }); + +Meteor.methods({ + 'instances/get'() { + if (!RocketChat.authz.hasPermission(Meteor.userId(), 'view-statistics')) { + throw new Meteor.Error('error-action-not-allowed', 'List instances is not allowed', { + method: 'instances/get' + }); + } + + return Object.keys(connections).map(address => { + const conn = connections[address]; + return Object.assign({ address, currentStatus: conn._stream.currentStatus }, _.pick(conn, 'instanceRecord', 'broadcastAuth')); + }); + } +}); diff --git a/tests/end-to-end/api/02-channels.js b/tests/end-to-end/api/02-channels.js index b872c27c2d9f55d9975ff9e00fa50e24cc8c7dab..0d3d0643328eea076d91dc00f1b1a28655fa992f 100644 --- a/tests/end-to-end/api/02-channels.js +++ b/tests/end-to-end/api/02-channels.js @@ -320,6 +320,21 @@ describe('channels', function() { .end(done); }); + it('/channels.close', (done) => { + request.post(api('channels.close')) + .set(credentials) + .send({ + roomName: apiPublicChannelName + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', `The channel, ${ apiPublicChannelName }, is already closed to the sender`); + }) + .end(done); + }); + it('/channels.open', (done) => { request.post(api('channels.open')) .set(credentials) @@ -502,4 +517,46 @@ describe('channels', function() { }) .end(done); }); + + describe('/channels.delete', () => { + let testChannel; + it('/channels.create', (done) => { + request.post(api('channels.create')) + .set(credentials) + .send({ + name: `channel.test.${ Date.now() }` + }) + .end((err, res) => { + testChannel = res.body.channel; + done(); + }); + }); + it('/channels.delete', (done) => { + request.post(api('channels.delete')) + .set(credentials) + .send({ + roomName: testChannel.name + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); + it('/channels.info', (done) => { + request.get(api('channels.info')) + .set(credentials) + .query({ + roomId: testChannel._id + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-room-not-found'); + }) + .end(done); + }); + }); }); diff --git a/tests/end-to-end/api/03-groups.js b/tests/end-to-end/api/03-groups.js index 98498ba0844371fd877e71ce7108cdea31c0adee..3323c992585e21ac1e1fb15ebdf1684ed953f335 100644 --- a/tests/end-to-end/api/03-groups.js +++ b/tests/end-to-end/api/03-groups.js @@ -298,6 +298,21 @@ describe('groups', function() { .end(done); }); + it('/groups.close', (done) => { + request.post(api('groups.close')) + .set(credentials) + .send({ + roomName: apiPrivateChannelName + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', `The private group, ${ apiPrivateChannelName }, is already closed to the sender`); + }) + .end(done); + }); + it('/groups.open', (done) => { request.post(api('groups.open')) .set(credentials) @@ -408,4 +423,46 @@ describe('groups', function() { }) .end(done); }); + + describe('/groups.delete', () => { + let testGroup; + it('/groups.create', (done) => { + request.post(api('groups.create')) + .set(credentials) + .send({ + name: `group.test.${ Date.now() }` + }) + .end((err, res) => { + testGroup = res.body.group; + done(); + }); + }); + it('/groups.delete', (done) => { + request.post(api('groups.delete')) + .set(credentials) + .send({ + roomName: testGroup.name + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); + it('/groups.info', (done) => { + request.get(api('groups.info')) + .set(credentials) + .query({ + roomId: testGroup._id + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-room-not-found'); + }) + .end(done); + }); + }); }); diff --git a/tests/end-to-end/ui/09-channel.js b/tests/end-to-end/ui/09-channel.js index 1b7bf7d2870869e3b4e3ceb6cf1424e6a60546a7..2a852d12589c999409113941887ef8d2ce2f8e41 100644 --- a/tests/end-to-end/ui/09-channel.js +++ b/tests/end-to-end/ui/09-channel.js @@ -37,6 +37,7 @@ describe('channel', ()=> { it('should start a direct message with rocket.cat', () => { sideNav.searchChannel('rocket.cat'); + mainContent.channelTitle.waitForVisible(5000); mainContent.channelTitle.getText().should.equal('rocket.cat'); }); }); diff --git a/tests/pageobjects/main-content.page.js b/tests/pageobjects/main-content.page.js index beee3e77ed56e917e3472d0094fc66d37db2b953..0a672a9eabbe75ff15449e125aa9177284f7c88f 100644 --- a/tests/pageobjects/main-content.page.js +++ b/tests/pageobjects/main-content.page.js @@ -70,6 +70,7 @@ class MainContent extends Page { this.setTextToInput(text); this.sendBtn.click(); browser.waitUntil(function() { + browser.waitForVisible('.message:last-child .body', 5000); return browser.getText('.message:last-child .body') === text; }, 2000); } @@ -105,18 +106,21 @@ class MainContent extends Page { waitForLastMessageTextAttachmentEqualsText(text) { browser.waitUntil(function() { + browser.waitForVisible('.message:last-child .attachment-text', 5000); return browser.getText('.message:last-child .attachment-text') === text; }, 2000); } waitForLastMessageEqualsText(text) { browser.waitUntil(function() { + browser.waitForVisible('.message:last-child .body', 5000); return browser.getText('.message:last-child .body') === text; }, 4000); } waitForLastMessageUserEqualsText(text) { browser.waitUntil(function() { + browser.waitForVisible('.message:last-child .user-card-message:nth-of-type(2)', 5000); return browser.getText('.message:last-child .user-card-message:nth-of-type(2)') === text; }, 2000); } diff --git a/tests/pageobjects/side-nav.page.js b/tests/pageobjects/side-nav.page.js index f83e9a693ea73b8d96a88ad620cdb24a1ab19ed8..a7191a4f4c2beb2fc3711d5eeed506ff997d79f3 100644 --- a/tests/pageobjects/side-nav.page.js +++ b/tests/pageobjects/side-nav.page.js @@ -43,6 +43,7 @@ class SideNav extends Page { browser.click(`.rooms-list > .wrapper > ul [title="${ channelName }"]`); this.messageInput.waitForExist(5000); browser.waitUntil(function() { + browser.waitForVisible('.room-title', 5000); return browser.getText('.room-title') === channelName; }, 5000); } @@ -54,6 +55,7 @@ class SideNav extends Page { browser.waitForVisible(`.room-title=${ channelName }`, 10000); browser.click(`.room-title=${ channelName }`); browser.waitUntil(function() { + browser.waitForVisible('.room-title', 5000); return browser.getText('.room-title') === channelName; }, 5000); }