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/.meteor/packages b/.meteor/packages index 45753817ec06de3b628aff1d41a2b2f0c2810adc..9c3ec1031737afe626f26299515963b49da8c251 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -8,15 +8,15 @@ accounts-facebook@1.1.1 accounts-github@1.2.1 accounts-google@1.1.2 accounts-meteor-developer@1.2.1 -accounts-password@1.3.4 +accounts-password@1.3.6 accounts-twitter@1.2.1 blaze-html-templates check@1.2.5 coffeescript@1.11.1_4 ddp-rate-limiter@1.0.7 -ecmascript@0.7.2 +ecmascript@0.7.3 ejson@1.0.13 -email@1.1.18 +email@1.2.1 fastclick@1.0.13 http@1.2.12 jquery@1.11.10 @@ -24,7 +24,7 @@ less@2.7.9 logging@1.1.17 meteor-base@1.0.4 mobile-experience@1.0.4 -mongo@1.1.16 +mongo@1.1.17 random@1.0.10 rate-limit@1.0.8 reactive-dict@1.1.8 @@ -36,7 +36,7 @@ shell-server@0.2.3 spacebars standard-minifier-css@1.3.4 standard-minifier-js@2.0.0 -tracker@1.1.2 +tracker@1.1.3 rocketchat:2fa rocketchat:action-links diff --git a/.meteor/release b/.meteor/release index 605b4e1f03103658e752cf5bad58f52bee90009f..fb6f3bc15e2230094f0495e9d4a67d2bcf537d9d 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@1.4.4.1 +METEOR@1.4.4.2 diff --git a/.meteor/versions b/.meteor/versions index 0801efdbd9c33d5e7b11ee5f56ec7baadc7af609..67e75bb0fc3d0e429392123df1749c372a7a8764 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -1,10 +1,10 @@ -accounts-base@1.2.16 +accounts-base@1.2.17 accounts-facebook@1.1.1 accounts-github@1.2.1 accounts-google@1.1.2 accounts-meteor-developer@1.2.1 accounts-oauth@1.1.15 -accounts-password@1.3.5 +accounts-password@1.3.6 accounts-twitter@1.2.1 aldeed:simple-schema@1.5.3 allow-deny@1.0.5 @@ -81,14 +81,14 @@ meteorhacks:meteorx@1.4.1 meteorspark:util@0.2.0 minifier-css@1.2.16 minifier-js@2.0.0 -minimongo@1.0.21 +minimongo@1.0.23 mizzao:autocomplete@0.5.1 mizzao:timesync@0.3.4 mobile-experience@1.0.4 mobile-status-bar@1.0.14 modules@0.8.2 modules-runtime@0.7.10 -mongo@1.1.16 +mongo@1.1.17 mongo-id@1.0.6 mongo-livedata@1.0.12 mrt:reactive-store@0.0.1 @@ -243,7 +243,7 @@ templating-tools@1.1.2 tmeasday:crypto-base@3.1.2 tmeasday:crypto-md5@3.1.2 todda00:friendly-slugs@0.6.0 -tracker@1.1.2 +tracker@1.1.3 twitter-oauth@1.2.0 ui@1.0.13 underscore@1.0.10 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..7dee556b5b785f8c6745b7bbc5879b696d1080e7 --- /dev/null +++ b/.scripts/set-version.js @@ -0,0 +1,48 @@ +/* eslint object-shorthand: 0, prefer-template: 0 */ + +const path = require('path'); +const fs = require('fs'); +let pkgJson = {}; + +try { + pkgJson = require(path.resolve( + process.cwd(), + './package.json' + )); +} catch (err) { + 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', + './.docker/Dockerfile', + './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'); + }); +}); 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/.snapcraft/README.md b/.snapcraft/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f0197f2dc65bf5e06c9f2b3c30b8790f77bcbb13 --- /dev/null +++ b/.snapcraft/README.md @@ -0,0 +1,61 @@ + + +# rocketchat-server snap for Ubuntu Core (all arch) + +Features: +* bundles ubuntu distribution specific and RC compatible mongodb version +* oplog tailing for mongo by default +* mongodb backup command +* mongodb restore command +* caddy reverse proxy built-in - capable of handling free lestencrypt ssl + +Note: + +Currently, this repository is mirrored on launchpad, and used to build latest ARMHF and i386 snaps. + +You can download recent builds here: +https://code.launchpad.net/~sing-li/+snap/rocketchat-server + +Due an issue with the existing installed base of amd64 users (existing snap always installed mongodb 3.2 [#issue](https://github.com/RocketChat/rocketchat-server-snap/issues/3)), this snap is not currently used for amd64 builds. + +### Test installation + +Download the latest snap file of the corresponding architecture to your Ubuntu Core 16 or 16.04LTS server. + +`sudo snap install ./rocketchat-server-xxxxxxxx.snap --dangerous` + + +### Development or compile your own snap + +Make sure you have `snapcraft` installed. + +``` +git clone https://github.com/RocketChat/rocketchat-server-snap +cd rocketchat-server-snap +snapcraft snap +``` + +### Regression tests (run for amd64, i386 and armhf): +- snapcraft runs properly +- snap installs properly +- all services start automatically +- rc service shows a 5-second restart delay while waiting for mongo + - to test manually, stop rc, stop mongo, start rc, wait 20s or so, start mongo +- rc can be successfully restarted via the "Restart the server" button under Admin > Settings > General +- rc service shows a 5-second delay when restarted via this button +- all commands execute successfully: + - initcaddy + - modify the Caddyfile to test: + - self-signed TLS certificate (use the "tls self_signed" caddy directive) + - changing ports (with and without TLS) + - using IP address (only works without TLS) + - successfully acquiring a Let's Encrypt certificate (requires a registered domain) + - backupdb + - should run only with sudo + - restoredb + - ideally, stop rc service prior to running this (mongo must be running) + - should run only with sudo + - use any file outside of $snap_common (should fail) + - use the file created with backupdb + - use a dummy .tgz file without actual data + - with and without a "parties" directory in the archive diff --git a/.snapcraft/candidate/snapcraft.yaml b/.snapcraft/candidate/snapcraft.yaml index 4bab82b5b24fd5c205ee482beaa49ed82cf61798..567d2e4790021d44cb13228f34fc80831ac0f2d8 100644 --- a/.snapcraft/candidate/snapcraft.yaml +++ b/.snapcraft/candidate/snapcraft.yaml @@ -11,6 +11,7 @@ version: #{RC_VERSION} summary: Rocket.Chat server description: Have your own Slack like online chat, built with Meteor. https://rocket.chat/ confinement: strict +assumes: [snapd2.21] apps: rocketchat-server: command: startRocketChat @@ -24,6 +25,12 @@ apps: command: env LC_ALL=C caddy -conf=$SNAP_DATA/Caddyfile -host=localhost:8080 daemon: simple plugs: [network, network-bind] + mongo: + command: env LC_ALL=C mongo + plugs: [network] + restoredb: + command: env LC_ALL=C restoredb + plugs: [network] backupdb: command: env LC_ALL=c rcbackup plugs: [network] @@ -48,7 +55,7 @@ parts: lib/node_modules: node_modules rocketchat-server: plugin: dump - after: [mongodb] + after: [node] source: https://rocket.chat/releases/release-candidate/download source-type: tar stage-packages: @@ -59,22 +66,15 @@ parts: - .node_version.txt - usr - lib - snap: - - programs - - main.js - - .node_version.txt - - usr - - lib mongodb: - source: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-3.2.7.tgz + build-packages: + - wget + source: ./ + prepare: ./resources/preparemongo plugin: dump stage-packages: - libssl1.0.0 - stage: - - usr - - bin - - lib - snap: + prime: - usr - bin - lib @@ -83,23 +83,20 @@ parts: source: resources/ organize: rcbackup: bin/rcbackup + restoredb: bin/restoredb startmongo: bin/startmongo startRocketChat: bin/startRocketChat initreplset.js: bin/initreplset.js Caddyfile: bin/Caddyfile initcaddy: bin/initcaddy - snap: + prime: - bin caddy: - plugin: go - go-importpath: github.com/mholt/caddy - source: https://github.com/mholt/caddy - source-type: git - source-commit: 53e117802fedd5915eeb32907873d8786a4b2936 - snap: - - bin/caddy - after: [go] - go: - source-tag: go1.8 - stage: - - bin + prepare: ./resources/preparecaddy + plugin: dump + source: ./ + prime: + - bin + organize: + caddy: bin/caddy + after: [mongodb] diff --git a/.snapcraft/edge/snapcraft.yaml b/.snapcraft/edge/snapcraft.yaml index 00a0782a212a01e0b708501e794142c70f3b4001..2934d80e5907cc9e77bac8c132df486c19f8fa7f 100644 --- a/.snapcraft/edge/snapcraft.yaml +++ b/.snapcraft/edge/snapcraft.yaml @@ -11,6 +11,7 @@ version: #{RC_VERSION} summary: Rocket.Chat server description: Have your own Slack like online chat, built with Meteor. https://rocket.chat/ confinement: strict +assumes: [snapd2.21] apps: rocketchat-server: command: startRocketChat @@ -24,6 +25,12 @@ apps: command: env LC_ALL=C caddy -conf=$SNAP_DATA/Caddyfile -host=localhost:8080 daemon: simple plugs: [network, network-bind] + mongo: + command: env LC_ALL=C mongo + plugs: [network] + restoredb: + command: env LC_ALL=C restoredb + plugs: [network] backupdb: command: env LC_ALL=c rcbackup plugs: [network] @@ -48,7 +55,7 @@ parts: lib/node_modules: node_modules rocketchat-server: plugin: dump - after: [mongodb] + after: [node] source: https://rocket.chat/releases/develop/download source-type: tar stage-packages: @@ -59,22 +66,15 @@ parts: - .node_version.txt - usr - lib - snap: - - programs - - main.js - - .node_version.txt - - usr - - lib mongodb: - source: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-3.2.7.tgz + build-packages: + - wget + source: ./ + prepare: ./resources/preparemongo plugin: dump stage-packages: - libssl1.0.0 - stage: - - usr - - bin - - lib - snap: + prime: - usr - bin - lib @@ -83,23 +83,20 @@ parts: source: resources/ organize: rcbackup: bin/rcbackup + restoredb: bin/restoredb startmongo: bin/startmongo startRocketChat: bin/startRocketChat initreplset.js: bin/initreplset.js Caddyfile: bin/Caddyfile initcaddy: bin/initcaddy - snap: + prime: - bin caddy: - plugin: go - go-importpath: github.com/mholt/caddy - source: https://github.com/mholt/caddy - source-type: git - source-commit: 53e117802fedd5915eeb32907873d8786a4b2936 - snap: - - bin/caddy - after: [go] - go: - source-tag: go1.8 - stage: - - bin + prepare: ./resources/preparecaddy + plugin: dump + source: ./ + prime: + - bin + organize: + caddy: bin/caddy + after: [mongodb] diff --git a/.snapcraft/resources/preparecaddy b/.snapcraft/resources/preparecaddy new file mode 100755 index 0000000000000000000000000000000000000000..86d530554c04be8bf519ba3d44d68f0d1d1c60a2 --- /dev/null +++ b/.snapcraft/resources/preparecaddy @@ -0,0 +1,36 @@ +#! /bin/bash + +caddy_bin="caddy" +caddy_dl_ext=".tar.gz" + +# NOTE: `uname -m` is more accurate and universal than `arch` +# See https://en.wikipedia.org/wiki/Uname +unamem="$(uname -m)" +if [[ $unamem == *aarch64* ]]; then + caddy_arch="arm64" +elif [[ $unamem == *64* ]]; then + caddy_arch="amd64" +elif [[ $unamem == *86* ]]; then + caddy_arch="386" +elif [[ $unamem == *armv5* ]]; then + caddy_arch="arm" + caddy_arm="5" +elif [[ $unamem == *armv6l* ]]; then + caddy_arch="arm" + caddy_arm="6" +elif [[ $unamem == *armv7l* ]]; then + caddy_arch="arm" + caddy_arm="7" +else + echo "Aborted, unsupported or unknown architecture: $unamem" + return 2 +fi + +echo "Downloading Caddy for $caddy_os/$caddy_arch$caddy_arm..." +caddy_file="caddy_linux_$caddy_arch${caddy_arm}_custom$caddy_dl_ext" +caddy_url="https://caddyserver.com/download/linux/$caddy_arch$caddy_arm?plugins=$caddy_plugins" +echo "$caddy_url" + +wget --quiet "$caddy_url" -O "$caddy_file" +tar -xzf $caddy_file -C . "$caddy_bin" +chmod +x $caddy_bin diff --git a/.snapcraft/resources/preparemongo b/.snapcraft/resources/preparemongo new file mode 100755 index 0000000000000000000000000000000000000000..332dd7d46847a0742b5f3014967aa904e02dfc21 --- /dev/null +++ b/.snapcraft/resources/preparemongo @@ -0,0 +1,22 @@ +#! /bin/bash + +if [[ $(uname -m) == "x86_64" ]] +then + wget --backups=0 "https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-3.2.7.tgz" + tar -zxf ./mongodb-linux-x86_64-ubuntu1604-3.2.7.tgz --strip-components=1 +else + IFS=" " read -a links <<< $(apt-get -y --print-uris install mongodb | egrep -o "https?://[^']+") + for link in ${links[@]} + do + wget --backups=0 ${link} + done + + IFS=" " read -a deb_pkgs <<< $(ls ./ | egrep "\.deb") + for pkg in ${deb_pkgs[@]} + do + echo "Extracting ${pkg}..." + dpkg-deb -R ${pkg} ./ + done + + mv usr/bin bin +fi diff --git a/.snapcraft/resources/restoredb b/.snapcraft/resources/restoredb new file mode 100755 index 0000000000000000000000000000000000000000..0a204e8377097d3d923a30864233267c77e6ff53 --- /dev/null +++ b/.snapcraft/resources/restoredb @@ -0,0 +1,72 @@ +#! /bin/bash + +if [[ ${EUID} != 0 ]] +then + echo "[-] This task must be run with 'sudo'." + exit +fi + +backup_file=${1} +if [[ ! -f ${backup_file} ]] +then + echo "[-] Usage: snap run rocketchat-server.restoredb ${SNAP_COMMON}/backup_file.tgz" + exit +fi + +cd ${backup_file%/*} +if [[ -z $(pwd | grep "${SNAP_COMMON}") ]] +then + echo "[-] Backup file must be within ${SNAP_COMMON}." + exit +fi + +function ask_backup { + echo -n "\ +*** ATTENTION *** +* Your current database WILL BE DROPPED prior to the restore! +* Would you like to make a backup of the current database before proceeding? +* (y/n/Q)> " + + read choice + [[ "${choice,,}" = n* ]] && return + [[ "${choice,,}" = y* ]] && backupdb && return + exit +} + +function warn { + echo "[!] ${1}" + echo "[*] Check ${restore_dir}/${log_name} for details." +} + +function abort { + echo "[!] ${1}" + echo "[*] Check ${restore_dir}/${log_name} for details." + echo "[-] Restore aborted!" + exit +} + +mongo parties --eval "db.getCollectionNames()" | grep "\[ \]" >> /dev/null || ask_backup +echo "[*] Extracting backup file..." +restore_dir="${SNAP_COMMON}/restore" +log_name="extraction.log" +mkdir -p ${restore_dir} +cd ${restore_dir} +tar --no-same-owner --overwrite -zxvf ${backup_file} &> "${restore_dir}/${log_name}" +[[ $? != 0 ]] && abort "Failed to extract backup files to ${restore_dir}!" +echo "[*] Restoring data..." +data_dir=$(tail "${restore_dir}/${log_name}" | grep parties/. | head -n 1) +[[ -z ${data_dir} ]] && abort "Restore data not found within ${backup_file}! + Please check that your backup file contains the backup data within the \"parties\" directory." +data_dir=$(dirname ${data_dir}) +log_name="mongorestore.log" +mongorestore --db parties --noIndexRestore --drop ${data_dir} &> "${restore_dir}/${log_name}" +[[ $? != 0 ]] && abort "Failed to execute mongorestore from ${data_dir}!" +# If mongorestore.log only has a few lines, it likely didn't find the dump files +log_lines=$(wc -l < "${restore_dir}/${log_name}") +[[ ${log_lines} -lt 24 ]] && warn "Little or no restore data found within ${backup_file}! + Please check that your backup file contains all the backup data within the \"parties\" directory." +echo "[*] Preparing database..." +log_name="mongoprepare.log" +mongo parties --eval "db.repairDatabase()" --verbose &> "${restore_dir}/${log_name}" +[[ $? != 0 ]] && abort "Failed to prepare database for usage!" +echo "[+] Restore completed! Please restart the snap.rocketchat services to verify." diff --git a/.snapcraft/stable/snapcraft.yaml b/.snapcraft/stable/snapcraft.yaml index 9fca9ec62b6fc481bf9e1ab24f5184e2088e227f..e548ef967a894277a1659c51581942ebee2599d3 100644 --- a/.snapcraft/stable/snapcraft.yaml +++ b/.snapcraft/stable/snapcraft.yaml @@ -11,6 +11,7 @@ version: #{RC_VERSION} summary: Rocket.Chat server description: Have your own Slack like online chat, built with Meteor. https://rocket.chat/ confinement: strict +assumes: [snapd2.21] apps: rocketchat-server: command: startRocketChat @@ -24,6 +25,12 @@ apps: command: env LC_ALL=C caddy -conf=$SNAP_DATA/Caddyfile -host=localhost:8080 daemon: simple plugs: [network, network-bind] + mongo: + command: env LC_ALL=C mongo + plugs: [network] + restoredb: + command: env LC_ALL=C restoredb + plugs: [network] backupdb: command: env LC_ALL=c rcbackup plugs: [network] @@ -48,7 +55,7 @@ parts: lib/node_modules: node_modules rocketchat-server: plugin: dump - after: [mongodb] + after: [node] source: https://rocket.chat/releases/latest/download source-type: tar stage-packages: @@ -59,22 +66,15 @@ parts: - .node_version.txt - usr - lib - snap: - - programs - - main.js - - .node_version.txt - - usr - - lib mongodb: - source: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-3.2.7.tgz + build-packages: + - wget + source: ./ + prepare: ./resources/preparemongo plugin: dump stage-packages: - libssl1.0.0 - stage: - - usr - - bin - - lib - snap: + prime: - usr - bin - lib @@ -83,23 +83,20 @@ parts: source: resources/ organize: rcbackup: bin/rcbackup + restoredb: bin/restoredb startmongo: bin/startmongo startRocketChat: bin/startRocketChat initreplset.js: bin/initreplset.js Caddyfile: bin/Caddyfile initcaddy: bin/initcaddy - snap: + prime: - bin caddy: - plugin: go - go-importpath: github.com/mholt/caddy - source: https://github.com/mholt/caddy - source-type: git - source-commit: 53e117802fedd5915eeb32907873d8786a4b2936 - snap: - - bin/caddy - after: [go] - go: - source-tag: go1.8 - stage: - - bin + prepare: ./resources/preparecaddy + plugin: dump + source: ./ + prime: + - bin + organize: + caddy: bin/caddy + after: [mongodb] diff --git a/.travis.yml b/.travis.yml index 09bfc03484ba452e68017397e6886774bbf969d8..3192c2d4b13915c5050fc7386285e1f94fbc226c 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 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/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..ec5c5b29e0b57a955382206d83e65096011af2b2 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 && conventional-changelog --config .github/changelog.js -i HISTORY.md -s" }, "license": "MIT", "repository": { @@ -66,7 +67,7 @@ "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", @@ -74,7 +75,7 @@ "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/rocketchat-api/package.js b/packages/rocketchat-api/package.js index 6918b5f1da8fed725f32153405e772b459cf491d..ef8aaf159327b7b036b6c590911440315fc6646e 100644 --- a/packages/rocketchat-api/package.js +++ b/packages/rocketchat-api/package.js @@ -19,6 +19,7 @@ Package.onUse(function(api) { //Register v1 helpers api.addFiles('server/v1/helpers/getPaginationItems.js', 'server'); api.addFiles('server/v1/helpers/getUserFromParams.js', 'server'); + api.addFiles('server/v1/helpers/isUserFromParams.js', 'server'); api.addFiles('server/v1/helpers/parseJsonQuery.js', 'server'); api.addFiles('server/v1/helpers/getLoggedInUser.js', 'server'); diff --git a/packages/rocketchat-api/server/v1/channels.js b/packages/rocketchat-api/server/v1/channels.js index e145a84f0e1499cafd3da47ffaf589e867d87e61..1de2d82d724b238b812fdbb812f855044ddf9530 100644 --- a/packages/rocketchat-api/server/v1/channels.js +++ b/packages/rocketchat-api/server/v1/channels.js @@ -1,10 +1,15 @@ //Returns the channel IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property -function findChannelById({ roomId, checkedArchived = true }) { - if (!roomId || !roomId.trim()) { - throw new Meteor.Error('error-roomid-param-not-provided', 'The parameter "roomId" is required'); +function findChannelByIdOrName({ roomId, roomName, checkedArchived = true }) { + if ((!roomId || !roomId.trim()) && (!roomName || !roomName.trim())) { + throw new Meteor.Error('error-roomid-param-not-provided', 'The parameter "roomId" or "roomName" is required'); } - const room = RocketChat.models.Rooms.findOneById(roomId, { fields: RocketChat.API.v1.defaultFieldsToExclude }); + 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 (!room || room.t !== 'c') { throw new Meteor.Error('error-room-not-found', `No channel found by the id of: ${ roomId }`); @@ -19,7 +24,7 @@ function findChannelById({ roomId, checkedArchived = true }) { RocketChat.API.v1.addRoute('channels.addAll', { authRequired: true }, { post() { - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); Meteor.runAsUser(this.userId, () => { Meteor.call('addAllUserToRoom', findResult._id, this.bodyParams.activeUsersOnly); @@ -33,7 +38,7 @@ RocketChat.API.v1.addRoute('channels.addAll', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.addModerator', { authRequired: true }, { post() { - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); const user = this.getUserFromParams(); @@ -47,7 +52,7 @@ RocketChat.API.v1.addRoute('channels.addModerator', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.addOwner', { authRequired: true }, { post() { - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); const user = this.getUserFromParams(); @@ -61,7 +66,7 @@ RocketChat.API.v1.addRoute('channels.addOwner', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.archive', { authRequired: true }, { post() { - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); Meteor.runAsUser(this.userId, () => { Meteor.call('archiveRoom', findResult._id); @@ -73,7 +78,7 @@ RocketChat.API.v1.addRoute('channels.archive', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.cleanHistory', { authRequired: true }, { post() { - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); if (!this.bodyParams.latest) { return RocketChat.API.v1.failure('Body parameter "latest" is required.'); @@ -101,7 +106,7 @@ RocketChat.API.v1.addRoute('channels.cleanHistory', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.close', { authRequired: true }, { post() { - const findResult = findChannelById({ roomId: this.bodyParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId, checkedArchived: false }); const sub = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(findResult._id, this.userId); @@ -157,7 +162,7 @@ RocketChat.API.v1.addRoute('channels.create', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.delete', { authRequired: true }, { post() { - const findResult = findChannelById({ roomId: this.bodyParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId, checkedArchived: false }); //The find method returns either with the group or the failur @@ -177,7 +182,7 @@ RocketChat.API.v1.addRoute('channels.getIntegrations', { authRequired: true }, { return RocketChat.API.v1.unauthorized(); } - const findResult = findChannelById({ roomId: this.queryParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ roomId: this.queryParams.roomId, checkedArchived: false }); let includeAllPublicChannels = true; if (typeof this.queryParams.includeAllPublicChannels !== 'undefined') { @@ -217,7 +222,7 @@ RocketChat.API.v1.addRoute('channels.getIntegrations', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.history', { authRequired: true }, { get() { - const findResult = findChannelById({ roomId: this.queryParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ roomId: this.queryParams.roomId, checkedArchived: false }); let latestDate = new Date(); if (this.queryParams.latest) { @@ -257,7 +262,7 @@ RocketChat.API.v1.addRoute('channels.history', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.info', { authRequired: true }, { get() { - const findResult = findChannelById({ roomId: this.queryParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ roomId: this.queryParams.roomId, roomName: this.queryParams.roomName, checkedArchived: false }); return RocketChat.API.v1.success({ channel: RocketChat.models.Rooms.findOneById(findResult._id, { fields: RocketChat.API.v1.defaultFieldsToExclude }) @@ -267,7 +272,7 @@ RocketChat.API.v1.addRoute('channels.info', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.invite', { authRequired: true }, { post() { - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); const user = this.getUserFromParams(); @@ -283,7 +288,7 @@ RocketChat.API.v1.addRoute('channels.invite', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.join', { authRequired: true }, { post() { - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); Meteor.runAsUser(this.userId, () => { Meteor.call('joinRoom', findResult._id, this.bodyParams.joinCode); @@ -297,7 +302,7 @@ RocketChat.API.v1.addRoute('channels.join', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.kick', { authRequired: true }, { post() { - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); const user = this.getUserFromParams(); @@ -313,7 +318,7 @@ RocketChat.API.v1.addRoute('channels.kick', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.leave', { authRequired: true }, { post() { - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); Meteor.runAsUser(this.userId, () => { Meteor.call('leaveRoom', findResult._id); @@ -409,7 +414,7 @@ RocketChat.API.v1.addRoute('channels.online', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.open', { authRequired: true }, { post() { - const findResult = findChannelById({ roomId: this.bodyParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId, checkedArchived: false }); const sub = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(findResult._id, this.userId); @@ -431,7 +436,7 @@ RocketChat.API.v1.addRoute('channels.open', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.removeModerator', { authRequired: true }, { post() { - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); const user = this.getUserFromParams(); @@ -445,7 +450,7 @@ RocketChat.API.v1.addRoute('channels.removeModerator', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.removeOwner', { authRequired: true }, { post() { - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); const user = this.getUserFromParams(); @@ -463,7 +468,7 @@ RocketChat.API.v1.addRoute('channels.rename', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "name" is required'); } - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ 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.'); @@ -485,7 +490,7 @@ RocketChat.API.v1.addRoute('channels.setDescription', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "description" is required'); } - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); if (findResult.description === this.bodyParams.description) { return RocketChat.API.v1.failure('The channel description is the same as what it would be changed to.'); @@ -507,7 +512,7 @@ RocketChat.API.v1.addRoute('channels.setJoinCode', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "joinCode" is required'); } - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); Meteor.runAsUser(this.userId, () => { Meteor.call('saveRoomSettings', findResult._id, 'joinCode', this.bodyParams.joinCode); @@ -525,7 +530,7 @@ RocketChat.API.v1.addRoute('channels.setPurpose', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "purpose" is required'); } - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); 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.'); @@ -547,7 +552,7 @@ RocketChat.API.v1.addRoute('channels.setReadOnly', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "readOnly" is required'); } - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); 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.'); @@ -569,7 +574,7 @@ RocketChat.API.v1.addRoute('channels.setTopic', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "topic" is required'); } - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); if (findResult.topic === this.bodyParams.topic) { return RocketChat.API.v1.failure('The channel topic is the same as what it would be changed to.'); @@ -591,7 +596,7 @@ RocketChat.API.v1.addRoute('channels.setType', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "type" is required'); } - const findResult = findChannelById({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); if (findResult.t === this.bodyParams.type) { return RocketChat.API.v1.failure('The channel type is the same as what it would be changed to.'); @@ -609,7 +614,7 @@ RocketChat.API.v1.addRoute('channels.setType', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.unarchive', { authRequired: true }, { post() { - const findResult = findChannelById({ roomId: this.bodyParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId, 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 f05538c4d6d93ab4cf61f38587b270570dbc0733..10bef449b9c1a51e83f15a76d842dc6f46861ec2 100644 --- a/packages/rocketchat-api/server/v1/groups.js +++ b/packages/rocketchat-api/server/v1/groups.js @@ -1,4 +1,4 @@ -//Returns the private group subscription IF found otherwise it will reutrn the failure of why it didn't. Check the `statusCode` property +//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())) { throw new Meteor.Error('error-roomid-param-not-provided', 'The parameter "roomId" or "roomName" is required'); diff --git a/packages/rocketchat-api/server/v1/helpers/getUserFromParams.js b/packages/rocketchat-api/server/v1/helpers/getUserFromParams.js index 99976b38990e2ba0812b9324c93c1aa81213837b..f3e4c81950b84f8a2f1b5bbb0ff77766a94d6b3e 100644 --- a/packages/rocketchat-api/server/v1/helpers/getUserFromParams.js +++ b/packages/rocketchat-api/server/v1/helpers/getUserFromParams.js @@ -1,33 +1,30 @@ -//Convience method, almost need to turn it into a middleware of sorts +//Convenience method, almost need to turn it into a middleware of sorts RocketChat.API.v1.helperMethods.set('getUserFromParams', function _getUserFromParams() { const doesntExist = { _doesntExist: true }; let user; + let params; 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; - } + params = this.bodyParams; 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; - } + params = this.queryParams; break; } - if (!user) { + 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'); - } 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 new file mode 100644 index 0000000000000000000000000000000000000000..28c8bf4ee5e8c62df0384d17c52940c56f5ddb9e --- /dev/null +++ b/packages/rocketchat-api/server/v1/helpers/isUserFromParams.js @@ -0,0 +1,18 @@ +RocketChat.API.v1.helperMethods.set('isUserFromParams', function _isUserFromParams() { + let params; + + switch (this.request.method) { + case 'POST': + case 'PUT': + params = this.bodyParams; + break; + default: + params = this.queryParams; + break; + } + + 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/users.js b/packages/rocketchat-api/server/v1/users.js index e808a0ff10b6915eca4c53c2636e1456b388b39b..aa66fab610f0714dc65cd1eb7aaa007a5b23b360 100644 --- a/packages/rocketchat-api/server/v1/users.js +++ b/packages/rocketchat-api/server/v1/users.js @@ -67,20 +67,19 @@ RocketChat.API.v1.addRoute('users.getAvatar', { authRequired: false }, { RocketChat.API.v1.addRoute('users.getPresence', { authRequired: true }, { get() { - //BLAHHHHHHHHHH :'( - if ((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 user = this.getUserFromParams(); - + if (this.isUserFromParams()) { + const user = RocketChat.models.Users.findOneById(this.userId); return RocketChat.API.v1.success({ - presence: user.status + presence: user.status, + connectionStatus: user.statusConnection, + lastLogin: user.lastLogin }); } - const user = RocketChat.models.Users.findOneById(this.userId); + const user = this.getUserFromParams(); + return RocketChat.API.v1.success({ - presence: user.status, - connectionStatus: user.statusConnection, - lastLogin: user.lastLogin + presence: user.status }); } }); @@ -185,17 +184,19 @@ RocketChat.API.v1.addRoute('users.resetAvatar', { authRequired: true }, { } }); -//TODO: Make this route work with support for usernames RocketChat.API.v1.addRoute('users.setAvatar', { authRequired: true }, { post() { check(this.bodyParams, { avatarUrl: Match.Maybe(String), userId: Match.Maybe(String) }); - if (typeof this.bodyParams.userId !== 'undefined' && this.userId !== this.bodyParams.userId && !RocketChat.authz.hasPermission(this.userId, 'edit-other-user-info')) { + let user; + if (this.isUserFromParams()) { + user = Meteor.users.findOne(this.userId); + } else if (RocketChat.authz.hasPermission(this.userId, 'edit-other-user-info')) { + user = this.getUserFromParams(); + } else { return RocketChat.API.v1.unauthorized(); } - const user = Meteor.users.findOne(this.bodyParams.userId ? this.bodyParams.userId : this.userId); - if (this.bodyParams.avatarUrl) { RocketChat.setUserAvatar(user, this.bodyParams.avatarUrl, '', 'url'); } else { @@ -248,7 +249,7 @@ RocketChat.API.v1.addRoute('users.update', { authRequired: true }, { const userData = _.extend({ _id: this.bodyParams.userId }, this.bodyParams.data); - RocketChat.saveUser(this.userId, userData); + Meteor.runAsUser(this.userId, () => RocketChat.saveUser(this.userId, userData)); if (this.bodyParams.data.customFields) { RocketChat.saveCustomFields(this.bodyParams.userId, this.bodyParams.data.customFields); @@ -263,3 +264,14 @@ RocketChat.API.v1.addRoute('users.update', { authRequired: true }, { return RocketChat.API.v1.success({ user: RocketChat.models.Users.findOneById(this.bodyParams.userId, { fields: RocketChat.API.v1.defaultFieldsToExclude }) }); } }); + +RocketChat.API.v1.addRoute('users.createToken', { authRequired: true }, { + post() { + const user = this.getUserFromParams(); + let data; + Meteor.runAsUser(this.userId, () => { + data = Meteor.call('createToken', user._id); + }); + return data ? RocketChat.API.v1.success({data}) : RocketChat.API.v1.unauthorized(); + } +}); diff --git a/packages/rocketchat-authorization/server/functions/canAccessRoom.js b/packages/rocketchat-authorization/server/functions/canAccessRoom.js index faf4164698f58ea4dc6a54315244ea84f3656f74..82a6b761c1bbaf9dee841f0f28946da859a5a37f 100644 --- a/packages/rocketchat-authorization/server/functions/canAccessRoom.js +++ b/packages/rocketchat-authorization/server/functions/canAccessRoom.js @@ -2,7 +2,7 @@ RocketChat.authz.roomAccessValidators = [ function(room, user = {}) { if (room.t === 'c') { - if (!user._id && RocketChat.settings.get('Accounts_AllowAnonymousAccess') === true) { + if (!user._id && RocketChat.settings.get('Accounts_AllowAnonymousRead') === true) { return true; } diff --git a/packages/rocketchat-authorization/server/startup.js b/packages/rocketchat-authorization/server/startup.js index cf4ac5c7bd7224f2b9c91d5ab924212a3b86ce32..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'] }, @@ -46,20 +47,21 @@ Meteor.startup(function() { { _id: 'set-moderator', roles : ['admin', 'owner'] }, { _id: 'set-owner', roles : ['admin', 'owner'] }, { _id: 'unarchive-room', roles : ['admin'] }, - { _id: 'view-c-room', roles : ['admin', 'user', 'bot'] }, + { _id: 'view-c-room', roles : ['admin', 'user', 'bot', 'anonymous'] }, + { _id: 'user-generate-access-token', roles : ['admin'] }, { _id: 'view-d-room', roles : ['admin', 'user', 'bot'] }, { _id: 'view-full-other-user-info', roles : ['admin'] }, - { _id: 'view-history', roles : ['admin', 'user'] }, - { _id: 'view-joined-room', roles : ['guest', 'bot'] }, + { _id: 'view-history', roles : ['admin', 'user', 'anonymous'] }, + { _id: 'view-joined-room', roles : ['guest', 'bot', 'anonymous'] }, { _id: 'view-join-code', roles : ['admin'] }, { _id: 'view-logs', roles : ['admin'] }, { _id: 'view-other-user-channels', roles : ['admin'] }, - { _id: 'view-p-room', roles : ['admin', 'user'] }, + { _id: 'view-p-room', roles : ['admin', 'user', 'anonymous'] }, { _id: 'view-privileged-setting', roles : ['admin'] }, { _id: 'view-room-administration', roles : ['admin'] }, { _id: 'view-statistics', roles : ['admin'] }, { _id: 'view-user-administration', roles : ['admin'] }, - { _id: 'preview-c-room', roles : ['admin', 'user'] } + { _id: 'preview-c-room', roles : ['admin', 'user', 'anonymous'] } ]; for (const permission of permissions) { @@ -74,7 +76,8 @@ Meteor.startup(function() { { name: 'owner', scope: 'Subscriptions', description: 'Owner' }, { name: 'user', scope: 'Users', description: '' }, { name: 'bot', scope: 'Users', description: '' }, - { name: 'guest', scope: 'Users', description: '' } + { name: 'guest', scope: 'Users', description: '' }, + { name: 'anonymous', scope: 'Users', description: '' } ]; for (const role of defaultRoles) { diff --git a/packages/rocketchat-channel-settings/client/lib/ChannelSettings.js b/packages/rocketchat-channel-settings/client/lib/ChannelSettings.js index 947c32b10ac997f171e40f693ccf048291688f6d..1b222ab393569b3ab6ad8e491176ea9e4e63468e 100644 --- a/packages/rocketchat-channel-settings/client/lib/ChannelSettings.js +++ b/packages/rocketchat-channel-settings/client/lib/ChannelSettings.js @@ -25,7 +25,7 @@ RocketChat.ChannelSettings = new class { const allOptions = _.toArray(this.options.get()); const allowedOptions = _.compact(_.map(allOptions, function(option) { if (option.validation == null || option.validation()) { - option.data = Object.assign({}, option.data, currentData); + option.data = Object.assign({}, typeof option.data === 'function' ? option.data() : option.data, currentData); return option; } })).filter(function(option) { 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-google-natural-language/.npm/package/npm-shrinkwrap.json b/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json index e89b775dfaf7e205e5ecd26562d14f10bbcb3900..e171621ad42f49f5aeb214b231cd0a9f40a36a58 100644 --- a/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json +++ b/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json @@ -1,8 +1,8 @@ { "dependencies": { "ajv": { - "version": "4.11.7", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.7.tgz", + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", "from": "ajv@>=4.9.1 <5.0.0" }, "ansi-regex": { @@ -46,8 +46,8 @@ "from": "assert-plus@>=0.2.0 <0.3.0" }, "async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.3.0.tgz", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.4.0.tgz", "from": "async@>=2.1.2 <3.0.0" }, "asynckit": { @@ -88,7 +88,7 @@ "brace-expansion": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", - "from": "brace-expansion@>=1.0.0 <2.0.0" + "from": "brace-expansion@>=1.1.7 <2.0.0" }, "buffer-equal-constant-time": { "version": "1.0.1", @@ -233,8 +233,8 @@ "from": "escape-string-regexp@>=1.0.2 <2.0.0" }, "extend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", "from": "extend@>=3.0.0 <4.0.0" }, "extsprintf": { @@ -268,8 +268,8 @@ "from": "generate-object-property@>=1.1.0 <2.0.0" }, "getpass": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "from": "getpass@>=0.1.1 <0.2.0", "dependencies": { "assert-plus": { @@ -322,8 +322,8 @@ "from": "graceful-readlink@>=1.0.0" }, "grpc": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.2.4.tgz", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.3.1.tgz", "from": "grpc@>=1.1.0 <2.0.0", "dependencies": { "node-pre-gyp": { @@ -378,8 +378,8 @@ "from": "npmlog@>=4.0.2 <5.0.0", "dependencies": { "are-we-there-yet": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", "from": "are-we-there-yet@>=1.1.2 <1.2.0", "dependencies": { "delegates": { @@ -390,7 +390,7 @@ "readable-stream": { "version": "2.2.9", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.9.tgz", - "from": "readable-stream@>=2.0.0 <3.0.0||>=1.1.13 <2.0.0", + "from": "readable-stream@>=2.0.6 <3.0.0", "dependencies": { "buffer-shims": { "version": "1.0.0", @@ -437,8 +437,8 @@ "from": "console-control-strings@>=1.1.0 <1.2.0" }, "gauge": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.3.tgz", + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "from": "gauge@>=2.7.1 <2.8.0", "dependencies": { "aproba": { @@ -571,8 +571,8 @@ } }, "extend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", "from": "extend@>=3.0.0 <3.1.0" }, "forever-agent": { @@ -598,8 +598,8 @@ "from": "har-validator@>=4.2.1 <4.3.0", "dependencies": { "ajv": { - "version": "4.11.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.6.tgz", + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", "from": "ajv@>=4.9.1 <5.0.0", "dependencies": { "co": { @@ -693,8 +693,8 @@ } }, "sshpk": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.11.0.tgz", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.0.tgz", "from": "sshpk@>=1.7.0 <2.0.0", "dependencies": { "asn1": { @@ -723,8 +723,8 @@ "from": "ecc-jsbn@>=0.1.1 <0.2.0" }, "getpass": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "from": "getpass@>=0.1.1 <0.2.0" }, "jodid25519": { @@ -939,14 +939,14 @@ "from": "tar-pack@>=3.4.0 <4.0.0", "dependencies": { "debug": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.3.tgz", + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.6.tgz", "from": "debug@>=2.2.0 <3.0.0", "dependencies": { "ms": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", - "from": "ms@0.7.2" + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.3.tgz", + "from": "ms@0.7.3" } } }, @@ -963,7 +963,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "from": "inherits@>=2.0.0 <2.1.0" + "from": "inherits@>=2.0.1 <2.1.0" } } }, @@ -1033,7 +1033,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "from": "inherits@>=2.0.0 <2.1.0" + "from": "inherits@>=2.0.1 <2.1.0" }, "isarray": { "version": "1.0.0", @@ -1139,8 +1139,8 @@ "from": "is-property@>=1.0.0 <2.0.0" }, "is-stream-ended": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.0.tgz", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.3.tgz", "from": "is-stream-ended@>=0.1.0 <0.2.0" }, "is-typedarray": { @@ -1261,8 +1261,8 @@ "from": "mime-types@>=2.1.7 <2.2.0" }, "minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "from": "minimatch@>=3.0.2 <4.0.0" }, "modelo": { @@ -1413,16 +1413,9 @@ "from": "sntp@>=1.0.0 <2.0.0" }, "split-array-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/split-array-stream/-/split-array-stream-1.0.0.tgz", - "from": "split-array-stream@>=1.0.0 <2.0.0", - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "from": "async@>=1.4.0 <2.0.0" - } - } + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/split-array-stream/-/split-array-stream-1.0.3.tgz", + "from": "split-array-stream@>=1.0.0 <2.0.0" }, "sshpk": { "version": "1.13.0", @@ -1437,8 +1430,8 @@ } }, "stream-events": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.1.tgz", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.2.tgz", "from": "stream-events@>=1.0.1 <2.0.0" }, "stream-shift": { @@ -1472,9 +1465,9 @@ "from": "strip-ansi@>=3.0.0 <4.0.0" }, "stubs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-1.1.2.tgz", - "from": "stubs@>=1.1.0 <2.0.0" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "from": "stubs@>=3.0.0 <4.0.0" }, "supports-color": { "version": "2.0.0", diff --git a/packages/rocketchat-i18n/i18n/bg.i18n.json b/packages/rocketchat-i18n/i18n/bg.i18n.json index fbedfc1e680bcccd56ea91e6c9ead847b979af6a..893252288ea96917be8e73ad5559b94db11f58ec 100644 --- a/packages/rocketchat-i18n/i18n/bg.i18n.json +++ b/packages/rocketchat-i18n/i18n/bg.i18n.json @@ -1,6 +1,8 @@ { "#channel": "#канал", "0_Errors_Only": "0 - Само Грешки", + "1_Errors_and_Information": "1 - Грешки и ИнформациÑ", + "2_Erros_Information_and_Debug": "2 - Грешки, Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¸ Дебъг", "403": "Забранен", "@username": "@потребителÑко име", "@username_message": "@потребителÑко име<message>", @@ -23,6 +25,7 @@ "Accounts_OAuth_Facebook": "Влизана Ñ Facebook профил", "Accounts_OAuth_Google": "Влизане Ñ Google профил", "Accounts_RegistrationForm_Public": "ОбщеÑтвен", + "All_channels": "Ð’Ñички канали", "All_messages": "Ð’Ñички ÑъобщениÑ", "and": "и", "Application_Name": "Име на Приложение", @@ -38,6 +41,8 @@ "Busy_male": "Зает", "channel": "канал", "Channel": "Канал", + "Channels": "Канали", + "Channels_list": "СпиÑък на публични канали", "Click_here": "ÐатиÑни тук", "close": "затвори", "Close": "Затвори", @@ -84,6 +89,7 @@ "LDAP_Port": "Порт", "Leave_room": "Излез от ÑтаÑта", "line": "линиÑ", + "List_of_Channels": "СпиÑък Ñ ÐšÐ°Ð½Ð°Ð»Ð¸", "Loading...": "Зареждане...", "Log_Package": "Покажи пакет", "Login": "Влез", @@ -96,6 +102,7 @@ "Message_too_long": "Съобщението е твърде дълго", "Messages": "СъобщениÑ", "minutes": "Минути", + "More_channels": "Още канали", "My_Account": "ÐœÐ¾Ñ ÐŸÑ€Ð¾Ñ„Ð¸Ð»", "n_messages": "%s ÑъобщениÑ", "N_new_messages": "%s нови ÑъобщениÑ", @@ -112,6 +119,7 @@ "Online": "Ðа линиÑ", "Oops!": "УпÑ", "Open": "Отвори", + "Password": "Парола", "People": "Хора", "Please_add_a_comment": "Добави коментар молÑ", "Please_wait": "ÐœÐ¾Ð»Ñ Ð¸Ð·Ñ‡Ð°ÐºÐ°Ð¹Ñ‚Ðµ", @@ -145,6 +153,8 @@ "Site_Name": "Име на Сайта", "Skip": "ПреÑкочи", "Smarsh_MissingEmail_Email": "ЛипÑваща Електрона поща", + "Stats_Total_Channels": "Общо Канали", + "Stats_Total_Messages_Channel": "Общо Ð¡ÑŠÐ¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð² Канали", "Test_Desktop_Notifications": "Изпробвай ИзвеÑÑ‚Ð¸Ñ Ð½Ð° Ñ€Ð°Ð±Ð¾Ñ‚Ð½Ð¸Ñ Ð¿Ð»Ð¾Ñ‚", "Thank_you_exclamation_mark": "БлагодарÑ!", "The_server_will_restart_in_s_seconds": "Сървъра ще бъде реÑтартиран Ñлед %s Ñекунди", diff --git a/packages/rocketchat-i18n/i18n/ca.i18n.json b/packages/rocketchat-i18n/i18n/ca.i18n.json index c80a1e4b61fba829f4297f2e533a21d0d096fbe7..436bcca1e9de01d37326825effe7f30fbfa4fe04 100644 --- a/packages/rocketchat-i18n/i18n/ca.i18n.json +++ b/packages/rocketchat-i18n/i18n/ca.i18n.json @@ -17,6 +17,7 @@ "Accessing_permissions": "L'accés als permisos", "Account_SID": "Compte SID", "Accounts": "Comptes", + "Accounts_AllowAnonymousAccess": "Permet accés anònim", "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 ", @@ -416,6 +417,7 @@ "Desktop_Notifications_Enabled": "Les notificacions d'escriptori estan activades", "Direct_message_someone": "Envia un missatge directe a algú", "Direct_Messages": "Missatges directes", + "Disable_Notifications": "Desactiva notificacions", "Disable_two-factor_authentication": "Desactiva l'autenticació de dos factors", "Display_offline_form": "Mostra el formulari de fora de lÃnia", "Displays_action_text": "Mostra text de l'acció", @@ -644,6 +646,7 @@ "Hide_room": "Oculta sala", "Hide_Room_Warning": "Segur que voleu ocultar la sala \"%s\"?", "Hide_roles": "Amaga rols", + "Hide_Unread_Room_Status": "Amaga l'estat de sales no llegides", "Hide_usernames": "Oculta els noms d'usuari", "Highlights": "Ressalta", "Highlights_How_To": "Per ser notificat quan algú esmenta una paraula o frase, afegeix-la aquÃ. Es poden separar les paraules o frases amb comes. No es distingeix entre majúscules i minúscules.", @@ -1082,6 +1085,7 @@ "Nothing_found": "No s'ha trobat res", "Notification_Duration": "Duració de la notificació", "Notifications": "Notificacions", + "Notifications_Muted_Description": "Si esculls silenciar-ho tot, no veurà s la sala destacada a la llista quan hi hagi nous missatges, excepte si són mencions. Silenciar les notificacions sobreescriurà les opcions de notificació.", "Notify_all_in_this_room": "Notifica a tothom d'aquest canal", "Notify_active_in_this_room": "Notifica als usuaris actius d'aquesta sala", "Num_Agents": "# d'agents", @@ -1224,6 +1228,7 @@ "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", @@ -1547,7 +1552,7 @@ "Unmute_user": "Dóna veu a l'usuari", "Unnamed": "Sense nom", "Unpin_Message": "Desfixa el missatge", - "Unread_Alert": "Alerta de no llegit", + "Unread_Tray_Icon_Alert": "Icona d'alerta de no llegits a la safata", "Unread_Messages": "Missatges no llegits", "Unread_Rooms": "Sales no llegides", "Unread_Rooms_Mode": "Mode de sales no llegides", diff --git a/packages/rocketchat-i18n/i18n/cs.i18n.json b/packages/rocketchat-i18n/i18n/cs.i18n.json index 29faa0d6d50cfa45b2b9d3b5a3d752b3500ba76c..ba2dab094d9aaf1329a074484b881bde67931f71 100644 --- a/packages/rocketchat-i18n/i18n/cs.i18n.json +++ b/packages/rocketchat-i18n/i18n/cs.i18n.json @@ -17,6 +17,7 @@ "Accessing_permissions": "PÅ™Ãstup k oprávnÄ›nÃ", "Account_SID": "SID úÄtu", "Accounts": "ÚÄty", + "Accounts_AllowAnonymousAccess": "Povolit anonymnà pÅ™Ãstup", "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", @@ -62,6 +63,10 @@ "Accounts_OAuth_Custom_Token_Path": "Cesta k tokenu", "Accounts_OAuth_Custom_Token_Sent_Via": "Token odesÃlány pÅ™es", "Accounts_OAuth_Custom_Username_Field": "Pole uživatelské jméno", + "Accounts_OAuth_Drupal": "Povolit Drupal pÅ™ihlášenÃ", + "Accounts_OAuth_Drupal_callback_url": "Drupal oAuth2 URI PÅ™esmÄ›rovánÃ", + "Accounts_OAuth_Drupal_id": "Drupal oAuth2 ID klienta", + "Accounts_OAuth_Drupal_secret": "Drupal oAuth2 Secret klienta", "Accounts_OAuth_Facebook": "Facebook PÅ™ihlášenÃ", "Accounts_OAuth_Facebook_callback_url": "Facebook Callback URL", "Accounts_OAuth_Facebook_id": "Facebook App Id", @@ -170,6 +175,8 @@ "API_CORS_Origin": "CORS Origin", "API_Default_Count": "Výchozà poÄet", "API_Default_Count_Description": "Výchozà poÄet výsledků v REST API pokud nenà zažádáno konkrétnà ÄÃslo", + "API_Drupal_URL": "Drupal URL Serveru", + "API_Drupal_URL_Description": "NapÅ™Ãklad: https://domain.com (bez lomÃtka na konci)", "API_Embed": "Náhled vložených odkazů", "API_Embed_Description": "Zda zobrazit náhled stránky když uživatel poÅ¡le odkaz", "API_EmbedCacheExpirationDays": "PoÄet dnà expirace cache embed", @@ -410,6 +417,7 @@ "Desktop_Notifications_Enabled": "Oznámenà na ploÅ¡e jsou povolena", "Direct_message_someone": "PÅ™Ãmá zpráva nÄ›komu", "Direct_Messages": "PÅ™Ãmé zprávy", + "Disable_Notifications": "Zakázat notifikace", "Disable_two-factor_authentication": "Zakázat dvoufázové ověřenÃ", "Display_offline_form": "Zobrazit offline formulář", "Displays_action_text": "Zobrazuje text akce", @@ -638,6 +646,7 @@ "Hide_room": "Skrýt mÃstnost", "Hide_Room_Warning": "Jste si jisti, že chcete skrýt mÃstnost \"%s\"?", "Hide_roles": "Schovat role", + "Hide_Unread_Room_Status": "Schovat stav nepÅ™eÄtených mÃstnostÃ", "Hide_usernames": "Skrýt uživatelská jména", "Highlights": "KlÃÄová slova", "Highlights_How_To": "Chcete-li být upozornÄ›ni, když nÄ›kdo zmÃnà slovo nebo frázi, pÅ™idejte jej sem. Můžete oddÄ›lit slova nebo fráze Äárkami. Velikost pÃsmen nehraje roli", @@ -730,6 +739,8 @@ "Integration_Retry_Count_Description": "Kolikrát by se integrace mÄ›la znova pokusit volat URL pokud byl prvnà pokus neúspěšný?", "Integration_Retry_Delay": "ÄŒas prodlenà opakovánÃ", "Integration_Retry_Delay_Description": "Jaký algoritmus prodlenà by se mÄ›l použÃt?<code class=\"inline\">10^x</code>, <code class=\"inline\">2^x</code> nebo <code class=\"inline\">x*2</code>", + "Integration_Run_When_Message_Is_Edited": "Spustit pÅ™i editaci", + "Integration_Run_When_Message_Is_Edited_Description": "MÄ›la by být integrace spuÅ¡tÄ›na po editaci zprávy? Pokud volbu vypnete, integrace se bude spouÅ¡tÄ›t pouze pro <strong>nové</strong> zprávy.", "Integration_Word_Trigger_Placement": "PÅ™episovánà slov kdekoliv", "Integration_Word_Trigger_Placement_Description": "MÄ›lo by se vyvolat pokud je slovo umÃstÄ›no jinde než na zaÄátku?", "Integration_updated": "Integrace byla aktualizována", @@ -1074,6 +1085,7 @@ "Nothing_found": "Nic nalezeno", "Notification_Duration": "Délka zobrazenà oznámenÃ", "Notifications": "OznámenÃ", + "Notifications_Muted_Description": "Pokud ztiÅ¡Ãte vÅ¡echno, neuvidÃte zvýraznÄ›né mÃstnosti s novými zprávami, krom zmÃnek. ZtiÅ¡enà notifikacà pÅ™etěžà nastavenà notifikacà v jednotlivých mÃstnostech.", "Notify_all_in_this_room": "Oznámit vÅ¡em v této mÃstnosti", "Notify_active_in_this_room": "Notifikovat aktivnà uživatele v mÃstnosti", "Num_Agents": "# Operátorů", @@ -1216,6 +1228,7 @@ "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", @@ -1539,7 +1552,7 @@ "Unmute_user": "ZruÅ¡it ztlumenà uživatele", "Unnamed": "Nepojmenovaný", "Unpin_Message": "Odepnout Zprávu", - "Unread_Alert": "NepÅ™eÄtené upozornÄ›nÃ", + "Unread_Tray_Icon_Alert": "Ikona v oznamovacà oblasti upozorňuje na nepÅ™eÄtené zprávy", "Unread_Messages": "NepÅ™eÄtÄ›né zprávy", "Unread_Rooms": "NepÅ™eÄtené mÃstnosti", "Unread_Rooms_Mode": "Mód NepÅ™eÄtených mÃstnostÃ", diff --git a/packages/rocketchat-i18n/i18n/de-AT.i18n.json b/packages/rocketchat-i18n/i18n/de-AT.i18n.json index d342f6c1d80602d954a3b2f17b1c1f420b9577a9..95bf6f0116731ae1a8db1a1a98390008a1ecc203 100644 --- a/packages/rocketchat-i18n/i18n/de-AT.i18n.json +++ b/packages/rocketchat-i18n/i18n/de-AT.i18n.json @@ -1127,7 +1127,6 @@ "Unmute_user": "Benutzern das Chatten erlauben ", "Unnamed": "Unbenannt", "Unpin_Message": "Nachicht nicht mehr fixieren", - "Unread_Alert": "Ãœber Ungelesenes benachrichtigen", "Unread_Rooms": "Ungelesene Räume", "Unread_Rooms_Mode": "Ungelesene Räume aufgelistet anzeigen ", "Unstar_Message": "Markierung entfernen", diff --git a/packages/rocketchat-i18n/i18n/de.i18n.json b/packages/rocketchat-i18n/i18n/de.i18n.json index 18f8dd6d08c79bfd911c4854b679ad4f7680ae54..24e7b29e401dcc777e1c19727e05da759fc595ea 100644 --- a/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/packages/rocketchat-i18n/i18n/de.i18n.json @@ -1072,7 +1072,7 @@ "Selected_agents": "Ausgewählte Agenten", "Send": "Senden", "Send_a_message": "Eine Nachricht schicken", - "Send_a_test_mail_to_my_user": "Eine Test-E-Mail an die Benutzer senden", + "Send_a_test_mail_to_my_user": "Eine Test-E-Mail an mich senden", "Send_a_test_push_to_my_user": "Eine Test-Push-Nachricht an mich senden", "Send_confirmation_email": "Bestätigungsmail versenden", "Send_data_into_RocketChat_in_realtime": "Daten in Rocket.Chat in Echtzeit senden.", @@ -1089,7 +1089,7 @@ "Sending": "Senden...", "Service": "Service", "Set_as_moderator": "Zum Moderator ernennen", - "Set_as_owner": "zum Besitzer machen", + "Set_as_owner": "Zum Besitzer machen", "Settings": "Einstellungen", "Settings_updated": "Die Einstellungen wurden aktualisiert.", "Share_Location_Title": "Standort teilen?", @@ -1233,7 +1233,6 @@ "Unmute_user": "Benutzern das Chatten erlauben ", "Unnamed": "Unbenannt", "Unpin_Message": "Nachicht nicht mehr anheften", - "Unread_Alert": "Ãœber Ungelesenes benachrichtigen", "Unread_Rooms": "Ungelesene Räume", "Unread_Rooms_Mode": "Ungelesene Räume aufgelistet anzeigen ", "Unstar_Message": "Markierung entfernen", diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 3c3fef7b1187238dbf4815cfbf3398308cf1fdf8..1d18d31ef92382a2ac30d5dd485734ed7c9739f6 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -17,7 +17,8 @@ "Accessing_permissions": "Accessing permissions", "Account_SID": "Account SID", "Accounts": "Accounts", - "Accounts_AllowAnonymousAccess": "Allow anonymous access", + "Accounts_AllowAnonymousRead": "Allow anonymous read", + "Accounts_AllowAnonymousWrite": "Allow anonymous write", "Accounts_AllowDeleteOwnAccount": "Allow users to delete own account", "Accounts_AllowedDomainsList": "Allowed Domains List", "Accounts_AllowedDomainsList_Description": "Comma-separated list of allowed domains", @@ -35,6 +36,7 @@ "Accounts_BlockedUsernameList": "Blocked Username List", "Accounts_BlockedUsernameList_Description": "Comma-separated list of blocked usernames (case-insensitive)", "Accounts_CustomFields_Description": "Should be a valid JSON where keys are the field names containing a dictionary of field settings. Example:<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": "Default username prefix suggestion", "Accounts_denyUnverifiedEmail": "Deny unverified email", "Accounts_EmailVerification": "Email Verification", "Accounts_EmailVerification_Description": "Make sure you have correct SMTP settings to use this feature", @@ -668,11 +670,11 @@ "Iframe_Integration_receive_enable": "Enable Receive", "Iframe_Integration_receive_enable_Description": "Allow parent window to send commands to Rocket.Chat.", "Iframe_Integration_receive_origin": "Receive origins", - "Iframe_Integration_receive_origin_Description": "Only pages with given origin will be able to send commands or `*` for all origins. You can use multiple values separated by `,`. Example `http://localhost,https://localhost`", + "Iframe_Integration_receive_origin_Description": "Origins with protocol prefix, separated by commas, which are allowed to receive commands e.g. 'https://localhost, http://localhost', or * to allow receiving from anywhere.", "Iframe_Integration_send_enable": "Enable Send", "Iframe_Integration_send_enable_Description": "Send events to parent window", "Iframe_Integration_send_target_origin": "Send target origin", - "Iframe_Integration_send_target_origin_Description": "Only pages with given origin will be able to listen to events or `*` for all origins. Example `http://localhost`", + "Iframe_Integration_send_target_origin_Description": "Origin with protocol prefix, which commands are sent to e.g. 'https://localhost', or * to allow sending to anywhere.", "Importer_Archived": "Archived", "Importer_CSV_Information": "The CSV importer requires a specific format, please read the documentation for how to structure your zip file:", "Importer_HipChatEnterprise_Information": "The file uploaded must be a decrypted tar.gz, please read the documentation for further information:", @@ -1124,6 +1126,7 @@ "or": "or", "Open_your_authentication_app_and_enter_the_code": "Open your authentication app and enter the code. You can also use one of your backup codes.", "Order": "Order", + "Or_talk_as_anonymous": "Or talk as anonymous", "OS_Arch": "OS Arch", "OS_Cpus": "OS CPU Count", "OS_Freemem": "OS Free Memory", @@ -1228,7 +1231,6 @@ "Register": "Register a new account", "Registration": "Registration", "Registration_Succeeded": "Registration Succeeded", - "Register_or_login_to_send_messages": "Register or login to send messages", "Registration_via_Admin": "Registration via Admin", "Regular_Expressions": "Regular Expressions", "Release": "Release", @@ -1363,6 +1365,7 @@ "Showing_archived_results": "<p>Showing <b>%s</b> archived results</p>", "Showing_online_users": "Showing: <b>__total_showing__</b>, Online: __online__, Total: __total__ users", "Showing_results": "<p>Showing <b>%s</b> results</p>", + "Sign_in_to_start_talking": "Sign in to start talking", "since_creation": "since %s", "Site_Name": "Site Name", "Site_Url": "Site URL", @@ -1716,4 +1719,4 @@ "your_message_optional": "your message (optional)", "Your_password_is_wrong": "Your password is wrong!", "Your_push_was_sent_to_s_devices": "Your push was sent to %s devices" -} +} \ No newline at end of file diff --git a/packages/rocketchat-i18n/i18n/es.i18n.json b/packages/rocketchat-i18n/i18n/es.i18n.json index d1af87871a109630a7913ef1bd8ba201295dd6ef..e5dceb0f4260ca4bede8b1b3338d7e963e327b8c 100644 --- a/packages/rocketchat-i18n/i18n/es.i18n.json +++ b/packages/rocketchat-i18n/i18n/es.i18n.json @@ -1227,7 +1227,6 @@ "Unmute_user": "Des-silenciar usuario", "Unnamed": "Sin nombre", "Unpin_Message": "Desfijar Mensaje", - "Unread_Alert": "Alerta de no leÃdos", "Unread_Rooms": "Salas sin leer", "Unread_Rooms_Mode": "Modo Salas sin leer", "Unstar_Message": "Eliminar Destacado", diff --git a/packages/rocketchat-i18n/i18n/fr.i18n.json b/packages/rocketchat-i18n/i18n/fr.i18n.json index 8edb98662179f5a44ac442f75d41b7a144eff9a8..7d9b43e0c9fcb5533db0a5d4bffa9cd8a4ecb787 100644 --- a/packages/rocketchat-i18n/i18n/fr.i18n.json +++ b/packages/rocketchat-i18n/i18n/fr.i18n.json @@ -337,13 +337,13 @@ "Custom": "Personnalisé", "Custom_Emoji": "Emoticône personnalisée", "Custom_Emoji_Add": "Ajouter une nouvelle émoticône", - "Custom_Emoji_Added_Successfully": "Emoji personnalisé ajouté avec succès", - "Custom_Emoji_Delete_Warning": "Effacer un emoji ne peut pas être annulé.", - "Custom_Emoji_Error_Invalid_Emoji": "émoticône non valide", - "Custom_Emoji_Error_Name_Or_Alias_Already_In_Use": "L'émoji personnalisé ou un de ses alias est déjà en cours d'utilisation.", - "Custom_Emoji_Has_Been_Deleted": "L'émoji personnalisé a été effacé.", - "Custom_Emoji_Info": "Information sur l'émoji personnalisé", - "Custom_Emoji_Updated_Successfully": "Emoji personnalisé mis à jour avec succès", + "Custom_Emoji_Added_Successfully": "Emoticône personnalisée ajouté avec succès", + "Custom_Emoji_Delete_Warning": "Effacer une émoticône est irréversible.", + "Custom_Emoji_Error_Invalid_Emoji": "Emoticône non valide", + "Custom_Emoji_Error_Name_Or_Alias_Already_In_Use": "L'émoticône personnalisée ou un de ses alias est déjà en cours d'utilisation.", + "Custom_Emoji_Has_Been_Deleted": "L'émoticône personnalisée a été effacé.", + "Custom_Emoji_Info": "Information sur l'émoticône personnalisée", + "Custom_Emoji_Updated_Successfully": "Emoticône personnalisée mise à jour avec succès", "Custom_Fields": "Champs personnalisés", "Custom_oauth_helper": "Lorsque vous configurez votre service OAuth, vous devez indiquer une URL pour le Callback. Utilisez <pre>%s</pre>", "Custom_oauth_unique_name": "Nom unique de l'OAuth personnalisé", @@ -366,16 +366,16 @@ "Date_From": "De", "Date_to": "à ", "days": "jours", - "DB_Migration": "Base de données de migration", - "DB_Migration_Date": "Base de données de migration date", + "DB_Migration": "Mise à jour de la base de données", + "DB_Migration_Date": "Date de mise à jour de la base de données", "Deactivate": "Désactiver", "Decline": "Refuser", "Default": "Défaut", "Delete": "Supprimer", - "Delete_message": "Suppression de messages", + "Delete_message": "Supprimer le message", "Delete_my_account": "Supprimer mon compte", "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 ne peut être annulée.", + "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é", @@ -393,7 +393,7 @@ "Direct_Messages": "Messages Privés", "Display_offline_form": "Affichage formulaire hors ligne", "Displays_action_text": "Texte d'affichage", - "Do_you_want_to_change_to_s_question": "Voulez-vous changer pour <strong>%s</strong> ?", + "Do_you_want_to_change_to_s_question": "Voulez-vous changer pour <strong>%s</strong> ?", "Domain": "Domaine", "Domain_added": "Domaine ajouté", "Domain_removed": "Domaine supprimé", @@ -404,7 +404,7 @@ "Dry_run_description": "Envoi un unique e-mail, à l'adresse de l'expéditeur. L'adresse e-mail doit appartenir à un utilisateur valide.", "Duplicate_archived_channel_name": "Un canal archivé avec le nom '#%s' existe", "Duplicate_archived_private_group_name": "Un groupe privé archivé avec le nom '%s' existe", - "Duplicate_channel_name": "Un canal avec le nom '% s' existe", + "Duplicate_channel_name": "Un canal avec le nom '%s' existe", "Duplicate_private_group_name": "Un groupe privé avec le nom '%s' existe déjà ", "Duration": "Durée", "Edit": "Modifier", @@ -419,9 +419,9 @@ "Email_already_exists": "L'adresse e-mail existe déjà ", "Email_body": "Corps du message", "Email_Change_Disabled": "Votre administrateur de Rocket.Chat a désactivé le changement d'adresse e-mail", - "Email_Footer_Description": "Vous pouvez utiliser les variables suivantes: <br /><ul><li> [Site_Name] et [Site_URL] pour le nom de l'application et URL respectivement. </li></ul>", + "Email_Footer_Description": "Vous pouvez utiliser les variables suivantes: <br/><ul><li> [Site_Name] et [Site_URL] pour le nom de l'application et URL respectivement. </li></ul>", "Email_from": "De", - "Email_Header_Description": "Vous pouvez utiliser les variables suivantes: <br /><ul><li> [Site_Name] et [Site_URL] pour le nom de l'application et URL respectivement. </li></ul>", + "Email_Header_Description": "Vous pouvez utiliser les variables suivantes: <br/><ul><li> [Site_Name] et [Site_URL] pour le nom de l'application et URL respectivement. </li></ul>", "Email_Notification_Mode": "Notifications hors-ligne par e-mail", "Email_Notification_Mode_All": "Toutes les Mentions/MP", "Email_Notification_Mode_Disabled": "Désactivé", @@ -429,7 +429,7 @@ "Email_subject": "Sujet", "Email_verified": "Adresse e-mail vérifiée", "Emoji": "Emoticône ", - "EmojiCustomFilesystem": "Système de fichier d'émoticones personnalisés", + "EmojiCustomFilesystem": "Système de fichier d'émoticônes personnalisés", "Empty_title": "Titre vide", "Enable": "Activer", "Enable_Desktop_Notifications": "Activer les notifications sur le bureau", @@ -467,7 +467,7 @@ "error-file-too-large": "Le fichier est trop lourd", "error-importer-not-defined": "L'importateur n'a pas été défini correctement, il manque la classe Import.", "error-input-is-not-a-valid-field": "__input__ n'est pas un __field__ valide", - "error-invalid-actionlink": "lien d'action non valide", + "error-invalid-actionlink": "Lien d'action non valide", "error-invalid-arguments": "Arguments non valides", "error-invalid-asset": "Ressource invalide", "error-invalid-channel": "Canal non valide.", @@ -625,7 +625,7 @@ "If_you_are_sure_type_in_your_username": "Si vous êtes certain(e), saisissez votre nom d'utilisateur :", "Iframe_Integration": "Intégration Iframe", "Iframe_Integration_receive_enable": "Activer la réception", - "Iframe_Integration_receive_enable_Description": "Autoriser la fenêtre parente à envoyer des commandes à Rocket.Chat", + "Iframe_Integration_receive_enable_Description": "Autoriser la fenêtre parente à envoyer des commandes à Rocket.Chat.", "Iframe_Integration_receive_origin": "Recevoir les origines", "Iframe_Integration_receive_origin_Description": "Seules les pages ayant une origine donnée seront autorisée à envoyer des commandes ou `*` pour toutes les origines. Vous pouvez utiliser de multiples valeurs séparées par des` ,`. Exemple : `http://localhost,https://localhost`", "Iframe_Integration_send_enable": "Activer l'envoi", @@ -646,11 +646,11 @@ "Importer_Prepare_Restart_Import": "Recommencer l’Importation", "Importer_Prepare_Start_Import": "Commencer l'Importation", "Importer_Prepare_Uncheck_Archived_Channels": "Désélectionner les canaux archivés", - "Importer_Prepare_Uncheck_Deleted_Users": "Désélectionner les Utilisateurs Supprimés", + "Importer_Prepare_Uncheck_Deleted_Users": "Désélectionner les utilisateurs supprimés", "Importer_progress_error": "Impossible d'obtenir l'état de l'importation.", "Importer_setup_error": "Une erreur est survenue lors du paramétrage de l'importateur.", "Importer_Source_File": "Sélection du fichier source", - "Incoming_Livechats": "Arrivée de nouveaux Livechats", + "Incoming_Livechats": "Arrivée de nouveaux chats en direct", "inline_code": "Ligne de code", "Install_Extension": "Installer l'extension", "Install_FxOs": "Installez Rocket.Chat dans votre Firefox", @@ -696,6 +696,7 @@ "Invalid_pass": "Le mot de passe doit être renseigné", "Invalid_room_name": "<strong>%s</strong> n'est pas un nom de salon valide.<br/>Utilisez uniquement des lettres, des chiffres et des tirets (milieu et bas)", "Invalid_secret_URL_message": "L'URL fournie est invalide.", + "Invalid_setting_s": "Paramètre invalide : %s", "invisible": "invisible", "Invisible": "Invisible", "Invitation": "Invitation", @@ -739,11 +740,11 @@ "Jump_to_message": "Aller au message", "Jump_to_recent_messages": "Aller aux messages récents", "Katex_Dollar_Syntax": "Autoriser Dollar Syntaxe", - "Katex_Dollar_Syntax_Description": "Autoriser avec $$ bloc Katex $$ et $ Katex inline $ syntaxes", + "Katex_Dollar_Syntax_Description": "Autoriser les syntaxes : $$bloc katex$$ et $katex inline$", "Katex_Enabled": "Katex activé", "Katex_Enabled_Description": "Autoriser l' utilisation <a target=\"_blank\" href=\"http://khan.github.io/KaTeX/\">Katex</a> pour les mathématiques photocomposition dans les messages", "Katex_Parenthesis_Syntax": "Autoriser Parenthesis Syntaxe", - "Katex_Parenthesis_Syntax_Description": "Autoriser l'utilisation \\ [bloc Katex \\] et \\ (inline Katex \\) syntaxes", + "Katex_Parenthesis_Syntax_Description": "Autoriser les syntaxes : \\[bloc katex\\] et \\(inline katex\\)", "Knowledge_Base": "Base de connaissances", "Label": "Label", "Language": "Langue", @@ -825,7 +826,6 @@ "Domains_allowed_to_embed_the_livechat_widget": "Liste des domaines autorisés à intégrer le widget de chat en direct, séparés par des virgules. Laissez vide pour autoriser tous les domaines. ", "Livechat_Dashboard": "Tableau de bord du chat en direct", "Livechat_enabled": "Chat en direct activé", - "Livechat_forward_open_chats": "chats Forward ouverts", "Livechat_forward_open_chats_timeout": "Timeout (en secondes) de transmettre les chats", "Livechat_guest_count": "Compteur d'invités", "Livechat_Inquiry_Already_Taken": "Demande de chat en direct déjà prise en compte", @@ -906,7 +906,7 @@ "Message_GroupingPeriodDescription": "Les messages seront regroupés avec les messages précédents si ils sont du même utilisateur et si le temps écoulé est inférieur au temps indiqué en secondes.", "Message_HideType_au": "Masquer les messages \"Utilisateur ajouté\"", "Message_HideType_mute_unmute": "Masquer les messages \"Utilisateur rendu muet / a retrouvé la parole\"", - "Message_HideType_ru": "Masquer les messages \"Utilisateur ejecté\"", + "Message_HideType_ru": "Masquer les messages \"Utilisateur éjecté\"", "Message_HideType_uj": "Masquer les messages \"L'utilisateur a rejoint\"", "Message_HideType_ul": "Masquer les messages \"L'utilisateur a quitté\"", "Message_KeepHistory": "Conserver l'historique des messages", @@ -928,7 +928,7 @@ "Message_VideoRecorderEnabled": "Enregistreur vidéo activé", "Message_VideoRecorderEnabledDescription": "Nécessite que le type de média \"video/webm\" soit accepté dans les paramètres d'envoi des fichiers.", "Messages": "Messages", - "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Les messages envoyés au WebHook Entrant seront postés ici", + "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Les messages envoyés au WebHook Entrant seront postés ici.", "Meta": "Meta", "Meta_fb_app_id": "App ID Facebook", "Meta_google-site-verification": "Google Site Verification", @@ -1008,13 +1008,13 @@ "Office_hours_updated": "Heures de bureau modifiées", "Offline": "Hors ligne", "Offline_DM_Email": "Vous avez reçu des messages privés de __user__", - "Offline_form": "forme Hors ligne", - "Offline_form_unavailable_message": "forme Offline Message indisponible", - "Offline_Link_Message": "Aller au message", + "Offline_form": "Formulaire hors ligne", + "Offline_form_unavailable_message": "Message indisponible du formulaire hors ligne", + "Offline_Link_Message": "ALLER AU MESSAGE", "Offline_Mention_Email": "Vous avez été mentionné par __user__ dans le salon #__room__", - "Offline_message": "un message Hors ligne", - "Offline_success_message": "message de succès hors ligne", - "Offline_unavailable": "Offline indisponible", + "Offline_message": "Message hors ligne", + "Offline_success_message": "Message de succès hors ligne", + "Offline_unavailable": "Hors ligne indisponible", "On": "Allumé", "Online": "Connecté", "Only_On_Desktop": "Mode Bureau (envoyé seulement quand Entrée sur le bureau)", @@ -1022,7 +1022,7 @@ "Oops!": "Oups", "Open": "Ouvrerture", "Open_days_of_the_week": "Jours d'ouverture", - "Open_Livechats": "Ouvrir les Livechats", + "Open_Livechats": "Ouvrir les chats en direct", "Opened": "Ouvert", "Opened_in_a_new_window": "Ouvert dans une nouvelle fenêtre.", "Opens_a_channel_group_or_direct_message": "Ouvre un canal, un groupe ou un message direct", @@ -1060,27 +1060,27 @@ "PiwikAnalytics_url_Description": "L'URL où le Piwik réside, assurez-vous d'inclure la barre trialing. Exemple: //piwik.rocket.chat/", "Placeholder_for_email_or_username_login_field": "Texte de remplacement pour l'adresse e-mail ou le nom d'utilisateur à la connexion", "Placeholder_for_password_login_field": "Texte de remplacement pour le mot de passe", - "Please_add_a_comment": "S'il vous plaît ajouter un commentaire", + "Please_add_a_comment": "Merci d'ajouter un commentaire", "Please_add_a_comment_to_close_the_room": "Merci d'ajouter un commentaire pour fermer le salon", - "Please_answer_survey": "S'il vous plait , prenez un moment pour répondre à un court sondage à propos de ce chat ", + "Please_answer_survey": "Merci de prendre un moment pour répondre à un court sondage à propos de cette conversation", "Please_enter_value_for_url": "Veuillez entrer l'url de votre avatar.", "Please_enter_your_new_password_below": "Veuillez écrire votre nouveau mot de passe ci-dessous :", - "Please_enter_your_password": "Veuillez ré-entrer votre mot de passe", + "Please_enter_your_password": "Veuillez entrer votre mot de passe", "please_enter_valid_domain": "Merci d'entrer un domaine valide", "Please_fill_a_label": "Veuillez entrer une étiquette", "Please_fill_a_name": "Veuillez saisir un nom", "Please_fill_a_username": "Veuillez enter un nom d'utilisateur", "Please_fill_name_and_email": "Veuillez remplir le nom et l'adresse e-mail", "Please_select_an_user": "Merci de sélectionner un utilisateur", - "Please_select_enabled_yes_or_no": "Veuillez choisir une option pour Activé", + "Please_select_enabled_yes_or_no": "Veuillez choisir une option pour \"Activé\"", "Please_wait": "Veuillez patienter", "Please_wait_activation": "Veuillez patienter, cela peut prendre un peu de temps.", "Please_wait_while_OTR_is_being_established": "Veuillez patienter pendant l'établissement de l'OTR", - "Please_wait_while_your_account_is_being_deleted": "Veuillez patienter pendant que votre compte est supprimé...", - "Please_wait_while_your_profile_is_being_saved": "Veuillez patienter pendant que votre profil est enregistré ...", + "Please_wait_while_your_account_is_being_deleted": "Veuillez patienter pendant la suppression de votre compte...", + "Please_wait_while_your_profile_is_being_saved": "Veuillez patienter pendant l'enregistrement de votre profil...", "Port": "Port", "Post_as": "Publié en tant que", - "Post_to_Channel": "Publié sur le Canal", + "Post_to_Channel": "Publié sur le canal", "Post_to_s_as_s": "Publié sur <strong>%s</strong> en tant que <strong>%s</strong>", "Preferences": "Préférences", "Preferences_saved": "Préférences enregistrées", @@ -1323,6 +1323,7 @@ "Stats_Total_Messages": "Nombre total de messages", "Stats_Total_Messages_Channel": "Nombre total de messages dans les canaux", "Stats_Total_Messages_Direct": "Nombre total de messages dans les discussions privées", + "Stats_Total_Messages_Livechat": "Nombre total de messages de chat en direct", "Stats_Total_Private_Groups": "Nombre total de groupes privés", "Stats_Total_Rooms": "Nombre total de salons", "Stats_Total_Users": "Nombre total d'utilisateurs", @@ -1379,10 +1380,10 @@ "theme-color-unread-notification-color": "Couleur des notifications non-lues", "theme-custom-css": "CSS personnalisé", "theme-font-body-font-family": "Famille de police du body", - "There_are_no_agents_added_to_this_department_yet": "Il n'y a pas d'assistant ajouté à ce département pour le moment", + "There_are_no_agents_added_to_this_department_yet": "Il n'y a pas d'assistant ajouté à ce département pour le moment.", "There_are_no_integrations": "Il n'y a aucune intégration", "There_are_no_users_in_this_role": "Il n'y a aucun utilisateur avec ce rôle.", - "This_conversation_is_already_closed": "Ces conversation a déjà été fermée.", + "This_conversation_is_already_closed": "Cette conversation a déjà été fermée.", "This_email_has_already_been_used_and_has_not_been_verified__Please_change_your_password": "Cette adresse e-mail a déjà été utilisée et n'a pas été vérifiée. Veuillez changer votre mot de passe.", "This_is_a_desktop_notification": "Ceci est une notification sur le bureau.", "This_is_a_push_test_messsage": "Ceci est une notification de test", @@ -1422,7 +1423,6 @@ "Unmute_user": "Rendre la parole", "Unnamed": "Sans nom", "Unpin_Message": "Désépingler ce message", - "Unread_Alert": "Alerte non lue", "Unread_Messages": "Messages non lus", "Unread_Rooms": "Salons contenant des messages non-lus", "Unread_Rooms_Mode": "Mode des salons non-lus", @@ -1431,10 +1431,10 @@ "Upload_file_name": "Nom du fichier", "Upload_file_question": "Envoyer le fichier ?", "Uploading_file": "Envoi du fichier en cours...", - "Uptime": "uptime", + "Uptime": "Durée de fonctionnement", "URL": "URL", "Use_account_preference": "Préférence du compte utilisateur", - "Use_Emojis": "Utiliser les Emojis", + "Use_Emojis": "Utiliser les émoticônes", "Use_Global_Settings": "Utiliser les paramètres globaux", "Use_initials_avatar": "Utiliser les initiales de votre nom d'utilisateur", "Use_service_avatar": "Utiliser l'avatar %s", @@ -1446,10 +1446,10 @@ "User__username__is_now_a_owner_of__room_name_": "L'utilisateur __username__ est désormais un propriétaire du salon __room_name__", "User__username__removed_from__room_name__moderators": "L'utilisateur __username__ n'est plus modérateur du salon __room_name__.", "User__username__removed_from__room_name__owners": "L'utilisateur __username__ n'est plus propriétaire du salon __room_name__.", - "User_added": "L'utilisateur <em>__user_added__</em> a été ajouté", + "User_added": "Utilisateur ajouté", "User_added_by": "L'utilisateur <em>__user_added__</em> a été ajouté par <em>__user_by__</em>.", "User_added_successfully": "Utilisateur ajouté avec succès", - "User_doesnt_exist": "Aucun utilisateur nommé `@%s` existe.", + "User_doesnt_exist": "Aucun utilisateur nommé `@%s` existant.", "User_has_been_activated": "L'utilisateur a été activé", "User_has_been_deactivated": "L'utilisateur a été désactivé", "User_has_been_deleted": "L'utilisateur a été supprimé", diff --git a/packages/rocketchat-i18n/i18n/he.i18n.json b/packages/rocketchat-i18n/i18n/he.i18n.json index 459b1008f5f8bfc46174c6b47d8b9809a0a9b2e6..b912f90ea58d43bb8bf104d0a775c82f666ca581 100644 --- a/packages/rocketchat-i18n/i18n/he.i18n.json +++ b/packages/rocketchat-i18n/i18n/he.i18n.json @@ -1097,7 +1097,6 @@ "Unmute_user": "בטל השתקת משתמש", "Unnamed": "×œ×œ× ×©×", "Unpin_Message": "שחרור הודעה", - "Unread_Alert": "התרעה על ×œ× × ×§×¨×", "Unread_Rooms": "×—×“×¨×™× ×©×œ× × ×§×¨×ו", "Unread_Rooms_Mode": "מצב ×—×“×¨×™× ×©×œ× × ×§×¨×ו", "Unstar_Message": "הסר ממועדפי×", diff --git a/packages/rocketchat-i18n/i18n/hr.i18n.json b/packages/rocketchat-i18n/i18n/hr.i18n.json index 59f172d3704d1f6be1dceb736f6a44accf1e4b3a..83e9f1a937d0ca6823ce174ed3dd834338c26f59 100644 --- a/packages/rocketchat-i18n/i18n/hr.i18n.json +++ b/packages/rocketchat-i18n/i18n/hr.i18n.json @@ -1292,7 +1292,6 @@ "Unmute_user": "UkljuÄi korisnika", "Unnamed": "Neimenovano", "Unpin_Message": "OtkvaÄi Poruku", - "Unread_Alert": "NeproÄitane obavijesti", "Unread_Rooms": "NeproÄitane Sobe", "Unread_Rooms_Mode": "Mod NeproÄitanih Soba", "Unstar_Message": "Ukloni zvjezdicu", diff --git a/packages/rocketchat-i18n/i18n/it.i18n.json b/packages/rocketchat-i18n/i18n/it.i18n.json index af5735a40eb18d0d381314c282e1cf5fd1aed165..06d62a2f4d7fc6289056fd013ef88e3cb6495c85 100644 --- a/packages/rocketchat-i18n/i18n/it.i18n.json +++ b/packages/rocketchat-i18n/i18n/it.i18n.json @@ -1482,7 +1482,6 @@ "Unmute_user": "Togli il muto all'utente", "Unnamed": "Senza nome", "Unpin_Message": "Rimuovi il pin dal messaggio", - "Unread_Alert": "Avviso Non Letto", "Unread_Rooms": "Stanze non letti", "Unread_Rooms_Mode": "Modalità Stanze Non Letta", "Unstar_Message": "Rimuovi segnalibro", diff --git a/packages/rocketchat-i18n/i18n/ja.i18n.json b/packages/rocketchat-i18n/i18n/ja.i18n.json index 8cc4b4e22f4ef63c61d1718ad5446aeb098db92c..d207cdff4f02c0f98c34490cc3f494e9b4044463 100644 --- a/packages/rocketchat-i18n/i18n/ja.i18n.json +++ b/packages/rocketchat-i18n/i18n/ja.i18n.json @@ -1103,7 +1103,6 @@ "Unmute_user": "ミュートを解除", "Unnamed": "ç„¡å", "Unpin_Message": "ピン留ã‚を外ã™", - "Unread_Alert": "未èªã‚¢ãƒ©ãƒ¼ãƒˆ", "Unread_Rooms": "未èªã®ã‚るルーム", "Unread_Rooms_Mode": "未èªãƒ«ãƒ¼ãƒ 表示モード", "Unstar_Message": "スターを外ã™", diff --git a/packages/rocketchat-i18n/i18n/ko.i18n.json b/packages/rocketchat-i18n/i18n/ko.i18n.json index ca1ae1f28e183a0469a480c2798377f7526ca833..02492e8aa80cd41ca749842fdf2d6630aa591094 100644 --- a/packages/rocketchat-i18n/i18n/ko.i18n.json +++ b/packages/rocketchat-i18n/i18n/ko.i18n.json @@ -16,7 +16,7 @@ "Accessing_permissions": "권한 액세스", "Account_SID": "ê³„ì • SID", "Accounts": "ê³„ì •", - "Accounts_AllowDeleteOwnAccount": "사용ìžê°€ ìžì‹ ì˜ ê³„ì •ì„ ì‚ì œí• ìˆ˜ìžˆìŠµë‹ˆë‹¤.", + "Accounts_AllowDeleteOwnAccount": "사용ìžê°€ ìžì‹ ì˜ ê³„ì •ì„ ì‚ì œí• ìˆ˜ 있습니다.", "Accounts_AllowedDomainsList": "í—ˆìš©ëœ ë„ë©”ì¸ ëª©ë¡", "Accounts_AllowedDomainsList_Description": "í—ˆìš©ëœ ë„ë©”ì¸ì„ 쉼표(,)ë¡œ 구분하기", "Accounts_AllowEmailChange": "ì´ë©”ì¼ ë³€ê²½ì„ í—ˆìš©í•©ë‹ˆë‹¤", @@ -203,6 +203,8 @@ "AutoLinker_Urls_www": "AutoLinker 'WWW'ì˜ URL", "AutoLinker_UrlsRegExp": "AutoLinker URL ì •ê·œ 표현ì‹", "Automatic_Translation": "ìžë™ 번ì—", + "Auto_Translate": "ìžë™ 번ì—", + "AutoTranslate_Enabled": "ìžë™ ë²ˆì— í™œì„±í™”", "AutoTranslate_GoogleAPIKey": "구글 API 키", "Available": "ìœ íš¨í•œ", "Available_agents": "사용 가능한 ì—ì´ì „트", @@ -221,9 +223,12 @@ "Back_to_integrations": "위로 통합ì—", "Back_to_login": "로그ì¸ìœ¼ë¡œ ëŒì•„가기", "Back_to_permissions": "ëŒì•„ 가기 권한", + "Block_User": "ì‚¬ìš©ìž ì°¨ë‹¨", "Body": "ì‹ ì²´", "bold": "굵게", + "BotHelpers_userFields": "ì‚¬ìš©ìž í•„ë“œ", "Branch": "분기", + "Bugsnag_api_key": "Bugsnag API 키", "busy": "ë°”ì¨", "Busy": "ë°”ì¨", "busy_female": "ë°”ì¨", @@ -231,9 +236,14 @@ "busy_male": "ë°”ì¨", "Busy_male": "ë°”ì¨", "by": "으로", + "cache_cleared": "ìºì‹œê°€ ì‚ì œë¨", "Cancel": "취소", "Cancel_message_input": "취소", "Cannot_invite_users_to_direct_rooms": "ê°ì‹¤ì„ 지시하는 사용ìžë¥¼ 초대 í• ìˆ˜ 없습니다", + "CAS_autoclose": "ë¡œê·¸ì¸ íŒì—…ì„ ìžë™ìœ¼ë¡œ ë‹«ìŒ", + "CAS_button_color": "ë¡œê·¸ì¸ ë²„íŠ¼ ë°°ê²½ 색ìƒ", + "CAS_button_label_color": "ë¡œê·¸ì¸ ë²„íŠ¼ í…스트 색ìƒ", + "CAS_button_label_text": "ë¡œê·¸ì¸ ë²„íŠ¼ ë ˆì´ë¸”", "CDN_PREFIX": "CDN Prefix", "Certificates_and_Keys": "ì¸ì¦ì„œì™€ 키", "Changing_email": "변경 ì´ë©”ì¼", diff --git a/packages/rocketchat-i18n/i18n/pl.i18n.json b/packages/rocketchat-i18n/i18n/pl.i18n.json index 85a9d5cfc41444c0819f1c94eee910ffe715e7db..28492fd71c39f769039508c18eef64a56c00c46a 100644 --- a/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -1202,7 +1202,6 @@ "Unmute_user": "Anuluj wyciszenie użytkownika", "Unnamed": "Anonimowy", "Unpin_Message": "Odepnij wiadomość", - "Unread_Alert": "Alarm nieprzeczytany", "Unread_Rooms": "Nieprzeczytane pokoje", "Unread_Rooms_Mode": "Tryb nieprzeczytanych pokoi", "Unstar_Message": "UsuÅ„ oznaczenie", diff --git a/packages/rocketchat-i18n/i18n/sv.i18n.json b/packages/rocketchat-i18n/i18n/sv.i18n.json index 92c5f703e5d6dc7670a27884dd73cc0926c65f2e..b677c0687450cad1c82971067170e86e4d5e398c 100644 --- a/packages/rocketchat-i18n/i18n/sv.i18n.json +++ b/packages/rocketchat-i18n/i18n/sv.i18n.json @@ -421,6 +421,7 @@ "Field": "Fält", "Field_removed": "fältet avlägsnas", "File_exceeds_allowed_size_of_bytes": "Filen överskrider tillÃ¥ten storlek __size__ bytes", + "File_uploaded": "Uppladdad fil", "FileUpload": "Uppladdad fil", "FileUpload_Enabled": "Filuppladdningar aktiverade", "FileUpload_File_Empty": "Tom fil", @@ -506,6 +507,7 @@ "Integration_added": "Integrationen har lagts", "Integration_Incoming_WebHook": "Inkommande WebHook Integration", "Integration_New": "Ny integrering", + "Integrations_Outgoing_Type_FileUploaded": "Uppladdad fil", "Integration_Outgoing_WebHook": "UtgÃ¥ende WebHook Integration", "Integration_updated": "Integrationen har uppdaterats", "Integrations": "Integreringar", @@ -1103,6 +1105,8 @@ "Unread_Rooms": "Olästa rum", "Unread_Rooms_Mode": "Olästa Rum Läge", "Unstar_Message": "Ta bort stjärnmarkering", + "Upload_file_description": "Filbeskrivning", + "Upload_file_name": "Filnamn", "Upload_file_question": "Ladda upp fil?", "Uploading_file": "Laddar upp fil...", "Uptime": "drifttid", diff --git a/packages/rocketchat-i18n/i18n/zh.i18n.json b/packages/rocketchat-i18n/i18n/zh.i18n.json index 10364945aa105887379f839e00182e14ca08ea2b..00b01cda799a2d11ed22421715aaecef70efc298 100644 --- a/packages/rocketchat-i18n/i18n/zh.i18n.json +++ b/packages/rocketchat-i18n/i18n/zh.i18n.json @@ -1341,7 +1341,6 @@ "Unmute_user": "å–消ç¦è¨€", "Unnamed": "未命å", "Unpin_Message": "å–消固定", - "Unread_Alert": "未读è¦æŠ¥", "Unread_Messages": "未读消æ¯", "Unread_Rooms": "未读房间", "Unread_Rooms_Mode": "未读房间模å¼", diff --git a/packages/rocketchat-importer-csv/server.js b/packages/rocketchat-importer-csv/server.js index 183346fb358e74df4b915e55d0378dcb5167f543..f701075e6a6cb9c85d33dd65549c7f3d9886d4cc 100644 --- a/packages/rocketchat-importer-csv/server.js +++ b/packages/rocketchat-importer-csv/server.js @@ -122,9 +122,9 @@ Importer.CSV = class ImporterCSV extends Importer.Base { super.updateRecord({ 'count.messages': messagesCount, 'messagesstatus': null }); super.addCountToTotal(messagesCount); - //Ensure we have some users, channels, and messages - if (tempUsers.length === 0 || tempChannels.length === 0 || messagesCount === 0) { - this.logger.warn(`The loaded users count ${ tempUsers.length }, the loaded channels ${ tempChannels.length }, and the loaded messages ${ messagesCount }`); + //Ensure we have at least a single user, channel, or message + if (tempUsers.length === 0 && tempChannels.length === 0 && messagesCount === 0) { + this.logger.error('No users, channels, or messages found in the import file.'); super.updateProgress(Importer.ProgressStep.ERROR); return super.getProgress(); } diff --git a/packages/rocketchat-katex/katex.coffee b/packages/rocketchat-katex/katex.coffee deleted file mode 100644 index 6e7973f943773960149c0a4a20e70c827ef0db84..0000000000000000000000000000000000000000 --- a/packages/rocketchat-katex/katex.coffee +++ /dev/null @@ -1,181 +0,0 @@ -### -# KaTeX is a fast, easy-to-use JavaScript library for TeX math rendering on the web. -# https://github.com/Khan/KaTeX -### - -katex = require('katex') - -class Katex - constructor: -> - @delimiters_map = [ - { opener: '\\[', closer: '\\]', displayMode: true , enabled: () => @parenthesis_syntax_enabled() }, - { opener: '\\(', closer: '\\)', displayMode: false, enabled: () => @parenthesis_syntax_enabled() }, - { opener: '$$' , closer: '$$' , displayMode: true , enabled: () => @dollar_syntax_enabled() }, - { opener: '$' , closer: '$' , displayMode: false, enabled: () => @dollar_syntax_enabled() }, - ] - - # Searches for the first opening delimiter in the string from a given position - find_opening_delimiter: (str, start) -> # Search the string for each opening delimiter - matches = ({options: o, pos: str.indexOf(o.opener, start)} for o in @delimiters_map when o.enabled()) - positions = (m.pos for m in matches when m.pos >= 0) - - # No opening delimiters were found - if positions.length == 0 - return null - - # Take the first delimiter found - pos = Math.min.apply Math, positions - - match_index = (m.pos for m in matches).indexOf(pos) - match = matches[match_index] - - return match - - class Boundary - length: -> - return @end - @start - - extract: (str) -> - return str.substr @start, @length() - - # Returns the outer and inner boundaries of the latex block starting - # at the given opening delimiter - get_latex_boundaries: (str, opening_delimiter_match) -> - inner = new Boundary - outer = new Boundary - - # The closing delimiter matching to the opening one - closer = opening_delimiter_match.options.closer - - outer.start = opening_delimiter_match.pos - inner.start = opening_delimiter_match.pos + closer.length - - # Search for a closer delimiter after the opening one - closer_index = str.substr(inner.start).indexOf(closer) - if closer_index < 0 - return null - - inner.end = inner.start + closer_index - outer.end = inner.end + closer.length - - return { - outer: outer - inner: inner - } - - # Searches for the first latex block in the given string - find_latex: (str) -> - start = 0 - while (opening_delimiter_match = @find_opening_delimiter str, start++)? - - match = @get_latex_boundaries str, opening_delimiter_match - - if match?.inner.extract(str).trim().length - match.options = opening_delimiter_match.options - return match - - return null - - # Breaks a message to what comes before, after and to the content of a - # matched latex block - extract_latex: (str, match) -> - before = str.substr 0, match.outer.start - after = str.substr match.outer.end - - latex = match.inner.extract str - latex = s.unescapeHTML latex - - return { before: before, latex : latex, after : after } - - # Takes a latex math string and the desired display mode and renders it - # to HTML using the KaTeX library - render_latex: (latex, displayMode) -> - try - rendered = katex.renderToString latex , {displayMode: displayMode} - catch e - display_mode = if displayMode then "block" else "inline" - rendered = "<div class=\"katex-error katex-#{display_mode}-error\">" - rendered += "#{s.escapeHTML e.message}" - rendered += "</div>" - - return rendered - - # Takes a string and renders all latex blocks inside it - render: (str, render_func) -> - result = '' - - loop - - # Find the first latex block in the string - match = @find_latex str - - unless match? - result += str - break - - parts = @extract_latex str, match - - # Add to the reuslt what comes before the latex block as well as - # the rendered latex content - rendered = render_func parts.latex, match.options.displayMode - result += parts.before + rendered - - # Set what comes after the latex block to be examined next - str = parts.after - - return result - - # Takes a rocketchat message and renders latex in its content - render_message: (message) -> - # Render only if enabled in admin panel - if @katex_enabled() - msg = message - - if not _.isString message - if _.trim message.html - msg = message.html - else - return message - - if _.isString message - render_func = (latex, displayMode) => - return @render_latex latex, displayMode - else - message.tokens ?= [] - - render_func = (latex, displayMode) => - token = "=!=#{Random.id()}=!=" - - message.tokens.push - token: token - text: @render_latex latex, displayMode - - return token - - msg = @render msg, render_func - - if not _.isString message - message.html = msg - else - message = msg - - return message - - katex_enabled: -> - return RocketChat.settings.get('Katex_Enabled') - - dollar_syntax_enabled: -> - return RocketChat.settings.get('Katex_Dollar_Syntax') - - parenthesis_syntax_enabled: -> - return RocketChat.settings.get('Katex_Parenthesis_Syntax') - - -RocketChat.katex = new Katex - -cb = RocketChat.katex.render_message.bind(RocketChat.katex) -RocketChat.callbacks.add 'renderMessage', cb, RocketChat.callbacks.priority.HIGH - 1, 'katex' - -if Meteor.isClient - Blaze.registerHelper 'RocketChatKatex', (text) -> - return RocketChat.katex.render_message text diff --git a/packages/rocketchat-katex/katex.js b/packages/rocketchat-katex/katex.js new file mode 100644 index 0000000000000000000000000000000000000000..234165def86daf55c97234f08947d98f6069dc80 --- /dev/null +++ b/packages/rocketchat-katex/katex.js @@ -0,0 +1,255 @@ +/* + * KaTeX is a fast, easy-to-use JavaScript library for TeX math rendering on the web. + * https://github.com/Khan/KaTeX + */ +const katex = require('katex'); + +class Boundary { + constructor() {} + + length() { + return this.end - this.start; + } + + extract(str) { + return str.substr(this.start, this.length()); + } + +} + +class Katex { + constructor() { + this.delimiters_map = [ + { + opener: '\\[', + closer: '\\]', + displayMode: true, + enabled: () => { + return this.parenthesis_syntax_enabled(); + } + }, { + opener: '\\(', + closer: '\\)', + displayMode: false, + enabled: () => { + return this.parenthesis_syntax_enabled(); + } + }, { + opener: '$$', + closer: '$$', + displayMode: true, + enabled: () => { + return this.dollar_syntax_enabled(); + } + }, { + opener: '$', + closer: '$', + displayMode: false, + enabled: () => { + return this.dollar_syntax_enabled(); + } + } + ]; + } + // Searches for the first opening delimiter in the string from a given position + + find_opening_delimiter(str, start) { // Search the string for each opening delimiter + const matches = (() => { + const map = this.delimiters_map; + const results = []; + + map.forEach((op) => { + if (op.enabled()) { + results.push({ + options: op, + pos: str.indexOf(op.opener, start) + }); + } + }); + return results; + })(); + + const positions = (() => { + const results = []; + matches.forEach((pos) => { + if (pos.pos >= 0) { + results.push(pos.pos); + } + }); + return results; + })(); + + // No opening delimiters were found + if (positions.length === 0) { + return null; + } + + //Take the first delimiter found + const pos = Math.min.apply(Math, positions); + + const match_index = (()=> { + const results = []; + matches.forEach((m) => { + results.push(m.pos); + }); + return results; + })().indexOf(pos); + + const match = matches[match_index]; + return match; + } + + // Returns the outer and inner boundaries of the latex block starting + // at the given opening delimiter + get_latex_boundaries(str, opening_delimiter_match) { + const inner = new Boundary; + const outer = new Boundary; + + // The closing delimiter matching to the opening one + const closer = opening_delimiter_match.options.closer; + outer.start = opening_delimiter_match.pos; + inner.start = opening_delimiter_match.pos + closer.length; + + // Search for a closer delimiter after the opening one + const closer_index = str.substr(inner.start).indexOf(closer); + if (closer_index < 0) { + return null; + } + inner.end = inner.start + closer_index; + outer.end = inner.end + closer.length; + return { + outer, + inner + }; + } + + // Searches for the first latex block in the given string + find_latex(str) { + let start = 0; + let opening_delimiter_match; + + while ((opening_delimiter_match = this.find_opening_delimiter(str, start++)) != null) { + const match = this.get_latex_boundaries(str, opening_delimiter_match); + if (match && match.inner.extract(str).trim().length) { + match.options = opening_delimiter_match.options; + return match; + } + } + return null; + } + + // Breaks a message to what comes before, after and to the content of a + // matched latex block + extract_latex(str, match) { + const before = str.substr(0, match.outer.start); + const after = str.substr(match.outer.end); + let latex = match.inner.extract(str); + latex = s.unescapeHTML(latex); + return { + before, + latex, + after + }; + } + + // Takes a latex math string and the desired display mode and renders it + // to HTML using the KaTeX library + render_latex(latex, displayMode) { + let rendered; + try { + rendered = katex.renderToString(latex, { + displayMode + }); + } catch (error) { + const e = error; + const display_mode = displayMode ? 'block' : 'inline'; + rendered = `<div class="katex-error katex-${ display_mode }-error">`; + rendered += `${ s.escapeHTML(e.message) }`; + rendered += '</div>'; + } + return rendered; + } + + // Takes a string and renders all latex blocks inside it + render(str, render_func) { + let result = ''; + while (this.find_latex(str) != null) { + // Find the first latex block in the string + const match = this.find_latex(str); + const parts = this.extract_latex(str, match); + + // Add to the reuslt what comes before the latex block as well as + // the rendered latex content + const rendered = render_func(parts.latex, match.options.displayMode); + result += parts.before + rendered; + // Set what comes after the latex block to be examined next + str = parts.after; + } + return result += str; + } + + // Takes a rocketchat message and renders latex in its content + render_message(message) { + //Render only if enabled in admin panel + let render_func; + if (this.katex_enabled()) { + let msg = message; + if (!_.isString(message)) { + if (_.trim(message.html)) { + msg = message.html; + } else { + return message; + } + } + if (_.isString(message)) { + render_func = (latex, displayMode) => { + return this.render_latex(latex, displayMode); + }; + } else { + if (message.tokens == null) { + message.tokens = []; + } + render_func = (latex, displayMode) => { + const token = `=!=${ Random.id() }=!=`; + message.tokens.push({ + token, + text: this.render_latex(latex, displayMode) + }); + return token; + }; + } + msg = this.render(msg, render_func); + if (!_.isString(message)) { + message.html = msg; + } else { + message = msg; + } + } + return message; + } + + katex_enabled() { + return RocketChat.settings.get('Katex_Enabled'); + } + + dollar_syntax_enabled() { + return RocketChat.settings.get('Katex_Dollar_Syntax'); + } + + parenthesis_syntax_enabled() { + return RocketChat.settings.get('Katex_Parenthesis_Syntax'); + } + +} + +RocketChat.katex = new Katex; + +const cb = RocketChat.katex.render_message.bind(RocketChat.katex); + +RocketChat.callbacks.add('renderMessage', cb, RocketChat.callbacks.priority.HIGH - 1, 'katex'); + +if (Meteor.isClient) { + Blaze.registerHelper('RocketChatKatex', function(text) { + return RocketChat.katex.render_message(text); + }); +} diff --git a/packages/rocketchat-katex/package.js b/packages/rocketchat-katex/package.js index 033ea7efeeea8f5826e53cb2e60400781c2abb08..de08f7f50f5de94f95204cc3e3787300d20cc012 100644 --- a/packages/rocketchat-katex/package.js +++ b/packages/rocketchat-katex/package.js @@ -6,15 +6,14 @@ Package.describe({ }); Package.onUse(function(api) { - api.use('coffeescript'); api.use('ecmascript'); api.use('underscore'); api.use('templating'); api.use('underscorestring:underscore.string'); api.use('rocketchat:lib'); - api.addFiles('settings.coffee', 'server'); - api.addFiles('katex.coffee'); + api.addFiles('settings.js', 'server'); + api.addFiles('katex.js'); api.addFiles('client/style.css', 'client'); const katexPath = 'node_modules/katex/dist/'; diff --git a/packages/rocketchat-katex/settings.coffee b/packages/rocketchat-katex/settings.coffee deleted file mode 100644 index 255aa3dc9c235f0b212def1bf3a0192e1d6c47fc..0000000000000000000000000000000000000000 --- a/packages/rocketchat-katex/settings.coffee +++ /dev/null @@ -1,6 +0,0 @@ -Meteor.startup -> - enableQuery = {_id: 'Katex_Enabled', value: true} - RocketChat.settings.add 'Katex_Enabled', true, {type: 'boolean', group: 'Message', section: 'Katex', public: true, i18n: 'Katex_Enabled_Description'} - - RocketChat.settings.add 'Katex_Parenthesis_Syntax', true, {type: 'boolean', group: 'Message', section: 'Katex', public: true, enableQuery, i18nDescription: 'Katex_Parenthesis_Syntax_Description'} - RocketChat.settings.add 'Katex_Dollar_Syntax', false, {type: 'boolean', group: 'Message', section: 'Katex', public: true, enableQuery, i18nDescription: 'Katex_Dollar_Syntax_Description'} diff --git a/packages/rocketchat-katex/settings.js b/packages/rocketchat-katex/settings.js new file mode 100644 index 0000000000000000000000000000000000000000..006ef790e64b719e69b51d85ab0bd32b3a687a68 --- /dev/null +++ b/packages/rocketchat-katex/settings.js @@ -0,0 +1,32 @@ +Meteor.startup(function() { + const enableQuery = { + _id: 'Katex_Enabled', + value: true + }; + RocketChat.settings.add('Katex_Enabled', true, { + type: 'boolean', + group: 'Message', + section: 'Katex', + 'public': true, + i18n: 'Katex_Enabled_Description' + }); + RocketChat.settings.add('Katex_Parenthesis_Syntax', true, { + type: 'boolean', + group: 'Message', + section: 'Katex', + 'public': true, + enableQuery, + i18nDescription: 'Katex_Parenthesis_Syntax_Description' + }); + return RocketChat.settings.add('Katex_Dollar_Syntax', false, { + type: 'boolean', + group: 'Message', + section: 'Katex', + 'public': true, + enableQuery, + i18nDescription: 'Katex_Dollar_Syntax_Description' + }); +}); + +// --- +// generated by coffee-script 1.9.2 diff --git a/packages/rocketchat-lib/client/MessageAction.coffee b/packages/rocketchat-lib/client/MessageAction.coffee deleted file mode 100644 index 80837c3a0bebe9e4dd587f2dcc35e2d4d9b1251c..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/client/MessageAction.coffee +++ /dev/null @@ -1,247 +0,0 @@ -import moment from 'moment' -import toastr from 'toastr' - -RocketChat.MessageAction = new class - buttons = new ReactiveVar {} - - ### - config expects the following keys (only id is mandatory): - id (mandatory) - icon: string - i18nLabel: string - action: function(event, instance) - validation: function(message) - order: integer - ### - addButton = (config) -> - unless config?.id - return false - - Tracker.nonreactive -> - btns = buttons.get() - btns[config.id] = config - buttons.set btns - - removeButton = (id) -> - Tracker.nonreactive -> - btns = buttons.get() - delete btns[id] - buttons.set btns - - updateButton = (id, config) -> - Tracker.nonreactive -> - btns = buttons.get() - if btns[id] - btns[id] = _.extend btns[id], config - buttons.set btns - - getButtonById = (id) -> - allButtons = buttons.get() - return allButtons[id] - - getButtons = (message, context) -> - allButtons = _.toArray buttons.get() - if message - allowedButtons = _.compact _.map allButtons, (button) -> - if not button.context? or button.context.indexOf(context) > -1 - if not button.validation? or button.validation(message, context) - return button - else - allowedButtons = allButtons - - return _.sortBy allowedButtons, 'order' - - resetButtons = -> - buttons.set {} - - getPermaLink = (msgId) -> - roomData = ChatSubscription.findOne({rid: Session.get('openedRoom')}) - if roomData - routePath = RocketChat.roomTypes.getRouteLink(roomData.t, roomData) - else - routePath = document.location.pathname - return Meteor.absoluteUrl().replace(/\/$/, '') + routePath + '?msg=' + msgId - - hideDropDown = () -> - $('.message-dropdown:visible').hide() - - addButton: addButton - removeButton: removeButton - updateButton: updateButton - getButtons: getButtons - getButtonById: getButtonById - resetButtons: resetButtons - getPermaLink: getPermaLink - hideDropDown: hideDropDown - -Meteor.startup -> - - $(document).click (event) => - target = $(event.target) - if !target.closest('.message-cog-container').length and !target.is('.message-cog-container') - RocketChat.MessageAction.hideDropDown() - - RocketChat.MessageAction.addButton - id: 'reply-message' - icon: 'icon-reply' - i18nLabel: 'Reply' - context: [ - 'message' - 'message-mobile' - ] - action: (event, instance) -> - message = @_arguments[1] - input = instance.find('.input-message') - url = RocketChat.MessageAction.getPermaLink(message._id) - text = '[ ](' + url + ') @' + message.u.username + ' ' - if input.value - input.value += if input.value.endsWith(' ') then '' else ' ' - input.value += text - input.focus() - RocketChat.MessageAction.hideDropDown() - validation: (message) -> - if not RocketChat.models.Subscriptions.findOne({ rid: message.rid })? - return false - return true - order: 1 - - RocketChat.MessageAction.addButton - id: 'edit-message' - icon: 'icon-pencil' - i18nLabel: 'Edit' - context: [ - 'message' - 'message-mobile' - ] - action: (e, instance) -> - message = $(e.currentTarget).closest('.message')[0] - chatMessages[Session.get('openedRoom')].edit(message) - RocketChat.MessageAction.hideDropDown() - input = instance.find('.input-message') - Meteor.setTimeout -> - input.focus() - input.updateAutogrow() - , 200 - validation: (message) -> - if not RocketChat.models.Subscriptions.findOne({ rid: message.rid })? - return false - - hasPermission = RocketChat.authz.hasAtLeastOnePermission('edit-message', message.rid) - isEditAllowed = RocketChat.settings.get 'Message_AllowEditing' - editOwn = message.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(message.ts) if message.ts? - currentTsDiff = moment().diff(msgTs, 'minutes') if msgTs? - return currentTsDiff < blockEditInMinutes - else - return true - order: 2 - - RocketChat.MessageAction.addButton - id: 'delete-message' - icon: 'icon-trash-alt' - i18nLabel: 'Delete' - context: [ - 'message' - 'message-mobile' - ] - action: (event, instance) -> - message = @_arguments[1] - RocketChat.MessageAction.hideDropDown() - chatMessages[Session.get('openedRoom')].confirmDeleteMsg(message) - validation: (message) -> - if not RocketChat.models.Subscriptions.findOne({ rid: message.rid })? - return false - - hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', message.rid) - isDeleteAllowed = RocketChat.settings.get 'Message_AllowDeleting' - deleteOwn = message.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(message.ts) if message.ts? - currentTsDiff = moment().diff(msgTs, 'minutes') if msgTs? - return currentTsDiff < blockDeleteInMinutes - else - return true - order: 3 - - RocketChat.MessageAction.addButton - id: 'permalink' - icon: 'icon-link' - i18nLabel: 'Permalink' - classes: 'clipboard' - context: [ - 'message' - 'message-mobile' - ] - action: (event, instance) -> - message = @_arguments[1] - permalink = RocketChat.MessageAction.getPermaLink(message._id) - RocketChat.MessageAction.hideDropDown() - if Meteor.isCordova - cordova.plugins.clipboard.copy(permalink); - else - $(event.currentTarget).attr('data-clipboard-text', permalink); - toastr.success(TAPi18n.__('Copied')) - validation: (message) -> - if not RocketChat.models.Subscriptions.findOne({ rid: message.rid })? - return false - - return true - order: 4 - - RocketChat.MessageAction.addButton - id: 'copy' - icon: 'icon-paste' - i18nLabel: 'Copy' - classes: 'clipboard' - context: [ - 'message' - 'message-mobile' - ] - action: (event, instance) -> - message = @_arguments[1].msg - RocketChat.MessageAction.hideDropDown() - if Meteor.isCordova - cordova.plugins.clipboard.copy(message); - else - $(event.currentTarget).attr('data-clipboard-text', message) - toastr.success(TAPi18n.__('Copied')) - validation: (message) -> - if not RocketChat.models.Subscriptions.findOne({ rid: message.rid })? - return false - - return true - order: 5 - - RocketChat.MessageAction.addButton - id: 'quote-message' - icon: 'icon-quote-left' - i18nLabel: 'Quote' - context: [ - 'message' - 'message-mobile' - ] - action: (event, instance) -> - message = @_arguments[1] - input = instance.find('.input-message') - url = RocketChat.MessageAction.getPermaLink(message._id) - text = '[ ](' + url + ') ' - if input.value - input.value += if input.value.endsWith(' ') then '' else ' ' - input.value += text - input.focus() - RocketChat.MessageAction.hideDropDown() - validation: (message) -> - if not RocketChat.models.Subscriptions.findOne({ rid: message.rid })? - return false - - return true - order: 6 diff --git a/packages/rocketchat-lib/client/MessageAction.js b/packages/rocketchat-lib/client/MessageAction.js new file mode 100644 index 0000000000000000000000000000000000000000..51ad67e3fa5783e18d1109aa7cad96c3ba8d9aef --- /dev/null +++ b/packages/rocketchat-lib/client/MessageAction.js @@ -0,0 +1,288 @@ +import moment from 'moment'; + +import toastr from 'toastr'; + +RocketChat.MessageAction = new class { + /* + config expects the following keys (only id is mandatory): + id (mandatory) + icon: string + i18nLabel: string + action: function(event, instance) + validation: function(message) + order: integer + */ + + constructor() { + this.buttons = new ReactiveVar({}); + } + + addButton(config) { + if (!config || !config.id) { + return false; + } + return Tracker.nonreactive(() => { + const btns = this.buttons.get(); + btns[config.id] = config; + return this.buttons.set(btns); + }); + } + + removeButton(id) { + return Tracker.nonreactive(() => { + const btns = this.buttons.get(); + delete btns[id]; + return this.buttons.set(btns); + }); + } + + updateButton(id, config) { + return Tracker.nonreactive(() => { + const btns = this.buttons.get(); + if (btns[id]) { + btns[id] = _.extend(btns[id], config); + return this.buttons.set(btns); + } + }); + } + + getButtonById(id) { + const allButtons = this.buttons.get(); + return allButtons[id]; + } + + getButtons(message, context) { + const allButtons = _.toArray(this.buttons.get()); + let allowedButtons = allButtons; + if (message) { + allowedButtons = _.compact(_.map(allButtons, function(button) { + if (button.context == null || button.context.includes(context)) { + if (button.validation == null || button.validation(message, context)) { + return button; + } + } + })); + } + return _.sortBy(allowedButtons, 'order'); + } + + resetButtons() { + return this.buttons.set({}); + } + + getPermaLink(msgId) { + const roomData = ChatSubscription.findOne({ + rid: Session.get('openedRoom') + }); + let routePath = document.location.pathname; + if (roomData) { + routePath = RocketChat.roomTypes.getRouteLink(roomData.t, roomData); + } + return `${ Meteor.absoluteUrl().replace(/\/$/, '') + routePath }?msg=${ msgId }`; + } + + hideDropDown() { + return $('.message-dropdown:visible').hide(); + } +}; + +Meteor.startup(function() { + $(document).click((event) => { + const target = $(event.target); + if (!target.closest('.message-cog-container').length && !target.is('.message-cog-container')) { + return RocketChat.MessageAction.hideDropDown(); + } + }); + + RocketChat.MessageAction.addButton({ + id: 'reply-message', + icon: 'icon-reply', + i18nLabel: 'Reply', + context: ['message', 'message-mobile'], + action(event, instance) { + const message = this._arguments[1]; + const input = instance.find('.input-message'); + const url = RocketChat.MessageAction.getPermaLink(message._id); + const text = `[ ](${ url }) @${ message.u.username } `; + if (input.value) { + input.value += input.value.endsWith(' ') ? '' : ' '; + } + input.value += text; + input.focus(); + return RocketChat.MessageAction.hideDropDown(); + }, + validation(message) { + if (RocketChat.models.Subscriptions.findOne({ + rid: message.rid + }) == null) { + return false; + } + return true; + }, + order: 1 + }); + /* globals chatMessages*/ + RocketChat.MessageAction.addButton({ + id: 'edit-message', + icon: 'icon-pencil', + i18nLabel: 'Edit', + context: ['message', 'message-mobile'], + action(e, instance) { + const message = $(e.currentTarget).closest('.message')[0]; + chatMessages[Session.get('openedRoom')].edit(message); + RocketChat.MessageAction.hideDropDown(); + const input = instance.find('.input-message'); + Meteor.setTimeout(() => { + input.focus(); + input.updateAutogrow(); + }, 200); + }, + validation(message) { + if (RocketChat.models.Subscriptions.findOne({ + rid: message.rid + }) == null) { + return false; + } + const hasPermission = RocketChat.authz.hasAtLeastOnePermission('edit-message', message.rid); + const isEditAllowed = RocketChat.settings.get('Message_AllowEditing'); + const editOwn = message.u && message.u._id === Meteor.userId(); + if (!(hasPermission || (isEditAllowed && editOwn))) { + return; + } + const blockEditInMinutes = RocketChat.settings.get('Message_AllowEditing_BlockEditInMinutes'); + if (blockEditInMinutes) { + let msgTs; + if (message.ts != null) { + msgTs = moment(message.ts); + } + let currentTsDiff; + if (msgTs != null) { + currentTsDiff = moment().diff(msgTs, 'minutes'); + } + return currentTsDiff < blockEditInMinutes; + } else { + return true; + } + }, + order: 2 + }); + RocketChat.MessageAction.addButton({ + id: 'delete-message', + icon: 'icon-trash-alt', + i18nLabel: 'Delete', + context: ['message', 'message-mobile'], + action() { + const message = this._arguments[1]; + RocketChat.MessageAction.hideDropDown(); + return chatMessages[Session.get('openedRoom')].confirmDeleteMsg(message); + }, + validation(message) { + 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) || forceDelete)) { + return; + } + const blockDeleteInMinutes = RocketChat.settings.get('Message_AllowDeleting_BlockDeleteInMinutes'); + if ((blockDeleteInMinutes != null) && blockDeleteInMinutes !== 0 && !(forceDelete)) { + let msgTs; + if (message.ts != null) { + msgTs = moment(message.ts); + } + let currentTsDiff; + if (msgTs != null) { + currentTsDiff = moment().diff(msgTs, 'minutes'); + } + return currentTsDiff < blockDeleteInMinutes; + } else { + return true; + } + }, + order: 3 + }); + /* globals cordova*/ + RocketChat.MessageAction.addButton({ + id: 'permalink', + icon: 'icon-link', + i18nLabel: 'Permalink', + classes: 'clipboard', + context: ['message', 'message-mobile'], + action() { + const message = this._arguments[1]; + const permalink = RocketChat.MessageAction.getPermaLink(message._id); + RocketChat.MessageAction.hideDropDown(); + if (Meteor.isCordova) { + cordova.plugins.clipboard.copy(permalink); + } else { + $(event.currentTarget).attr('data-clipboard-text', permalink); + } + return toastr.success(TAPi18n.__('Copied')); + }, + validation(message) { + if (RocketChat.models.Subscriptions.findOne({ + rid: message.rid + }) == null) { + return false; + } + return true; + }, + order: 4 + }); + RocketChat.MessageAction.addButton({ + id: 'copy', + icon: 'icon-paste', + i18nLabel: 'Copy', + classes: 'clipboard', + context: ['message', 'message-mobile'], + action() { + const message = this._arguments[1].msg; + RocketChat.MessageAction.hideDropDown(); + if (Meteor.isCordova) { + cordova.plugins.clipboard.copy(message); + } else { + $(event.currentTarget).attr('data-clipboard-text', message); + } + return toastr.success(TAPi18n.__('Copied')); + }, + validation(message) { + if (RocketChat.models.Subscriptions.findOne({ + rid: message.rid + }) == null) { + return false; + } + return true; + }, + order: 5 + }); + return RocketChat.MessageAction.addButton({ + id: 'quote-message', + icon: 'icon-quote-left', + i18nLabel: 'Quote', + context: ['message', 'message-mobile'], + action(event, instance) { + const message = this._arguments[1]; + const input = instance.find('.input-message'); + const url = RocketChat.MessageAction.getPermaLink(message._id); + const text = `[ ](${ url }) `; + if (input.value) { + input.value += input.value.endsWith(' ') ? '' : ' '; + } + input.value += text; + input.focus(); + return RocketChat.MessageAction.hideDropDown(); + }, + validation(message) { + if (RocketChat.models.Subscriptions.findOne({ + rid: message.rid + }) == null) { + return false; + } + return true; + }, + order: 6 + }); +}); diff --git a/packages/rocketchat-lib/client/Notifications.coffee b/packages/rocketchat-lib/client/Notifications.coffee deleted file mode 100644 index 9a51ceca34820ff0a5b1f665b42818616765fcf2..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/client/Notifications.coffee +++ /dev/null @@ -1,72 +0,0 @@ -RocketChat.Notifications = new class - constructor: -> - @logged = Meteor.userId() isnt null - @loginCb = [] - Tracker.autorun => - if Meteor.userId() isnt null and this.logged is false - cb() for cb in this.loginCb - - @logged = Meteor.userId() isnt null - - @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' - - if @debug is true - @onAll -> console.log "RocketChat.Notifications: onAll", arguments - @onUser -> console.log "RocketChat.Notifications: onAll", arguments - - onLogin: (cb) -> - @loginCb.push(cb) - if @logged - cb() - - notifyRoom: (room, eventName, args...) -> - console.log "RocketChat.Notifications: notifyRoom", arguments if @debug is true - - args.unshift "#{room}/#{eventName}" - @streamRoom.emit.apply @streamRoom, args - - notifyUser: (userId, eventName, args...) -> - console.log "RocketChat.Notifications: notifyUser", arguments if @debug is true - - args.unshift "#{userId}/#{eventName}" - @streamUser.emit.apply @streamUser, args - - notifyUsersOfRoom: (room, eventName, args...) -> - console.log "RocketChat.Notifications: notifyUsersOfRoom", arguments if @debug is true - - args.unshift "#{room}/#{eventName}" - @streamRoomUsers.emit.apply @streamRoomUsers, args - - onAll: (eventName, callback) -> - @streamAll.on eventName, callback - - onLogged: (eventName, callback) -> - @onLogin => - @streamLogged.on eventName, callback - - onRoom: (room, eventName, callback) -> - if @debug is true - @streamRoom.on room, -> console.log "RocketChat.Notifications: onRoom #{room}", arguments - - @streamRoom.on "#{room}/#{eventName}", callback - - onUser: (eventName, callback) -> - @streamUser.on "#{Meteor.userId()}/#{eventName}", callback - - - unAll: (callback) -> - @streamAll.removeListener 'notify', callback - - unLogged: (callback) -> - @streamLogged.removeListener 'notify', callback - - unRoom: (room, eventName, callback) -> - @streamRoom.removeListener "#{room}/#{eventName}", callback - - unUser: (callback) -> - @streamUser.removeListener Meteor.userId(), callback diff --git a/packages/rocketchat-lib/client/Notifications.js b/packages/rocketchat-lib/client/Notifications.js new file mode 100644 index 0000000000000000000000000000000000000000..4a474c9aec24aeb1227e6d772ea46c595701e1c9 --- /dev/null +++ b/packages/rocketchat-lib/client/Notifications.js @@ -0,0 +1,86 @@ +RocketChat.Notifications = new class { + constructor() { + this.logged = Meteor.userId() !== null; + this.loginCb = []; + Tracker.autorun(() => { + if (Meteor.userId() !== null && this.logged === false) { + this.loginCb.forEach(cb => cb()); + } + return this.logged = Meteor.userId() !== null; + }); + 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'); + if (this.debug === true) { + this.onAll(function() { + return console.log('RocketChat.Notifications: onAll', arguments); + }); + this.onUser(function() { + return console.log('RocketChat.Notifications: onAll', arguments); + }); + } + } + + onLogin(cb) { + this.loginCb.push(cb); + if (this.logged) { + return cb(); + } + } + notifyRoom(room, eventName, ...args) { + if (this.debug === true) { + console.log('RocketChat.Notifications: notifyRoom', arguments); + } + args.unshift(`${ room }/${ eventName }`); + return this.streamRoom.emit.apply(this.streamRoom, args); + } + notifyUser(userId, eventName, ...args) { + if (this.debug === true) { + console.log('RocketChat.Notifications: notifyUser', arguments); + } + args.unshift(`${ userId }/${ eventName }`); + return this.streamUser.emit.apply(this.streamUser, args); + } + notifyUsersOfRoom(room, eventName, ...args) { + if (this.debug === true) { + console.log('RocketChat.Notifications: notifyUsersOfRoom', arguments); + } + args.unshift(`${ room }/${ eventName }`); + return this.streamRoomUsers.emit.apply(this.streamRoomUsers, args); + } + onAll(eventName, callback) { + return this.streamAll.on(eventName, callback); + } + onLogged(eventName, callback) { + return this.onLogin(() => { + return this.streamLogged.on(eventName, callback); + }); + } + onRoom(room, eventName, callback) { + if (this.debug === true) { + this.streamRoom.on(room, function() { + return console.log(`RocketChat.Notifications: onRoom ${ room }`, arguments); + }); + } + return this.streamRoom.on(`${ room }/${ eventName }`, callback); + } + onUser(eventName, callback) { + return this.streamUser.on(`${ Meteor.userId() }/${ eventName }`, callback); + } + unAll(callback) { + return this.streamAll.removeListener('notify', callback); + } + unLogged(callback) { + return this.streamLogged.removeListener('notify', callback); + } + unRoom(room, eventName, callback) { + return this.streamRoom.removeListener(`${ room }/${ eventName }`, callback); + } + unUser(callback) { + return this.streamUser.removeListener(Meteor.userId(), callback); + } + +}; diff --git a/packages/rocketchat-lib/client/lib/openRoom.coffee b/packages/rocketchat-lib/client/lib/openRoom.coffee index 33e023abf9c97482f2c8a36f7eff1c08522e03d0..3130cd82a3e0f41ead8b19fd979e576125d94623 100644 --- a/packages/rocketchat-lib/client/lib/openRoom.coffee +++ b/packages/rocketchat-lib/client/lib/openRoom.coffee @@ -6,7 +6,7 @@ currentTracker = undefined Meteor.defer -> currentTracker = Tracker.autorun (c) -> user = Meteor.user() - if (user? and not user.username?) or (not user? and RocketChat.settings.get('Accounts_AllowAnonymousAccess') is false) + if (user? and not user.username?) or (not user? and RocketChat.settings.get('Accounts_AllowAnonymousRead') is false) BlazeLayout.render 'main' return diff --git a/packages/rocketchat-lib/client/lib/openRoom.js b/packages/rocketchat-lib/client/lib/openRoom.js new file mode 100644 index 0000000000000000000000000000000000000000..b283b51c08abd988c5f96c17c044bd6052e51f64 --- /dev/null +++ b/packages/rocketchat-lib/client/lib/openRoom.js @@ -0,0 +1,98 @@ +/* globals fireGlobalEvent readMessage currentTracker*/ +currentTracker = undefined; + +function openRoom(type, name) { + Session.set('openedRoom', null); + + return Meteor.defer(() => + currentTracker = Tracker.autorun(function(c) { + const user = Meteor.user(); + if ((user && user.username == null) || user == null && RocketChat.settings.get('Accounts_AllowAnonymousAccess') === false) { + BlazeLayout.render('main'); + return; + } + + if (RoomManager.open(type + name).ready() !== true) { + BlazeLayout.render('main', { modal: RocketChat.Layout.isEmbedded(), center: 'loading' }); + return; + } + if (currentTracker) { + currentTracker = undefined; + } + c.stop(); + + const room = RocketChat.roomTypes.findRoom(type, name, user); + if (room == null) { + if (type === 'd') { + Meteor.call('createDirectMessage', name, function(err) { + if (!err) { + RoomManager.close(type + name); + return openRoom('d', name); + } else { + Session.set('roomNotFound', {type, name}); + BlazeLayout.render('main', {center: 'roomNotFound'}); + return; + } + }); + } else { + Meteor.call('getRoomByTypeAndName', type, name, function(err, record) { + if (err) { + Session.set('roomNotFound', {type, name}); + return BlazeLayout.render('main', {center: 'roomNotFound'}); + } else { + delete record.$loki; + RocketChat.models.Rooms.upsert({ _id: record._id }, _.omit(record, '_id')); + RoomManager.close(type + name); + return openRoom(type, name); + } + }); + } + return; + } + + const mainNode = document.querySelector('.main-content'); + if (mainNode) { + for (const child of Array.from(mainNode.children)) { + if (child) { mainNode.removeChild(child); } + } + const 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() && window.chatMessages && window.chatMessages[room._id] != null) { + setTimeout(() => $('.message-form .input-message').focus(), 100); + } + + // update user's room subscription + const sub = ChatSubscription.findOne({rid: room._id}); + if (sub && sub.open === false) { + Meteor.call('openRoom', room._id, function(err) { + if (err) { + return handleError(err); + } + }); + } + + if (FlowRouter.getQueryParam('msg')) { + const msg = { _id: FlowRouter.getQueryParam('msg'), rid: room._id }; + RoomHistoryManager.getSurroundingMessages(msg); + } + + return RocketChat.callbacks.run('enter-room', sub); + }) + ); +} +export { openRoom }; +this.openRoom = openRoom; diff --git a/packages/rocketchat-lib/client/lib/roomExit.coffee b/packages/rocketchat-lib/client/lib/roomExit.coffee deleted file mode 100644 index d7da9ed4a8718746e4b3c507e468a6411e9d59b6..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/client/lib/roomExit.coffee +++ /dev/null @@ -1,20 +0,0 @@ -@roomExit = -> - RocketChat.callbacks.run 'roomExit' - - BlazeLayout.render 'main', {center: 'none'} - - if currentTracker? - currentTracker.stop() - - mainNode = document.querySelector('.main-content') - if mainNode? - for child in mainNode.children - if child? - if child.classList.contains('room-container') - wrapper = child.querySelector('.messages-box > .wrapper') - if wrapper - if wrapper.scrollTop >= wrapper.scrollHeight - wrapper.clientHeight - child.oldScrollTop = 10e10 - else - child.oldScrollTop = wrapper.scrollTop - mainNode.removeChild child diff --git a/packages/rocketchat-lib/client/lib/roomExit.js b/packages/rocketchat-lib/client/lib/roomExit.js new file mode 100644 index 0000000000000000000000000000000000000000..36f1260e34207bac72a5846602290a583f13b399 --- /dev/null +++ b/packages/rocketchat-lib/client/lib/roomExit.js @@ -0,0 +1,31 @@ +/*globals currentTracker */ +this.roomExit = function() { + RocketChat.callbacks.run('roomExit'); + BlazeLayout.render('main', { + center: 'none' + }); + + if (typeof currentTracker !== 'undefined') { + currentTracker.stop(); + } + const mainNode = document.querySelector('.main-content'); + if (mainNode == null) { + return; + } + return [...mainNode.children].forEach(child => { + if (child == null) { + return; + } + if (child.classList.contains('room-container')) { + const wrapper = child.querySelector('.messages-box > .wrapper'); + if (wrapper) { + if (wrapper.scrollTop >= wrapper.scrollHeight - wrapper.clientHeight) { + child.oldScrollTop = 10e10; + } else { + child.oldScrollTop = wrapper.scrollTop; + } + } + } + mainNode.removeChild(child); + }); +}; diff --git a/packages/rocketchat-lib/client/lib/roomTypes.coffee b/packages/rocketchat-lib/client/lib/roomTypes.coffee deleted file mode 100644 index afe19195bf91384b4a1baa13ee112cc350708116..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/client/lib/roomTypes.coffee +++ /dev/null @@ -1,90 +0,0 @@ -RocketChat.roomTypes = new class roomTypesClient extends roomTypesCommon - checkCondition: (roomType) -> - return not roomType.condition? or roomType.condition() - - getTypes: -> - orderedTypes = [] - - _.sortBy(@roomTypesOrder, 'order').forEach (type) => - orderedTypes.push @roomTypes[type.identifier] - - return orderedTypes - - getIcon: (roomType) -> - return @roomTypes[roomType]?.icon - - getRoomName: (roomType, roomData) -> - return @roomTypes[roomType]?.roomName roomData - - getSecondaryRoomName: (roomType, roomData) -> - return @roomTypes[roomType]?.secondaryRoomName?(roomData) - - getIdentifiers: (except) -> - except = [].concat except - list = _.reject @roomTypesOrder, (t) -> return except.indexOf(t.identifier) isnt -1 - return _.map list, (t) -> return t.identifier - - getUserStatus: (roomType, roomId) -> - return @roomTypes[roomType]?.getUserStatus?(roomId) - - findRoom: (roomType, identifier, user) -> - return @roomTypes[roomType]?.findRoom identifier, user - - canSendMessage: (roomId) -> - return ChatSubscription.find({ rid: roomId }).count() > 0 - - readOnly: (roomId, user) -> - - fields = { ro: 1 } - - # if a user has been specified then we want to see if that user has been muted in the room - if user - fields.muted = 1 - - room = ChatRoom.findOne({ _id: roomId }, fields : fields) - - unless user - return room?.ro; - - userOwner = RoomRoles.findOne({ rid: roomId, "u._id": user._id, roles: 'owner' }, { fields: { _id: 1 } }) - - return room?.ro is true and Array.isArray(room?.muted) and room?.muted.indexOf(user.username) != -1 and !userOwner - - archived: (roomId) -> - fields = { archived: 1 } - - room = ChatRoom.findOne({ _id: roomId }, fields : fields) - - return room?.archived is true - - verifyCanSendMessage: (roomId) -> - room = ChatRoom.findOne({ _id: roomId }, { fields: { t: 1 } }) - return if not room?.t? - - roomType = room.t - - return @roomTypes[roomType]?.canSendMessage roomId if @roomTypes[roomType]?.canSendMessage? - - return @canSendMessage roomId - - verifyShowJoinLink: (roomId) -> - room = ChatRoom.findOne({ _id: roomId }, { fields: { t: 1 } }) - return if not room?.t? - - roomType = room.t - - if not @roomTypes[roomType]?.showJoinLink? - return false - - return @roomTypes[roomType].showJoinLink roomId - - getNotSubscribedTpl: (roomId) -> - room = ChatRoom.findOne({ _id: roomId }, { fields: { t: 1 } }) - return if not room?.t? - - roomType = room.t - - if not @roomTypes[roomType]?.notSubscribedTpl? - return false - - return @roomTypes[roomType].notSubscribedTpl diff --git a/packages/rocketchat-lib/client/lib/roomTypes.js b/packages/rocketchat-lib/client/lib/roomTypes.js new file mode 100644 index 0000000000000000000000000000000000000000..fdc926c063da070bddc26e0ed9bd95dbcfb640b8 --- /dev/null +++ b/packages/rocketchat-lib/client/lib/roomTypes.js @@ -0,0 +1,115 @@ +import roomTypesCommon from '../../lib/roomTypesCommon'; + +RocketChat.roomTypes = new class extends roomTypesCommon { + checkCondition(roomType) { + return roomType.condition == null || roomType.condition(); + } + getTypes() { + return _.sortBy(this.roomTypesOrder, 'order').map((type) => this.roomTypes[type.identifier]); + } + getIcon(roomType) { + return this.roomTypes[roomType] && this.roomTypes[roomType].icon; + } + getRoomName(roomType, roomData) { + return this.roomTypes[roomType] && this.roomTypes[roomType].roomName && this.roomTypes[roomType].roomName(roomData); + } + getSecondaryRoomName(roomType, roomData) { + return this.roomTypes[roomType] && typeof this.roomTypes[roomType].secondaryRoomName === 'function' && this.roomTypes[roomType].secondaryRoomName(roomData); + } + getIdentifiers(e) { + const except = [].concat(e); + const list = _.reject(this.roomTypesOrder, (t) => except.indexOf(t.identifier) !== -1); + return _.map(list, (t) => t.identifier); + } + getUserStatus(roomType, 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); + } + canSendMessage(roomId) { + return ChatSubscription.find({ + rid: roomId + }).count() > 0; + } + readOnly(roomId, user) { + const fields = { + ro: 1 + }; + if (user) { + fields.muted = 1; + } + const room = ChatRoom.findOne({ + _id: roomId + }, { + fields + }); + if (!user) { + return room && room.ro; + } + /* globals RoomRoles */ + const userOwner = RoomRoles.findOne({ + rid: roomId, + 'u._id': user._id, + roles: 'owner' + }, { + fields: { + _id: 1 + } + }); + return room && (room.ro === true && Array.isArray(room.muted) && room.muted.indexOf(user.username) !== -1 && !userOwner); + } + archived(roomId) { + const fields = { + archived: 1 + }; + const room = ChatRoom.findOne({ + _id: roomId + }, { + fields + }); + return room && room.archived === true; + } + verifyCanSendMessage(roomId) { + const room = ChatRoom.findOne({ _id: roomId }, { fields: { t: 1 }}); + + if (!room || !room.t) { + return; + } + + const roomType = room.t; + if (this.roomTypes[roomType] && this.roomTypes[roomType].canSendMessage) { + return this.roomTypes[roomType].canSendMessage(roomId); + } + return this.canSendMessage(roomId); + } + verifyShowJoinLink(roomId) { + const room = ChatRoom.findOne({ + _id: roomId + }, { + fields: { + t: 1 + } + }); + if (!room || !room.t) { + return; + } + const roomType = room.t; + if (this.roomTypes[roomType] && !this.roomTypes[roomType].showJoinLink) { + return false; + } + return this.roomTypes[roomType].showJoinLink(roomId); + } + getNotSubscribedTpl(roomId) { + const room = ChatRoom.findOne({ _id: roomId }, { fields: { t: 1 }}); + if (!room || !room.t) { + return; + } + const roomType = room.t; + if (this.roomTypes[roomType] && !this.roomTypes[roomType].notSubscribedTpl) { + return false; + } + return this.roomTypes[roomType].notSubscribedTpl; + } + +}; diff --git a/packages/rocketchat-lib/client/lib/settings.coffee b/packages/rocketchat-lib/client/lib/settings.coffee deleted file mode 100644 index 38c08e546ba265db1bea2ae476ef3c1188dd76c5..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/client/lib/settings.coffee +++ /dev/null @@ -1,65 +0,0 @@ -### -# RocketChat.settings holds all packages settings -# @namespace RocketChat.settings -### - -RocketChat.settings.cachedCollection = new RocketChat.CachedCollection({ name: 'public-settings', eventType: 'onAll', userRelated: false }) -RocketChat.settings.collection = RocketChat.settings.cachedCollection.collection -RocketChat.settings.cachedCollection.init() - -RocketChat.settings.dict = new ReactiveDict 'settings' - -RocketChat.settings.get = (_id) -> - return RocketChat.settings.dict.get(_id) - -RocketChat.settings.init = -> - initialLoad = true - RocketChat.settings.collection.find().observe - added: (record) -> - Meteor.settings[record._id] = record.value - RocketChat.settings.dict.set record._id, record.value - RocketChat.settings.load record._id, record.value, initialLoad - changed: (record) -> - Meteor.settings[record._id] = record.value - RocketChat.settings.dict.set record._id, record.value - RocketChat.settings.load record._id, record.value, initialLoad - removed: (record) -> - delete Meteor.settings[record._id] - RocketChat.settings.dict.set record._id, undefined - RocketChat.settings.load record._id, undefined, initialLoad - initialLoad = false - -RocketChat.settings.init() - -Meteor.startup -> - if Meteor.isCordova is false - Tracker.autorun (c) -> - siteUrl = RocketChat.settings.get('Site_Url') - if not siteUrl or not Meteor.userId()? - return - - if RocketChat.authz.hasRole(Meteor.userId(), 'admin') is false or Meteor.settings.public.sandstorm - return c.stop() - - Meteor.setTimeout -> - if __meteor_runtime_config__.ROOT_URL isnt location.origin - currentUrl = location.origin + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX - swal - type: 'warning' - title: t('Warning') - text: t("The_setting_s_is_configured_to_s_and_you_are_accessing_from_s", t('Site_Url'), siteUrl, currentUrl) + '<br/><br/>' + t("Do_you_want_to_change_to_s_question", currentUrl) - showCancelButton: true - confirmButtonText: t('Yes') - cancelButtonText: t('Cancel') - closeOnConfirm: false - html: true - , -> - Meteor.call 'saveSetting', 'Site_Url', currentUrl, -> - swal - title: t('Saved') - type: 'success' - timer: 1000 - showConfirmButton: false - , 100 - - return c.stop() diff --git a/packages/rocketchat-lib/client/lib/settings.js b/packages/rocketchat-lib/client/lib/settings.js new file mode 100644 index 0000000000000000000000000000000000000000..0f0c946ecd81f4250bae1b0358b165754150f9a8 --- /dev/null +++ b/packages/rocketchat-lib/client/lib/settings.js @@ -0,0 +1,87 @@ + +/* +* RocketChat.settings holds all packages settings +* @namespace RocketChat.settings +*/ + +/* globals ReactiveDict*/ + +RocketChat.settings.cachedCollection = new RocketChat.CachedCollection({ + name: 'public-settings', + eventType: 'onAll', + userRelated: false +}); + +RocketChat.settings.collection = RocketChat.settings.cachedCollection.collection; + +RocketChat.settings.cachedCollection.init(); + +RocketChat.settings.dict = new ReactiveDict('settings'); + +RocketChat.settings.get = function(_id) { + return RocketChat.settings.dict.get(_id); +}; + +RocketChat.settings.init = function() { + let initialLoad = true; + RocketChat.settings.collection.find().observe({ + added(record) { + Meteor.settings[record._id] = record.value; + RocketChat.settings.dict.set(record._id, record.value); + return RocketChat.settings.load(record._id, record.value, initialLoad); + }, + changed(record) { + Meteor.settings[record._id] = record.value; + RocketChat.settings.dict.set(record._id, record.value); + return RocketChat.settings.load(record._id, record.value, initialLoad); + }, + removed(record) { + delete Meteor.settings[record._id]; + RocketChat.settings.dict.set(record._id, null); + return RocketChat.settings.load(record._id, null, initialLoad); + } + }); + return initialLoad = false; +}; + +RocketChat.settings.init(); + +Meteor.startup(function() { + if (Meteor.isCordova === true) { + return; + } + Tracker.autorun(function(c) { + const siteUrl = RocketChat.settings.get('Site_Url'); + if (!siteUrl || (Meteor.userId() == null)) { + return; + } + if (RocketChat.authz.hasRole(Meteor.userId(), 'admin') === false || Meteor.settings['public'].sandstorm) { + return c.stop(); + } + Meteor.setTimeout(function() { + if (__meteor_runtime_config__.ROOT_URL !== location.origin) { + const currentUrl = location.origin + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX; + swal({ + type: 'warning', + title: t('Warning'), + text: `${ t('The_setting_s_is_configured_to_s_and_you_are_accessing_from_s', t('Site_Url'), siteUrl, currentUrl) }<br/><br/>${ t('Do_you_want_to_change_to_s_question', currentUrl) }`, + showCancelButton: true, + confirmButtonText: t('Yes'), + cancelButtonText: t('Cancel'), + closeOnConfirm: false, + html: true + }, function() { + Meteor.call('saveSetting', 'Site_Url', currentUrl, function() { + swal({ + title: t('Saved'), + type: 'success', + timer: 1000, + showConfirmButton: false + }); + }); + }); + } + }, 100); + return c.stop(); + }); +}); diff --git a/packages/rocketchat-lib/client/methods/sendMessage.coffee b/packages/rocketchat-lib/client/methods/sendMessage.coffee deleted file mode 100644 index 68cb0a2f8c6ef0fc27ec54c4363be222dc72bed2..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/client/methods/sendMessage.coffee +++ /dev/null @@ -1,26 +0,0 @@ -Meteor.methods - sendMessage: (message) -> - if not Meteor.userId() - return false - - if _.trim(message.msg) isnt '' - if isNaN(TimeSync.serverOffset()) - message.ts = new Date() - else - message.ts = new Date(Date.now() + TimeSync.serverOffset()) - - user = Meteor.user() - message.u = - _id: Meteor.userId() - username: user.username - - if RocketChat.settings.get('UI_Use_Real_Name') - message.u.name = user.name - - message.temp = true - - message = RocketChat.callbacks.run 'beforeSaveMessage', message - - RocketChat.promises.run('onClientMessageReceived', message).then (message) -> - ChatMessage.insert message - RocketChat.callbacks.run 'afterSaveMessage', message diff --git a/packages/rocketchat-lib/client/methods/sendMessage.js b/packages/rocketchat-lib/client/methods/sendMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..85cce415c2a88907602509fd4196458200ff1586 --- /dev/null +++ b/packages/rocketchat-lib/client/methods/sendMessage.js @@ -0,0 +1,22 @@ +Meteor.methods({ + sendMessage(message) { + if (!Meteor.userId() || _.trim(message.msg) === '') { + return false; + } + const user = Meteor.user(); + message.ts = isNaN(TimeSync.serverOffset()) ? new Date() : new Date(Date.now() + TimeSync.serverOffset()); + message.u = { + _id: Meteor.userId(), + username: user.username + }; + if (RocketChat.settings.get('UI_Use_Real_Name')) { + message.u.name = user.name; + } + message.temp = true; + message = RocketChat.callbacks.run('beforeSaveMessage', message); + RocketChat.promises.run('onClientMessageReceived', message).then(function(message) { + ChatMessage.insert(message); + return RocketChat.callbacks.run('afterSaveMessage', message); + }); + } +}); diff --git a/packages/rocketchat-lib/client/models/Uploads.coffee b/packages/rocketchat-lib/client/models/Uploads.coffee deleted file mode 100644 index 4572e735b99b606c4227850370eeca118331bf0b..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/client/models/Uploads.coffee +++ /dev/null @@ -1,3 +0,0 @@ -RocketChat.models.Uploads = new class extends RocketChat.models._Base - constructor: -> - @_initModel 'uploads' diff --git a/packages/rocketchat-lib/client/models/Uploads.js b/packages/rocketchat-lib/client/models/Uploads.js new file mode 100644 index 0000000000000000000000000000000000000000..eefe630c708c45d0ea928bc463c709d57ebb185f --- /dev/null +++ b/packages/rocketchat-lib/client/models/Uploads.js @@ -0,0 +1,7 @@ + +RocketChat.models.Uploads = new class extends RocketChat.models._Base { + constructor() { + super(); + this._initModel('uploads'); + } +}; diff --git a/packages/rocketchat-lib/client/models/_Base.coffee b/packages/rocketchat-lib/client/models/_Base.coffee deleted file mode 100644 index fb485723241a050a72683b29197e66489c222a55..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/client/models/_Base.coffee +++ /dev/null @@ -1,44 +0,0 @@ -RocketChat.models._Base = class - _baseName: -> - return 'rocketchat_' - - _initModel: (name) -> - check name, String - - @model = new Mongo.Collection @_baseName() + name - - find: -> - return @model.find.apply @model, arguments - - findOne: -> - return @model.findOne.apply @model, arguments - - insert: -> - return @model.insert.apply @model, arguments - - update: -> - return @model.update.apply @model, arguments - - upsert: -> - return @model.upsert.apply @model, arguments - - remove: -> - return @model.remove.apply @model, arguments - - allow: -> - return @model.allow.apply @model, arguments - - deny: -> - return @model.deny.apply @model, arguments - - ensureIndex: -> - return - - dropIndex: -> - return - - tryEnsureIndex: -> - return - - tryDropIndex: -> - return diff --git a/packages/rocketchat-lib/client/models/_Base.js b/packages/rocketchat-lib/client/models/_Base.js new file mode 100644 index 0000000000000000000000000000000000000000..55623317be663df437f1d3e3d17191a97a47ddf8 --- /dev/null +++ b/packages/rocketchat-lib/client/models/_Base.js @@ -0,0 +1,52 @@ +RocketChat.models._Base = class { + + _baseName() { + return 'rocketchat_'; + } + + _initModel(name) { + check(name, String); + return this.model = new Mongo.Collection(this._baseName() + name); + } + + find() { + return this.model.find.apply(this.model, arguments); + } + + findOne() { + return this.model.findOne.apply(this.model, arguments); + } + + insert() { + return this.model.insert.apply(this.model, arguments); + } + + update() { + return this.model.update.apply(this.model, arguments); + } + + upsert() { + return this.model.upsert.apply(this.model, arguments); + } + + remove() { + return this.model.remove.apply(this.model, arguments); + } + + allow() { + return this.model.allow.apply(this.model, arguments); + } + + deny() { + return this.model.deny.apply(this.model, arguments); + } + + ensureIndex() {} + + dropIndex() {} + + tryEnsureIndex() {} + + tryDropIndex() {} + +}; diff --git a/packages/rocketchat-lib/lib/callbacks.coffee b/packages/rocketchat-lib/lib/callbacks.coffee deleted file mode 100644 index d0e3feb8ff32ee535f05d92f7e70be9533dd498e..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/lib/callbacks.coffee +++ /dev/null @@ -1,129 +0,0 @@ -# https://github.com/TelescopeJS/Telescope/blob/master/packages/telescope-lib/lib/callbacks.js - -### -# Callback hooks provide an easy way to add extra steps to common operations. -# @namespace RocketChat.callbacks -### -RocketChat.callbacks = {} - -if Meteor.isServer - RocketChat.callbacks.showTime = true - RocketChat.callbacks.showTotalTime = true -else - RocketChat.callbacks.showTime = false - RocketChat.callbacks.showTotalTime = false - -### -# Callback priorities -### -RocketChat.callbacks.priority = - HIGH: -1000 - MEDIUM: 0 - LOW: 1000 - -### -# Add a callback function to a hook -# @param {String} hook - The name of the hook -# @param {Function} callback - The callback function -### -RocketChat.callbacks.add = (hook, callback, priority, id) -> - # if callback array doesn't exist yet, initialize it - priority ?= RocketChat.callbacks.priority.MEDIUM - unless _.isNumber priority - priority = RocketChat.callbacks.priority.MEDIUM - callback.priority = priority - callback.id = id or Random.id() - RocketChat.callbacks[hook] ?= [] - - if RocketChat.callbacks.showTime is true - err = new Error - callback.stack = err.stack - - # if not id? - # console.log('Callback without id', callback.stack) - - # Avoid adding the same callback twice - for cb in RocketChat.callbacks[hook] - if cb.id is callback.id - return - - RocketChat.callbacks[hook].push callback - return - -### -# Remove a callback from a hook -# @param {string} hook - The name of the hook -# @param {string} id - The callback's id -### - -RocketChat.callbacks.remove = (hookName, id) -> - RocketChat.callbacks[hookName] = _.reject RocketChat.callbacks[hookName], (callback) -> - callback.id is id - return - -### -# Successively run all of a hook's callbacks on an item -# @param {String} hook - The name of the hook -# @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks -# @param {Object} [constant] - An optional constant that will be passed along to each callback -# @returns {Object} Returns the item after it's been through all the callbacks for this hook -### - -RocketChat.callbacks.run = (hook, item, constant) -> - callbacks = RocketChat.callbacks[hook] - if !!callbacks?.length - if RocketChat.callbacks.showTotalTime is true - totalTime = 0 - - # if the hook exists, and contains callbacks to run - result = _.sortBy(callbacks, (callback) -> return callback.priority or RocketChat.callbacks.priority.MEDIUM).reduce (result, callback) -> - # console.log(callback.name); - if RocketChat.callbacks.showTime is true or RocketChat.callbacks.showTotalTime is true - time = Date.now() - - callbackResult = callback result, constant - - if RocketChat.callbacks.showTime is true or RocketChat.callbacks.showTotalTime is true - currentTime = Date.now() - time - totalTime += currentTime - if RocketChat.callbacks.showTime is true - if Meteor.isServer - RocketChat.statsTracker.timing('callbacks.time', currentTime, ["hook:#{hook}", "callback:#{callback.id}"]); - else - console.log String(currentTime), hook, callback.id, callback.stack?.split?('\n')[2]?.match(/\(.+\)/)?[0] - - return if typeof callbackResult == 'undefined' then result else callbackResult - , item - - if RocketChat.callbacks.showTotalTime is true - if Meteor.isServer - RocketChat.statsTracker.timing('callbacks.totalTime', totalTime, ["hook:#{hook}"]); - else - console.log hook+':', totalTime - - return result - else - # else, just return the item unchanged - return item - -### -# Successively run all of a hook's callbacks on an item, in async mode (only works on server) -# @param {String} hook - The name of the hook -# @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks -# @param {Object} [constant] - An optional constant that will be passed along to each callback -### - -RocketChat.callbacks.runAsync = (hook, item, constant) -> - callbacks = RocketChat.callbacks[hook] - if Meteor.isServer and !!callbacks?.length - # use defer to avoid holding up client - Meteor.defer -> - # run all post submit server callbacks on post object successively - _.sortBy(callbacks, (callback) -> return callback.priority or RocketChat.callbacks.priority.MEDIUM).forEach (callback) -> - # console.log(callback.name); - callback item, constant - return - return - else - return item - return diff --git a/packages/rocketchat-lib/lib/callbacks.js b/packages/rocketchat-lib/lib/callbacks.js new file mode 100644 index 0000000000000000000000000000000000000000..86f56a2eea8aef5208a883edcfb7ccbffbb9651a --- /dev/null +++ b/packages/rocketchat-lib/lib/callbacks.js @@ -0,0 +1,131 @@ +/* +* Callback hooks provide an easy way to add extra steps to common operations. +* @namespace RocketChat.callbacks +*/ + +RocketChat.callbacks = {}; + +if (Meteor.isServer) { + RocketChat.callbacks.showTime = true; + RocketChat.callbacks.showTotalTime = true; +} else { + RocketChat.callbacks.showTime = false; + RocketChat.callbacks.showTotalTime = false; +} + + +/* +* Callback priorities +*/ + +RocketChat.callbacks.priority = { + HIGH: -1000, + MEDIUM: 0, + LOW: 1000 +}; + + +/* +* Add a callback function to a hook +* @param {String} hook - The name of the hook +* @param {Function} callback - The callback function +*/ + +RocketChat.callbacks.add = function(hook, callback, priority, id) { + if (priority == null) { + priority = RocketChat.callbacks.priority.MEDIUM; + } + if (!_.isNumber(priority)) { + priority = RocketChat.callbacks.priority.MEDIUM; + } + callback.priority = priority; + callback.id = id || Random.id(); + RocketChat.callbacks[hook] = RocketChat.callbacks[hook] || []; + if (RocketChat.callbacks.showTime === true) { + const err = new Error; + callback.stack = err.stack; + } + if (RocketChat.callbacks[hook].find((cb) => cb.id === callback.id)) { + return; + } + RocketChat.callbacks[hook].push(callback); +}; + + +/* +* Remove a callback from a hook +* @param {string} hook - The name of the hook +* @param {string} id - The callback's id +*/ + +RocketChat.callbacks.remove = function(hookName, id) { + RocketChat.callbacks[hookName] = _.reject(RocketChat.callbacks[hookName], (callback) => callback.id === id); +}; + + +/* +* Successively run all of a hook's callbacks on an item +* @param {String} hook - The name of the hook +* @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks +* @param {Object} [constant] - An optional constant that will be passed along to each callback +* @returns {Object} Returns the item after it's been through all the callbacks for this hook +*/ + +RocketChat.callbacks.run = function(hook, item, constant) { + const callbacks = RocketChat.callbacks[hook]; + if (callbacks && callbacks.length) { + let totalTime = 0; + const result = _.sortBy(callbacks, function(callback) { + return callback.priority || RocketChat.callbacks.priority.MEDIUM; + }).reduce(function(result, callback) { + let time = 0; + if (RocketChat.callbacks.showTime === true || RocketChat.callbacks.showTotalTime === true) { + time = Date.now(); + } + const callbackResult = callback(result, constant); + if (RocketChat.callbacks.showTime === true || RocketChat.callbacks.showTotalTime === true) { + const currentTime = Date.now() - time; + totalTime += currentTime; + if (RocketChat.callbacks.showTime === true) { + if (Meteor.isServer) { + RocketChat.statsTracker.timing('callbacks.time', currentTime, [`hook:${ hook }`, `callback:${ callback.id }`]); + } else { + let stack = callback.stack && typeof callback.stack.split === 'function' && callback.stack.split('\n'); + stack = stack && stack[2] && (stack[2].match(/\(.+\)/)||[])[0]; + console.log(String(currentTime), hook, callback.id, stack); + } + } + } + return (typeof callbackResult === 'undefined') ? result : callbackResult; + }, item); + if (RocketChat.callbacks.showTotalTime === true) { + if (Meteor.isServer) { + RocketChat.statsTracker.timing('callbacks.totalTime', totalTime, [`hook:${ hook }`]); + } else { + console.log(`${ hook }:`, totalTime); + } + } + return result; + } else { + return item; + } +}; + + +/* +* Successively run all of a hook's callbacks on an item, in async mode (only works on server) +* @param {String} hook - The name of the hook +* @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks +* @param {Object} [constant] - An optional constant that will be passed along to each callback +*/ + +RocketChat.callbacks.runAsync = function(hook, item, constant) { + const callbacks = RocketChat.callbacks[hook]; + if (Meteor.isServer && callbacks && callbacks.length) { + Meteor.defer(function() { + _.sortBy(callbacks, (callback) => callback.priority || RocketChat.callbacks.priority.MEDIUM).forEach((callback) => callback(item, constant)); + }); + } else { + return item; + } +}; diff --git a/packages/rocketchat-lib/lib/promises.coffee b/packages/rocketchat-lib/lib/promises.coffee deleted file mode 100644 index a23ab907b738c7960ddf4772ecdc7081e0995736..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/lib/promises.coffee +++ /dev/null @@ -1,93 +0,0 @@ -# https://github.com/TelescopeJS/Telescope/blob/master/packages/telescope-lib/lib/callbacks.js - -### -# Callback hooks provide an easy way to add extra steps to common operations. -# @namespace RocketChat.promises -### -RocketChat.promises = {} - -### -# Callback priorities -### -RocketChat.promises.priority = - HIGH: -1000 - MEDIUM: 0 - LOW: 1000 - -### -# Add a callback function to a hook -# @param {String} hook - The name of the hook -# @param {Function} callback - The callback function -### - -RocketChat.promises.add = (hook, callback, priority, id) -> - # if callback array doesn't exist yet, initialize it - priority ?= RocketChat.promises.priority.MEDIUM - unless _.isNumber priority - priority = RocketChat.promises.priority.MEDIUM - callback.priority = priority - callback.id = id or Random.id() - RocketChat.promises[hook] ?= [] - - # Avoid adding the same callback twice - for cb in RocketChat.promises[hook] - if cb.id is callback.id - return - - RocketChat.promises[hook].push callback - return - -### -# Remove a callback from a hook -# @param {string} hook - The name of the hook -# @param {string} id - The callback's id -### - -RocketChat.promises.remove = (hookName, id) -> - RocketChat.promises[hookName] = _.reject RocketChat.promises[hookName], (callback) -> - callback.id is id - return - -### -# Successively run all of a hook's callbacks on an item -# @param {String} hook - The name of the hook -# @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks -# @param {Object} [constant] - An optional constant that will be passed along to each callback -# @returns {Object} Returns the item after it's been through all the callbacks for this hook -### - -RocketChat.promises.run = (hook, item, constant) -> - callbacks = RocketChat.promises[hook] - if !!callbacks?.length - # if the hook exists, and contains callbacks to run - callbacks = _.sortBy(callbacks, (callback) -> return callback.priority or RocketChat.promises.priority.MEDIUM) - return callbacks.reduce (previousPromise, callback) -> - return new Promise (resolve, reject) -> - previousPromise.then (result) -> - callback(result, constant).then(resolve, reject) - , Promise.resolve(item) - else - # else, just return the item unchanged - return Promise.resolve(item) - -### -# Successively run all of a hook's callbacks on an item, in async mode (only works on server) -# @param {String} hook - The name of the hook -# @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks -# @param {Object} [constant] - An optional constant that will be passed along to each callback -### - -RocketChat.promises.runAsync = (hook, item, constant) -> - callbacks = RocketChat.promises[hook] - if Meteor.isServer and !!callbacks?.length - # use defer to avoid holding up client - Meteor.defer -> - # run all post submit server callbacks on post object successively - _.sortBy(callbacks, (callback) -> return callback.priority or RocketChat.promises.priority.MEDIUM).forEach (callback) -> - # console.log(callback.name); - callback item, constant - return - return - else - return item - return diff --git a/packages/rocketchat-lib/lib/promises.js b/packages/rocketchat-lib/lib/promises.js new file mode 100644 index 0000000000000000000000000000000000000000..1e040f172bc571ac60fcd7fda6f260e2c27fb826 --- /dev/null +++ b/packages/rocketchat-lib/lib/promises.js @@ -0,0 +1,89 @@ + +/* +* Callback hooks provide an easy way to add extra steps to common operations. +* @namespace RocketChat.promises +*/ + +RocketChat.promises = {}; + + +/* +* Callback priorities +*/ + +RocketChat.promises.priority = { + HIGH: -1000, + MEDIUM: 0, + LOW: 1000 +}; + + +/* +* Add a callback function to a hook +* @param {String} hook - The name of the hook +* @param {Function} callback - The callback function +*/ + +RocketChat.promises.add = function(hook, callback, p = RocketChat.promises.priority.MEDIUM, id) { + const priority = !_.isNumber(p) ? RocketChat.promises.priority.MEDIUM : p; + callback.priority = priority; + callback.id = id || Random.id(); + RocketChat.promises[hook] = RocketChat.promises[hook] || []; + if (RocketChat.promises[hook].find(cb => cb.id === callback.id)) { + return; + } + RocketChat.promises[hook].push(callback); +}; + + +/* +* Remove a callback from a hook +* @param {string} hook - The name of the hook +* @param {string} id - The callback's id +*/ + +RocketChat.promises.remove = function(hookName, id) { + RocketChat.promises[hookName] = _.reject(RocketChat.promises[hookName], (callback) => callback.id === id); +}; + + +/* +* Successively run all of a hook's callbacks on an item +* @param {String} hook - The name of the hook +* @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks +* @param {Object} [constant] - An optional constant that will be passed along to each callback +* @returns {Object} Returns the item after it's been through all the callbacks for this hook +*/ + +RocketChat.promises.run = function(hook, item, constant) { + let callbacks = RocketChat.promises[hook]; + if (callbacks == null || callbacks.length === 0) { + return Promise.resolve(item); + } + callbacks = _.sortBy(callbacks, (callback) => callback.priority || RocketChat.promises.priority.MEDIUM); + return callbacks.reduce(function(previousPromise, callback) { + return new Promise(function(resolve, reject) { + return previousPromise.then((result) => callback(result, constant).then(resolve, reject)); + }); + }, Promise.resolve(item)); +}; + + +/* +* Successively run all of a hook's callbacks on an item, in async mode (only works on server) +* @param {String} hook - The name of the hook +* @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks +* @param {Object} [constant] - An optional constant that will be passed along to each callback +*/ + +RocketChat.promises.runAsync = function(hook, item, constant) { + const callbacks = RocketChat.promises[hook]; + if (!Meteor.isServer || callbacks == null || callbacks.length === 0) { + return item; + } + Meteor.defer(() => { + _.sortBy(callbacks, (callback) => callback.priority || RocketChat.promises.priority.MEDIUM).forEach(function(callback) { + callback(item, constant); + }); + }); +}; diff --git a/packages/rocketchat-lib/lib/roomTypesCommon.coffee b/packages/rocketchat-lib/lib/roomTypesCommon.coffee deleted file mode 100644 index a351dc258922d7c20994afec0a29b84a833b54a4..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/lib/roomTypesCommon.coffee +++ /dev/null @@ -1,75 +0,0 @@ -class @roomTypesCommon - roomTypes: {} - roomTypesOrder: [] - mainOrder: 1 - - ### Adds a room type to app - @param identifier An identifier to the room type. If a real room, MUST BE the same of `db.rocketchat_room.t` field, if not, can be null - @param order Order number of the type - @param config - template: template name to render on sideNav - icon: icon class - route: - name: route name - action: route action function - ### - add: (identifier, order, config) -> - unless identifier? - identifier = Random.id() - - if @roomTypes[identifier]? - return false - - if not order? - order = @mainOrder + 10 - @mainOrder += 10 - - # @TODO validate config options - @roomTypesOrder.push - identifier: identifier - order: order - @roomTypes[identifier] = config - - if config.route?.path? and config.route?.name? and config.route?.action? - routeConfig = - name: config.route.name - action: config.route.action - - if Meteor.isClient - routeConfig.triggersExit = [ roomExit ] - - FlowRouter.route config.route.path, routeConfig - - hasCustomLink: (roomType) -> - return @roomTypes[roomType]?.route?.link? - - ### - @param roomType: room type (e.g.: c (for channels), d (for direct channels)) - @param subData: the user's subscription data - ### - getRouteLink: (roomType, subData) -> - unless @roomTypes[roomType]? - return false - - routeData = {} - - if @roomTypes[roomType]?.route?.link? - routeData = @roomTypes[roomType].route.link(subData) - else if subData?.name? - routeData = { name: subData.name } - - return FlowRouter.path @roomTypes[roomType].route.name, routeData - - openRouteLink: (roomType, subData, queryParams) -> - unless @roomTypes[roomType]? - return false - - routeData = {} - - if @roomTypes[roomType]?.route?.link? - routeData = @roomTypes[roomType].route.link(subData) - else if subData?.name? - routeData = { name: subData.name } - - return FlowRouter.go @roomTypes[roomType].route.name, routeData, queryParams - diff --git a/packages/rocketchat-lib/lib/roomTypesCommon.js b/packages/rocketchat-lib/lib/roomTypesCommon.js new file mode 100644 index 0000000000000000000000000000000000000000..1df20edae0b3785391499f4e32996a5c3501bc5b --- /dev/null +++ b/packages/rocketchat-lib/lib/roomTypesCommon.js @@ -0,0 +1,84 @@ +/* globals roomExit*/ +this.roomTypesCommon = class { + constructor() { + this.roomTypes = {}; + this.roomTypesOrder = []; + this.mainOrder = 1; + } + + /* Adds a room type to app + @param identifier An identifier to the room type. If a real room, MUST BE the same of `db.rocketchat_room.t` field, if not, can be null + @param order Order number of the type + @param config + template: template name to render on sideNav + icon: icon class + route: + name: route name + action: route action function + */ + + add(identifier = Random.id(), order, config) { + if (this.roomTypes[identifier] != null) { + return false; + } + if (order == null) { + order = this.mainOrder + 10; + this.mainOrder += 10; + } + this.roomTypesOrder.push({ + identifier, + order + }); + this.roomTypes[identifier] = config; + if (config.route && config.route.path && config.route.name && config.route.action) { + const routeConfig = { + name: config.route.name, + action: config.route.action + }; + if (Meteor.isClient) { + routeConfig.triggersExit = [roomExit]; + } + return FlowRouter.route(config.route.path, routeConfig); + } + } + + hasCustomLink(roomType) { + return this.roomTypes[roomType] && this.roomTypes[roomType].route && this.roomTypes[roomType].route.link != null; + } + + /* + @param roomType: room type (e.g.: c (for channels), d (for direct channels)) + @param subData: the user's subscription data + */ + + getRouteLink(roomType, subData) { + if (this.roomTypes[roomType] == null) { + return false; + } + let routeData = {}; + if (this.roomTypes[roomType] && this.roomTypes[roomType].route && this.roomTypes[roomType].route.link) { + routeData = this.roomTypes[roomType].route.link(subData); + } else if (subData && subData.name) { + routeData = { + name: subData.name + }; + } + return FlowRouter.path(this.roomTypes[roomType].route.name, routeData); + } + + openRouteLink(roomType, subData, queryParams) { + if (this.roomTypes[roomType] == null) { + return false; + } + let routeData = {}; + if (this.roomTypes[roomType] && this.roomTypes[roomType].route && this.roomTypes[roomType].route.link) { + routeData = this.roomTypes[roomType].route.link(subData); + } else if (subData && subData.name) { + routeData = { + name: subData.name + }; + } + return FlowRouter.go(this.roomTypes[roomType].route.name, routeData, queryParams); + } +}; +export default this.roomTypesCommon; diff --git a/packages/rocketchat-lib/lib/settings.coffee b/packages/rocketchat-lib/lib/settings.coffee deleted file mode 100644 index cdc221064b39afb3d27c1cbac64850ee077f4b9f..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/lib/settings.coffee +++ /dev/null @@ -1,83 +0,0 @@ -### -# RocketChat.settings holds all packages settings -# @namespace RocketChat.settings -### -RocketChat.settings = - callbacks: {} - regexCallbacks: {} - ts: new Date - - get: (_id, callback) -> - if callback? - RocketChat.settings.onload _id, callback - if _id is '*' and Meteor.settings? - for key, value of Meteor.settings - callback key, value - return - - if _.isRegExp(_id) - for key, value of Meteor.settings when _id.test(key) - callback key, value - return - - if Meteor.settings?[_id]? - callback _id, Meteor.settings?[_id] - else - if _.isRegExp(_id) - items = [] - for key, value of Meteor.settings when _id.test(key) - items.push - key: key - value: value - return items - - return Meteor.settings?[_id] - - set: (_id, value, callback) -> - Meteor.call 'saveSetting', _id, value, callback - - batchSet: (settings, callback) -> - - # async -> sync - # http://daemon.co.za/2012/04/simple-async-with-only-underscore/ - - save = (setting) -> - return (callback) -> - Meteor.call 'saveSetting', setting._id, setting.value, setting.editor, callback - - actions = _.map settings, (setting) -> save(setting) - _(actions).reduceRight(_.wrap, (err, success) -> return callback err, success)() - - load: (key, value, initialLoad) -> - if RocketChat.settings.callbacks[key]? - for callback in RocketChat.settings.callbacks[key] - callback key, value, initialLoad - - if RocketChat.settings.callbacks['*']? - for callback in RocketChat.settings.callbacks['*'] - callback key, value, initialLoad - - for cbKey, cbValue of RocketChat.settings.regexCallbacks - if cbValue.regex.test(key) - callback(key, value, initialLoad) for callback in cbValue.callbacks - - - onload: (key, callback) -> - # if key is '*' - # for key, value in Meteor.settings - # callback key, value, false - # else if Meteor.settings?[_id]? - # callback key, Meteor.settings[_id], false - - keys = [].concat key - - for k in keys - if _.isRegExp k - RocketChat.settings.regexCallbacks[k.source] ?= { - regex: k - callbacks: [] - } - RocketChat.settings.regexCallbacks[k.source].callbacks.push callback - else - RocketChat.settings.callbacks[k] ?= [] - RocketChat.settings.callbacks[k].push callback diff --git a/packages/rocketchat-lib/lib/settings.js b/packages/rocketchat-lib/lib/settings.js new file mode 100644 index 0000000000000000000000000000000000000000..ecdd2c87c4b3e663c5d6129c4f09352c38c2f161 --- /dev/null +++ b/packages/rocketchat-lib/lib/settings.js @@ -0,0 +1,99 @@ + +/* +* RocketChat.settings holds all packages settings +* @namespace RocketChat.settings +*/ +RocketChat.settings = { + callbacks: {}, + regexCallbacks: {}, + ts: new Date, + get(_id, callback) { + if (callback != null) { + RocketChat.settings.onload(_id, callback); + if (!Meteor.settings) { + return; + } + if (_id === '*') { + return Object.keys(Meteor.settings).forEach(key => { + const value = Meteor.settings[key]; + callback(key, value); + }); + } + if (_.isRegExp(_id) && Meteor.settings) { + return Object.keys(Meteor.settings).forEach(key => { + if (!_id.test(key)) { + return; + } + const value = Meteor.settings[key]; + callback(key, value); + }); + } + return Meteor.settings[_id] != null && callback(_id, Meteor.settings[_id]); + } else { + if (!Meteor.settings) { + return; + } + if (_.isRegExp(_id)) { + return Object.keys(Meteor.settings).reduce((items, key) => { + const value = Meteor.settings[key]; + if (_id.test(key)) { + items.push({ + key, + value + }); + } + return items; + }, []); + } + return Meteor.settings && Meteor.settings[_id]; + } + }, + set(_id, value, callback) { + return Meteor.call('saveSetting', _id, value, callback); + }, + batchSet(settings, callback) { + // async -> sync + // http://daemon.co.za/2012/04/simple-async-with-only-underscore/ + const save = function(setting) { + return function(callback) { + return Meteor.call('saveSetting', setting._id, setting.value, setting.editor, callback); + }; + }; + const actions = _.map(settings, (setting) => save(setting)); + return _(actions).reduceRight(_.wrap, (err, success) => callback(err, success))(); + }, + load(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 => { + const cbValue = RocketChat.settings.regexCallbacks[cbKey]; + if (!cbValue.regex.test(key)) { + return; + } + cbValue.callbacks.forEach(callback => callback(key, value, initialLoad)); + }); + }, + onload(key, callback) { + // if key is '*' + // for key, value in Meteor.settings + // callback key, value, false + // else if Meteor.settings?[_id]? + // callback key, Meteor.settings[_id], false + const keys = [].concat(key); + keys.forEach(k => { + if (_.isRegExp(k)) { + RocketChat.settings.regexCallbacks[name = k.source] = RocketChat.settings.regexCallbacks[name = k.source] || { + regex: k, + callbacks: [] + }; + RocketChat.settings.regexCallbacks[k.source].callbacks.push(callback); + } else { + RocketChat.settings.callbacks[k] = RocketChat.settings.callbacks[k] || []; + RocketChat.settings.callbacks[k].push(callback); + } + }); + } +}; diff --git a/packages/rocketchat-lib/package.js b/packages/rocketchat-lib/package.js index 8902fce8f2590c44633b848429ae383f1a87d275..3d7f8402927a4b2bbaa46da191ba8a3d2d5bcb2f 100644 --- a/packages/rocketchat-lib/package.js +++ b/packages/rocketchat-lib/package.js @@ -52,12 +52,12 @@ Package.onUse(function(api) { // COMMON LIB api.addFiles('lib/getURL.js'); - api.addFiles('lib/settings.coffee'); - api.addFiles('lib/callbacks.coffee'); + api.addFiles('lib/settings.js'); + api.addFiles('lib/callbacks.js'); api.addFiles('lib/fileUploadRestrictions.js'); api.addFiles('lib/placeholders.js'); - api.addFiles('lib/promises.coffee'); - api.addFiles('lib/roomTypesCommon.coffee'); + api.addFiles('lib/promises.js'); + api.addFiles('lib/roomTypesCommon.js'); api.addFiles('lib/slashCommand.js'); api.addFiles('lib/Message.js'); api.addFiles('lib/MessageTypes.js'); @@ -134,6 +134,7 @@ Package.onUse(function(api) { api.addFiles('server/methods/checkRegistrationSecretURL.js', 'server'); api.addFiles('server/methods/cleanChannelHistory.js', 'server'); api.addFiles('server/methods/createChannel.js', 'server'); + api.addFiles('server/methods/createToken.js', 'server'); api.addFiles('server/methods/createPrivateGroup.js', 'server'); api.addFiles('server/methods/deleteMessage.js', 'server'); api.addFiles('server/methods/deleteUserOwnAccount.js', 'server'); @@ -173,29 +174,29 @@ Package.onUse(function(api) { api.addFiles('lib/startup/settingsOnLoadSiteUrl.js'); // CLIENT LIB - api.addFiles('client/Notifications.coffee', 'client'); + api.addFiles('client/Notifications.js', 'client'); api.addFiles('client/OAuthProxy.js', 'client'); api.addFiles('client/lib/TabBar.js', 'client'); api.addFiles('client/lib/RocketChatTabBar.js', 'client'); api.addFiles('client/lib/cachedCollection.js', 'client'); - api.addFiles('client/lib/openRoom.coffee', 'client'); - api.addFiles('client/lib/roomExit.coffee', 'client'); - api.addFiles('client/lib/settings.coffee', 'client'); - api.addFiles('client/lib/roomTypes.coffee', 'client'); + api.addFiles('client/lib/openRoom.js', 'client'); + api.addFiles('client/lib/roomExit.js', 'client'); + api.addFiles('client/lib/settings.js', 'client'); + api.addFiles('client/lib/roomTypes.js', 'client'); api.addFiles('client/lib/userRoles.js', 'client'); api.addFiles('client/lib/Layout.js', 'client'); // CLIENT METHODS - api.addFiles('client/methods/sendMessage.coffee', 'client'); + api.addFiles('client/methods/sendMessage.js', 'client'); api.addFiles('client/AdminBox.js', 'client'); - api.addFiles('client/MessageAction.coffee', 'client'); + api.addFiles('client/MessageAction.js', 'client'); api.addFiles('client/defaultTabBars.js', 'client'); api.addFiles('client/CustomTranslations.js', 'client'); // CLIENT MODELS - api.addFiles('client/models/_Base.coffee', 'client'); - api.addFiles('client/models/Uploads.coffee', 'client'); + api.addFiles('client/models/_Base.js', 'client'); + api.addFiles('client/models/Uploads.js', 'client'); // CLIENT VIEWS api.addFiles('client/views/customFieldsForm.html', 'client'); 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/methods/createToken.js b/packages/rocketchat-lib/server/methods/createToken.js new file mode 100644 index 0000000000000000000000000000000000000000..29879093c1c55e68427b8375a06e39b032d6f73c --- /dev/null +++ b/packages/rocketchat-lib/server/methods/createToken.js @@ -0,0 +1,13 @@ +Meteor.methods({ + createToken(userId) { + if (Meteor.userId() !== userId && !RocketChat.authz.hasPermission(Meteor.userId(), 'user-generate-access-token')) { + throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'createToken' }); + } + const token = Accounts._generateStampedLoginToken(); + Accounts._insertLoginToken(userId, token); + return { + userId, + authToken: token.token + }; + } +}); 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/getRoomRoles.js b/packages/rocketchat-lib/server/methods/getRoomRoles.js index e35fdb3e33e96c1d93fbef85c6a9f653811a6dfe..4c7dc6c105f3e68e7544997fed42056d1bbc89c8 100644 --- a/packages/rocketchat-lib/server/methods/getRoomRoles.js +++ b/packages/rocketchat-lib/server/methods/getRoomRoles.js @@ -3,7 +3,7 @@ Meteor.methods({ check(rid, String); - if (!Meteor.userId() && RocketChat.settings.get('Accounts_AllowAnonymousAccess') === false) { + if (!Meteor.userId() && RocketChat.settings.get('Accounts_AllowAnonymousRead') === false) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getRoomRoles' }); } diff --git a/packages/rocketchat-lib/server/startup/settings.js b/packages/rocketchat-lib/server/startup/settings.js index b67af0cd860ffa1d1d8b1640c0f720b5907c47ec..bd77f407b7d70c0e630c151e1142befee161be11 100644 --- a/packages/rocketchat-lib/server/startup/settings.js +++ b/packages/rocketchat-lib/server/startup/settings.js @@ -8,10 +8,18 @@ RocketChat.settings.add('uniqueID', process.env.DEPLOYMENT_ID || Random.id(), { // if you add a node to the i18n.json with the same setting name but with `_Description` it will automatically work. RocketChat.settings.addGroup('Accounts', function() { - this.add('Accounts_AllowAnonymousAccess', false, { + this.add('Accounts_AllowAnonymousRead', false, { type: 'boolean', public: true }); + this.add('Accounts_AllowAnonymousWrite', false, { + type: 'boolean', + public: true, + enableQuery: { + _id: 'Accounts_AllowAnonymousRead', + value: true + } + }); this.add('Accounts_AllowDeleteOwnAccount', false, { type: 'boolean', 'public': true, @@ -62,7 +70,11 @@ RocketChat.settings.addGroup('Accounts', function() { type: 'boolean', 'public': true }); + this.section('Registration', function() { + this.add('Accounts_DefaultUsernamePrefixSuggestion', 'user', { + type: 'string' + }); this.add('Accounts_RequireNameForSignUp', true, { type: 'boolean', 'public': true @@ -145,6 +157,7 @@ RocketChat.settings.addGroup('Accounts', function() { i18nLabel: 'Custom_Fields' }); }); + this.section('Avatar', function() { this.add('Accounts_AvatarResize', true, { type: 'boolean' @@ -468,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, @@ -478,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-lib/startup/defaultRoomTypes.js b/packages/rocketchat-lib/startup/defaultRoomTypes.js index 692ddb8765284bb94e760d9c2c1c6760df209af1..40fe79c9daf12417d55d6102592f5fc557f7f05b 100644 --- a/packages/rocketchat-lib/startup/defaultRoomTypes.js +++ b/packages/rocketchat-lib/startup/defaultRoomTypes.js @@ -28,7 +28,7 @@ RocketChat.roomTypes.add('c', 10, { }, condition() { - return RocketChat.authz.hasAtLeastOnePermission(['view-c-room', 'view-joined-room']) || RocketChat.settings.get('Accounts_AllowAnonymousAccess') === true; + return RocketChat.authz.hasAtLeastOnePermission(['view-c-room', 'view-joined-room']) || RocketChat.settings.get('Accounts_AllowAnonymousRead') === true; }, showJoinLink(roomId) { diff --git a/packages/rocketchat-livechat/app/.meteor/packages b/packages/rocketchat-livechat/app/.meteor/packages index e8e4d7068409ad119d230806037aeefbd3b29503..e777a77fc027b856e0c3128f4b8034b3089d4457 100644 --- a/packages/rocketchat-livechat/app/.meteor/packages +++ b/packages/rocketchat-livechat/app/.meteor/packages @@ -7,12 +7,12 @@ meteor@1.6.1 webapp@1.3.15 logging@1.1.17 -tracker@1.1.2 +tracker@1.1.3 deps@1.0.12 session@1.1.7 ddp@1.2.5 livedata@1.0.18 -mongo@1.1.16 +mongo@1.1.17 blaze ui spacebars @@ -32,10 +32,10 @@ underscorestring:underscore.string momentjs:moment mizzao:timesync reactive-var@1.0.11 -accounts-password@1.3.5 +accounts-password@1.3.6 tap:i18n smoral:sweetalert -ecmascript@0.7.2 +ecmascript@0.7.3 es5-shim@4.6.15 standard-minifier-css@1.3.4 standard-minifier-js@2.0.0 diff --git a/packages/rocketchat-livechat/app/.meteor/release b/packages/rocketchat-livechat/app/.meteor/release index 605b4e1f03103658e752cf5bad58f52bee90009f..fb6f3bc15e2230094f0495e9d4a67d2bcf537d9d 100644 --- a/packages/rocketchat-livechat/app/.meteor/release +++ b/packages/rocketchat-livechat/app/.meteor/release @@ -1 +1 @@ -METEOR@1.4.4.1 +METEOR@1.4.4.2 diff --git a/packages/rocketchat-livechat/app/.meteor/versions b/packages/rocketchat-livechat/app/.meteor/versions index 8e1dfbd6e4f6eb146dda3f29bbb87c2cba260bb8..5d13dcd9ea34598eaebb66e74e3c25ba977be183 100644 --- a/packages/rocketchat-livechat/app/.meteor/versions +++ b/packages/rocketchat-livechat/app/.meteor/versions @@ -1,5 +1,5 @@ -accounts-base@1.2.16 -accounts-password@1.3.5 +accounts-base@1.2.17 +accounts-password@1.3.6 aldeed:simple-schema@1.5.3 allow-deny@1.0.5 babel-compiler@6.18.2 @@ -45,12 +45,12 @@ meteor@1.6.1 meteorspark:util@0.2.0 minifier-css@1.2.16 minifier-js@2.0.0 -minimongo@1.0.21 +minimongo@1.0.23 mizzao:timesync@0.5.0 modules@0.8.2 modules-runtime@0.7.10 momentjs:moment@2.18.1 -mongo@1.1.16 +mongo@1.1.17 mongo-id@1.0.6 npm-bcrypt@0.9.2 npm-mongo@2.2.24 @@ -80,7 +80,7 @@ templating@1.3.2 templating-compiler@1.3.2 templating-runtime@1.3.2 templating-tools@1.1.2 -tracker@1.1.2 +tracker@1.1.3 ui@1.0.13 underscore@1.0.10 underscorestring:underscore.string@3.3.4 diff --git a/packages/rocketchat-livechat/app/i18n/bg.i18n.json b/packages/rocketchat-livechat/app/i18n/bg.i18n.json index 060a3ee2788019a3cf1c2f2a23b454cfd9ec0b62..2a6ec769e46c9d6270cd01faf807e95888a3a2cd 100644 --- a/packages/rocketchat-livechat/app/i18n/bg.i18n.json +++ b/packages/rocketchat-livechat/app/i18n/bg.i18n.json @@ -1,6 +1,7 @@ { "Are_you_sure_do_you_want_end_this_chat": "Сигурен ли Ñи че иÑкаш да прекратиш този чат?", "Cancel": "Отказ", + "Change": "Промени", "Chat_ended": "Край на чата", "Close_menu": "ЗатварÑне на менюто", "Conversation_finished": "Разговорът завърши", diff --git a/packages/rocketchat-livechat/app/package.json b/packages/rocketchat-livechat/app/package.json index de148775e58610cf02cf0204bbcff6b0465e828f..e0e9add70d3b88c2dc600aa5dc8e000f4fc04ec4 100644 --- a/packages/rocketchat-livechat/app/package.json +++ b/packages/rocketchat-livechat/app/package.json @@ -20,11 +20,11 @@ "email": "support@rocket.chat" }, "dependencies": { - "autolinker": "^1.4.2", - "jquery": "^3.1.1", + "autolinker": "^1.4.3", + "jquery": "^3.2.1", "babel-runtime": "^6.23.0", "bcrypt": "^1.0.2", - "moment": "^2.17.1", + "moment": "^2.18.1", "toastr": "^2.1.2" } } diff --git a/packages/rocketchat-mailer/client/router.coffee b/packages/rocketchat-mailer/client/router.coffee deleted file mode 100644 index 0314ec8fd1ea3959b6e97ec2e2bb4735b93304f6..0000000000000000000000000000000000000000 --- a/packages/rocketchat-mailer/client/router.coffee +++ /dev/null @@ -1,10 +0,0 @@ -FlowRouter.route '/mailer', - name: 'mailer' - action: -> - BlazeLayout.render 'main', {center: 'mailer'} - -FlowRouter.route '/mailer/unsubscribe/:_id/:createdAt', - name: 'mailer-unsubscribe' - action: (params) -> - Meteor.call 'Mailer:unsubscribe', params._id, params.createdAt - BlazeLayout.render 'mailerUnsubscribe' diff --git a/packages/rocketchat-mailer/client/router.js b/packages/rocketchat-mailer/client/router.js new file mode 100644 index 0000000000000000000000000000000000000000..e2a9e67280355aa5c241a0271f07314ed5d329a1 --- /dev/null +++ b/packages/rocketchat-mailer/client/router.js @@ -0,0 +1,16 @@ +FlowRouter.route('/mailer', { + name: 'mailer', + action() { + return BlazeLayout.render('main', { + center: 'mailer' + }); + } +}); + +FlowRouter.route('/mailer/unsubscribe/:_id/:createdAt', { + name: 'mailer-unsubscribe', + action(params) { + Meteor.call('Mailer:unsubscribe', params._id, params.createdAt); + return BlazeLayout.render('mailerUnsubscribe'); + } +}); diff --git a/packages/rocketchat-mailer/client/startup.coffee b/packages/rocketchat-mailer/client/startup.coffee deleted file mode 100644 index 9b4dd0847261dc4f7f71d1158fd0f217e0814d35..0000000000000000000000000000000000000000 --- a/packages/rocketchat-mailer/client/startup.coffee +++ /dev/null @@ -1,5 +0,0 @@ -RocketChat.AdminBox.addOption - href: 'mailer' - i18nLabel: 'Mailer' - permissionGranted: -> - return RocketChat.authz.hasAllPermission('access-mailer') diff --git a/packages/rocketchat-mailer/client/startup.js b/packages/rocketchat-mailer/client/startup.js new file mode 100644 index 0000000000000000000000000000000000000000..728ebddf8c1014144472793f98e99fdffaf86368 --- /dev/null +++ b/packages/rocketchat-mailer/client/startup.js @@ -0,0 +1,7 @@ +RocketChat.AdminBox.addOption({ + href: 'mailer', + i18nLabel: 'Mailer', + permissionGranted() { + return RocketChat.authz.hasAllPermission('access-mailer'); + } +}); diff --git a/packages/rocketchat-mailer/client/views/mailer.coffee b/packages/rocketchat-mailer/client/views/mailer.coffee deleted file mode 100644 index 9f484b5a5e345d353375d478749152ae0a9e704b..0000000000000000000000000000000000000000 --- a/packages/rocketchat-mailer/client/views/mailer.coffee +++ /dev/null @@ -1,25 +0,0 @@ -import toastr from 'toastr' -Template.mailer.helpers - fromEmail: -> - return RocketChat.settings.get 'From_Email' - -Template.mailer.events - 'click .send': (e, t) -> - e.preventDefault() - from = $(t.find('[name=from]')).val() - subject = $(t.find('[name=subject]')).val() - body = $(t.find('[name=body]')).val() - dryrun = $(t.find('[name=dryrun]:checked')).val() - query = $(t.find('[name=query]')).val() - - unless from - toastr.error TAPi18n.__('error-invalid-from-address') - return - - if body.indexOf('[unsubscribe]') is -1 - toastr.error TAPi18n.__('error-missing-unsubscribe-link') - return - - Meteor.call 'Mailer.sendMail', from, subject, body, dryrun, query, (err) -> - return handleError(err) if err - toastr.success TAPi18n.__('The_emails_are_being_sent') diff --git a/packages/rocketchat-mailer/client/views/mailer.js b/packages/rocketchat-mailer/client/views/mailer.js new file mode 100644 index 0000000000000000000000000000000000000000..731df5aaa3ec0f6fb09ce42645818ac09eb48286 --- /dev/null +++ b/packages/rocketchat-mailer/client/views/mailer.js @@ -0,0 +1,31 @@ +import toastr from 'toastr'; +Template.mailer.helpers({ + fromEmail() { + return RocketChat.settings.get('From_Email'); + } +}); + +Template.mailer.events({ + 'click .send'(e, t) { + e.preventDefault(); + const from = $(t.find('[name=from]')).val(); + const subject = $(t.find('[name=subject]')).val(); + const body = $(t.find('[name=body]')).val(); + const dryrun = $(t.find('[name=dryrun]:checked')).val(); + const query = $(t.find('[name=query]')).val(); + if (!from) { + toastr.error(TAPi18n.__('error-invalid-from-address')); + return; + } + if (body.indexOf('[unsubscribe]') === -1) { + toastr.error(TAPi18n.__('error-missing-unsubscribe-link')); + return; + } + return Meteor.call('Mailer.sendMail', from, subject, body, dryrun, query, function(err) { + if (err) { + return handleError(err); + } + return toastr.success(TAPi18n.__('The_emails_are_being_sent')); + }); + } +}); diff --git a/packages/rocketchat-mailer/client/views/mailerUnsubscribe.coffee b/packages/rocketchat-mailer/client/views/mailerUnsubscribe.coffee deleted file mode 100644 index d8a7a1840e01af98ba3f803df8ee52f8c724aa63..0000000000000000000000000000000000000000 --- a/packages/rocketchat-mailer/client/views/mailerUnsubscribe.coffee +++ /dev/null @@ -1,2 +0,0 @@ -Template.mailerUnsubscribe.onRendered -> - $('#initial-page-loading').remove() diff --git a/packages/rocketchat-mailer/client/views/mailerUnsubscribe.js b/packages/rocketchat-mailer/client/views/mailerUnsubscribe.js new file mode 100644 index 0000000000000000000000000000000000000000..8012c4deb519cb4039414465ff3b241eeee98699 --- /dev/null +++ b/packages/rocketchat-mailer/client/views/mailerUnsubscribe.js @@ -0,0 +1,3 @@ +Template.mailerUnsubscribe.onRendered(function() { + return $('#initial-page-loading').remove(); +}); diff --git a/packages/rocketchat-mailer/lib/Mailer.coffee b/packages/rocketchat-mailer/lib/Mailer.coffee deleted file mode 100644 index 8158d6d741d11f0781290697aa859da139136b1b..0000000000000000000000000000000000000000 --- a/packages/rocketchat-mailer/lib/Mailer.coffee +++ /dev/null @@ -1 +0,0 @@ -Mailer = {} diff --git a/packages/rocketchat-mailer/lib/Mailer.js b/packages/rocketchat-mailer/lib/Mailer.js new file mode 100644 index 0000000000000000000000000000000000000000..e7e30cfebe4a4c3d77958073b2cd07714b135695 --- /dev/null +++ b/packages/rocketchat-mailer/lib/Mailer.js @@ -0,0 +1 @@ +Mailer = {};//eslint-disable-line diff --git a/packages/rocketchat-mailer/package.js b/packages/rocketchat-mailer/package.js index 30da5c981e53b3f17c01c813ba9681bd096a3d18..ffe0a7e317dc37cf5ddc210c3fa700448494c03f 100644 --- a/packages/rocketchat-mailer/package.js +++ b/packages/rocketchat-mailer/package.js @@ -7,7 +7,6 @@ Package.describe({ Package.onUse(function(api) { api.use([ 'ecmascript', - 'coffeescript', 'ddp-rate-limiter', 'kadira:flow-router', 'rocketchat:lib', @@ -16,24 +15,24 @@ Package.onUse(function(api) { api.use('templating', 'client'); - api.addFiles('lib/Mailer.coffee'); + api.addFiles('lib/Mailer.js'); api.addFiles([ - 'client/startup.coffee', - 'client/router.coffee', + 'client/startup.js', + 'client/router.js', 'client/views/mailer.html', - 'client/views/mailer.coffee', + 'client/views/mailer.js', 'client/views/mailerUnsubscribe.html', - 'client/views/mailerUnsubscribe.coffee' + 'client/views/mailerUnsubscribe.js' ], 'client'); api.addFiles([ - 'server/startup.coffee', - 'server/models/Users.coffee', - 'server/functions/sendMail.coffee', - 'server/functions/unsubscribe.coffee', - 'server/methods/sendMail.coffee', - 'server/methods/unsubscribe.coffee' + 'server/startup.js', + 'server/models/Users.js', + 'server/functions/sendMail.js', + 'server/functions/unsubscribe.js', + 'server/methods/sendMail.js', + 'server/methods/unsubscribe.js' ], 'server'); api.export('Mailer'); diff --git a/packages/rocketchat-mailer/server/functions/sendMail.coffee b/packages/rocketchat-mailer/server/functions/sendMail.coffee deleted file mode 100644 index d6fa86beb87f9ccf0f3f069139e278970520f7ad..0000000000000000000000000000000000000000 --- a/packages/rocketchat-mailer/server/functions/sendMail.coffee +++ /dev/null @@ -1,63 +0,0 @@ -Mailer.sendMail = (from, subject, body, dryrun, query) -> - - rfcMailPatternWithName = /^(?:.*<)?([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])?)*)(?:>?)$/ - # 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])?)*$/ - - unless rfcMailPatternWithName.test from - throw new Meteor.Error 'error-invalid-from-address', 'Invalid from address', { function: 'Mailer.sendMail' } - - if body.indexOf('[unsubscribe]') is -1 - throw new Meteor.Error 'error-missing-unsubscribe-link', 'You must provide the [unsubscribe] link.', { function: 'Mailer.sendMail' } - - header = RocketChat.placeholders.replace(RocketChat.settings.get('Email_Header') || ''); - footer = RocketChat.placeholders.replace(RocketChat.settings.get('Email_Footer') || ''); - - userQuery = { "mailer.unsubscribed": { $exists: 0 } } - if query - userQuery = { $and: [ userQuery, EJSON.parse(query) ] } - - if dryrun - Meteor.users.find({ "emails.address": from }).forEach (user) -> - # Meteor.users.find({ "username": /\.rocket\.team/ }).forEach (user) -> - email = user.emails?[0]?.address - - html = RocketChat.placeholders.replace(body, { - unsubscribe: Meteor.absoluteUrl(FlowRouter.path('mailer/unsubscribe/:_id/:createdAt', { _id: user._id, createdAt: user.createdAt.getTime() })), - name: user.name, - email: email - }); - - email = "#{user.name} <#{email}>" - - if rfcMailPatternWithName.test email - Meteor.defer -> - Email.send - to: email - from: from - subject: subject - html: header + html + footer - - console.log 'Sending email to ' + email - - else - Meteor.users.find({ "mailer.unsubscribed": { $exists: 0 } }).forEach (user) -> - # Meteor.users.find({ "username": /\.rocket\.team/ }).forEach (user) -> - email = user.emails?[0]?.address - - html = RocketChat.placeholders.replace(body, { - unsubscribe: Meteor.absoluteUrl(FlowRouter.path('mailer/unsubscribe/:_id/:createdAt', { _id: user._id, createdAt: user.createdAt.getTime() })), - name: user.name, - email: email - }); - - email = "#{user.name} <#{email}>" - - if rfcMailPatternWithName.test email - Meteor.defer -> - Email.send - to: email - from: from - subject: subject - html: header + html + footer - - console.log 'Sending email to ' + email diff --git a/packages/rocketchat-mailer/server/functions/sendMail.js b/packages/rocketchat-mailer/server/functions/sendMail.js new file mode 100644 index 0000000000000000000000000000000000000000..b8cd2e9b49bec5dabcabc91a8155bd27505ecc64 --- /dev/null +++ b/packages/rocketchat-mailer/server/functions/sendMail.js @@ -0,0 +1,80 @@ +/*globals Mailer */ +Mailer.sendMail = function(from, subject, body, dryrun, query) { + + const rfcMailPatternWithName = /^(?:.*<)?([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])?)*)(?:>?)$/; + if (!rfcMailPatternWithName.test(from)) { + throw new Meteor.Error('error-invalid-from-address', 'Invalid from address', { + 'function': 'Mailer.sendMail' + }); + } + if (body.indexOf('[unsubscribe]') === -1) { + throw new Meteor.Error('error-missing-unsubscribe-link', 'You must provide the [unsubscribe] link.', { + 'function': 'Mailer.sendMail' + }); + } + const header = RocketChat.placeholders.replace(RocketChat.settings.get('Email_Header') || ''); + const footer = RocketChat.placeholders.replace(RocketChat.settings.get('Email_Footer') || ''); + + let userQuery = { 'mailer.unsubscribed': { $exists: 0 } }; + if (query) { + userQuery = { $and: [ userQuery, EJSON.parse(query) ] }; + } + + if (dryrun) { + return Meteor.users.find({ + 'emails.address': from + }).forEach((user) => { + let email = undefined; + if (user.emails && user.emails[0] && user.emails[0].address) { + email = user.emails[0].address; + } + const html = RocketChat.placeholders.replace(body, { + unsubscribe: Meteor.absoluteUrl(FlowRouter.path('mailer/unsubscribe/:_id/:createdAt', { + _id: user._id, + createdAt: user.createdAt.getTime() + })), + name: user.name, + email + }); + email = `${ user.name } <${ email }>`; + if (rfcMailPatternWithName.test(email)) { + Meteor.defer(function() { + return Email.send({ + to: email, + from, + subject, + html: header + html + footer + }); + }); + return console.log(`Sending email to ${ email }`); + } + }); + } else { + return Meteor.users.find(userQuery).forEach(function(user) { + let email = undefined; + if (user.emails && user.emails[0] && user.emails[0].address) { + email = user.emails[0].address; + } + const html = RocketChat.placeholders.replace(body, { + unsubscribe: Meteor.absoluteUrl(FlowRouter.path('mailer/unsubscribe/:_id/:createdAt', { + _id: user._id, + createdAt: user.createdAt.getTime() + })), + name: user.name, + email + }); + email = `${ user.name } <${ email }>`; + if (rfcMailPatternWithName.test(email)) { + Meteor.defer(function() { + return Email.send({ + to: email, + from, + subject, + html: header + html + footer + }); + }); + return console.log(`Sending email to ${ email }`); + } + }); + } +}; diff --git a/packages/rocketchat-mailer/server/functions/unsubscribe.coffee b/packages/rocketchat-mailer/server/functions/unsubscribe.coffee deleted file mode 100644 index 955a0ec95fb313f980d6b2afc0f7d06fa04e592d..0000000000000000000000000000000000000000 --- a/packages/rocketchat-mailer/server/functions/unsubscribe.coffee +++ /dev/null @@ -1,4 +0,0 @@ -Mailer.unsubscribe = (_id, createdAt) -> - if _id and createdAt - return RocketChat.models.Users.RocketMailUnsubscribe(_id, createdAt) == 1 - return false diff --git a/packages/rocketchat-mailer/server/functions/unsubscribe.js b/packages/rocketchat-mailer/server/functions/unsubscribe.js new file mode 100644 index 0000000000000000000000000000000000000000..4e0bbc838ad92425c3994f109612b3f8fbb88f63 --- /dev/null +++ b/packages/rocketchat-mailer/server/functions/unsubscribe.js @@ -0,0 +1,7 @@ +/* globals Mailer */ +Mailer.unsubscribe = function(_id, createdAt) { + if (_id && createdAt) { + return RocketChat.models.Users.rocketMailUnsubscribe(_id, createdAt) === 1; + } + return false; +}; diff --git a/packages/rocketchat-mailer/server/methods/sendMail.coffee b/packages/rocketchat-mailer/server/methods/sendMail.coffee deleted file mode 100644 index 17b8946353f5aea16236118bd2cd5f5c6ddbd92c..0000000000000000000000000000000000000000 --- a/packages/rocketchat-mailer/server/methods/sendMail.coffee +++ /dev/null @@ -1,16 +0,0 @@ -Meteor.methods - 'Mailer.sendMail': (from, subject, body, dryrun, query) -> - if not Meteor.userId() - throw new Meteor.Error 'error-invalid-user', "Invalid user", { method: 'Mailer.sendMail' } - - unless RocketChat.authz.hasRole( Meteor.userId(), 'admin') is true - throw new Meteor.Error 'error-not-allowed', 'Not allowed', { method: 'Mailer.sendMail' } - - return Mailer.sendMail from, subject, body, dryrun, query - -# Limit setting username once per minute -# DDPRateLimiter.addRule -# type: 'method' -# name: 'Mailer.sendMail' -# connectionId: -> return true -# , 1, 60000 diff --git a/packages/rocketchat-mailer/server/methods/sendMail.js b/packages/rocketchat-mailer/server/methods/sendMail.js new file mode 100644 index 0000000000000000000000000000000000000000..c45b394b01b277c6e692302b196d8a634f1b0005 --- /dev/null +++ b/packages/rocketchat-mailer/server/methods/sendMail.js @@ -0,0 +1,25 @@ +/*globals Mailer */ +Meteor.methods({ + 'Mailer.sendMail'(from, subject, body, dryrun, query) { + const userId = Meteor.userId(); + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'Mailer.sendMail' + }); + } + if (RocketChat.authz.hasRole(userId, 'admin') !== true) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'Mailer.sendMail' + }); + } + return Mailer.sendMail(from, subject, body, dryrun, query); + } +}); + + +//Limit setting username once per minute +//DDPRateLimiter.addRule +// type: 'method' +// name: 'Mailer.sendMail' +// connectionId: -> return true +// , 1, 60000 diff --git a/packages/rocketchat-mailer/server/methods/unsubscribe.coffee b/packages/rocketchat-mailer/server/methods/unsubscribe.coffee deleted file mode 100644 index 88e5d7a1494b65d965f9247aa4de02b87aa2efa0..0000000000000000000000000000000000000000 --- a/packages/rocketchat-mailer/server/methods/unsubscribe.coffee +++ /dev/null @@ -1,10 +0,0 @@ -Meteor.methods - 'Mailer:unsubscribe': (_id, createdAt) -> - return Mailer.unsubscribe _id, createdAt - -# Limit setting username once per minute -DDPRateLimiter.addRule - type: 'method' - name: 'Mailer:unsubscribe' - connectionId: -> return true -, 1, 60000 diff --git a/packages/rocketchat-mailer/server/methods/unsubscribe.js b/packages/rocketchat-mailer/server/methods/unsubscribe.js new file mode 100644 index 0000000000000000000000000000000000000000..914c80e7b143456edb6996b0cd4f181a5b2b7291 --- /dev/null +++ b/packages/rocketchat-mailer/server/methods/unsubscribe.js @@ -0,0 +1,14 @@ +/*globals Mailer */ +Meteor.methods({ + 'Mailer:unsubscribe'(_id, createdAt) { + return Mailer.unsubscribe(_id, createdAt); + } +}); + +DDPRateLimiter.addRule({ + type: 'method', + name: 'Mailer:unsubscribe', + connectionId() { + return true; + } +}, 1, 60000); diff --git a/packages/rocketchat-mailer/server/models/Users.coffee b/packages/rocketchat-mailer/server/models/Users.coffee deleted file mode 100644 index 7622b890d4809369954c0e09763e1ecbce80b329..0000000000000000000000000000000000000000 --- a/packages/rocketchat-mailer/server/models/Users.coffee +++ /dev/null @@ -1,17 +0,0 @@ -# Extends model Users - -RocketChat.models.Users.RocketMailUnsubscribe = (_id, createdAt) -> - - query = - _id: _id - createdAt: new Date(parseInt createdAt) - - update = - $set: - "mailer.unsubscribed": true - - affectedRows = @update query, update - - console.log '[Mailer:Unsubscribe]', _id, createdAt, new Date(parseInt createdAt), affectedRows - - return affectedRows diff --git a/packages/rocketchat-mailer/server/models/Users.js b/packages/rocketchat-mailer/server/models/Users.js new file mode 100644 index 0000000000000000000000000000000000000000..9649db1972ffecb03a5db198dbf31a0ca4db173c --- /dev/null +++ b/packages/rocketchat-mailer/server/models/Users.js @@ -0,0 +1,14 @@ +RocketChat.models.Users.rocketMailUnsubscribe = function(_id, createdAt) { + const query = { + _id, + createdAt: new Date(parseInt(createdAt)) + }; + const update = { + $set: { + 'mailer.unsubscribed': true + } + }; + const affectedRows = this.update(query, update); + console.log('[Mailer:Unsubscribe]', _id, createdAt, new Date(parseInt(createdAt)), affectedRows); + return affectedRows; +}; diff --git a/packages/rocketchat-mailer/server/startup.coffee b/packages/rocketchat-mailer/server/startup.coffee deleted file mode 100644 index acf55f73c49df2b434547e248b79b422e084518c..0000000000000000000000000000000000000000 --- a/packages/rocketchat-mailer/server/startup.coffee +++ /dev/null @@ -1,2 +0,0 @@ -Meteor.startup -> - RocketChat.models.Permissions.upsert( 'access-mailer', { $setOnInsert : { _id: 'access-mailer', roles : ['admin'] } }) diff --git a/packages/rocketchat-mailer/server/startup.js b/packages/rocketchat-mailer/server/startup.js new file mode 100644 index 0000000000000000000000000000000000000000..e1ef976a18fd59c81f886ca65c3b5111a2acbc9d --- /dev/null +++ b/packages/rocketchat-mailer/server/startup.js @@ -0,0 +1,8 @@ +Meteor.startup(function() { + return RocketChat.models.Permissions.upsert('access-mailer', { + $setOnInsert: { + _id: 'access-mailer', + roles: ['admin'] + } + }); +}); diff --git a/packages/rocketchat-mentions-flextab/client/actionButton.coffee b/packages/rocketchat-mentions-flextab/client/actionButton.coffee deleted file mode 100644 index 84d350c96809238dd64ee447fa82b9fb6f70dc01..0000000000000000000000000000000000000000 --- a/packages/rocketchat-mentions-flextab/client/actionButton.coffee +++ /dev/null @@ -1,17 +0,0 @@ -Meteor.startup -> - RocketChat.MessageAction.addButton - id: 'jump-to-message' - icon: 'icon-right-hand' - i18nLabel: 'Jump_to_message' - context: [ - 'mentions' - ] - action: (event, instance) -> - message = @_arguments[1] - RocketChat.MessageAction.hideDropDown() - RoomHistoryManager.getSurroundingMessages(message, 50) - - validation: (message) -> - return message.mentionedList is true - - order: 100 diff --git a/packages/rocketchat-mentions-flextab/client/actionButton.js b/packages/rocketchat-mentions-flextab/client/actionButton.js new file mode 100644 index 0000000000000000000000000000000000000000..9f23356936370ed44b1b37934e9976b276d4e322 --- /dev/null +++ b/packages/rocketchat-mentions-flextab/client/actionButton.js @@ -0,0 +1,17 @@ +Meteor.startup(function() { + return RocketChat.MessageAction.addButton({ + id: 'jump-to-message', + icon: 'icon-right-hand', + i18nLabel: 'Jump_to_message', + context: ['mentions'], + action() { + const message = this._arguments[1]; + RocketChat.MessageAction.hideDropDown(); + return RoomHistoryManager.getSurroundingMessages(message, 50); + }, + validation(message) { + return message.mentionedList === true; + }, + order: 100 + }); +}); diff --git a/packages/rocketchat-mentions-flextab/client/lib/MentionedMessage.coffee b/packages/rocketchat-mentions-flextab/client/lib/MentionedMessage.coffee deleted file mode 100644 index 98325aa643a805811600daaba200572b90cd1678..0000000000000000000000000000000000000000 --- a/packages/rocketchat-mentions-flextab/client/lib/MentionedMessage.coffee +++ /dev/null @@ -1 +0,0 @@ -@MentionedMessage = new Mongo.Collection 'rocketchat_mentioned_message' diff --git a/packages/rocketchat-mentions-flextab/client/lib/MentionedMessage.js b/packages/rocketchat-mentions-flextab/client/lib/MentionedMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..fffbdd257ebaf74d11c554db5d620bca337c7315 --- /dev/null +++ b/packages/rocketchat-mentions-flextab/client/lib/MentionedMessage.js @@ -0,0 +1 @@ +this.MentionedMessage = new Mongo.Collection('rocketchat_mentioned_message'); diff --git a/packages/rocketchat-mentions-flextab/client/tabBar.coffee b/packages/rocketchat-mentions-flextab/client/tabBar.js similarity index 64% rename from packages/rocketchat-mentions-flextab/client/tabBar.coffee rename to packages/rocketchat-mentions-flextab/client/tabBar.js index 70b75724e52f8a349f8e8d369f5741695b9e2dd1..272b29ede3151612408c01851b246753fa1931ff 100644 --- a/packages/rocketchat-mentions-flextab/client/tabBar.coffee +++ b/packages/rocketchat-mentions-flextab/client/tabBar.js @@ -1,9 +1,10 @@ -Meteor.startup -> - RocketChat.TabBar.addButton({ +Meteor.startup(function() { + return RocketChat.TabBar.addButton({ groups: ['channel', 'group'], id: 'mentions', i18nTitle: 'Mentions', icon: 'icon-at', template: 'mentionsFlexTab', order: 3 - }) + }); +}); diff --git a/packages/rocketchat-mentions-flextab/client/views/mentionsFlexTab.coffee b/packages/rocketchat-mentions-flextab/client/views/mentionsFlexTab.coffee deleted file mode 100644 index 0d6fab788374077235d6cc1416dedd939ae8e9ff..0000000000000000000000000000000000000000 --- a/packages/rocketchat-mentions-flextab/client/views/mentionsFlexTab.coffee +++ /dev/null @@ -1,39 +0,0 @@ -Template.mentionsFlexTab.helpers - hasMessages: -> - return MentionedMessage.find({ rid: @rid }, { sort: { ts: -1 } }).count() > 0 - - messages: -> - return MentionedMessage.find { rid: @rid }, { sort: { ts: -1 } } - - message: -> - return _.extend(this, { customClass: 'mentions' }) - - hasMore: -> - return Template.instance().hasMore.get() - -Template.mentionsFlexTab.onCreated -> - @hasMore = new ReactiveVar true - @limit = new ReactiveVar 50 - @autorun => - @subscribe 'mentionedMessages', @data.rid, @limit.get(), => - if MentionedMessage.find({ rid: @data.rid }).count() < @limit.get() - @hasMore.set false - -Template.mentionsFlexTab.events - 'click .message-cog': (e, t) -> - e.stopPropagation() - e.preventDefault() - message_id = $(e.currentTarget).closest('.message').attr('id') - RocketChat.MessageAction.hideDropDown() - t.$("\##{message_id} .message-dropdown").remove() - message = MentionedMessage.findOne message_id - actions = RocketChat.MessageAction.getButtons message, 'mentions' - el = Blaze.toHTMLWithData Template.messageDropdown, { actions: actions } - t.$("\##{message_id} .message-cog-container").append el - dropDown = t.$("\##{message_id} .message-dropdown") - dropDown.show() - - 'scroll .content': _.throttle (e, instance) -> - if e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight && instance.hasMore.get() - instance.limit.set(instance.limit.get() + 50) - , 200 diff --git a/packages/rocketchat-mentions-flextab/client/views/mentionsFlexTab.js b/packages/rocketchat-mentions-flextab/client/views/mentionsFlexTab.js new file mode 100644 index 0000000000000000000000000000000000000000..a929120d1595007963978d08724c5b76e6942c2c --- /dev/null +++ b/packages/rocketchat-mentions-flextab/client/views/mentionsFlexTab.js @@ -0,0 +1,65 @@ +/*globals MentionedMessage */ +Template.mentionsFlexTab.helpers({ + hasMessages() { + return MentionedMessage.find({ + rid: this.rid + }, { + sort: { + ts: -1 + } + }).count() > 0; + }, + messages() { + return MentionedMessage.find({ + rid: this.rid + }, { + sort: { + ts: -1 + } + }); + }, + message() { + return _.extend(this, { + customClass: 'mentions' + }); + }, + hasMore() { + return Template.instance().hasMore.get(); + } +}); + +Template.mentionsFlexTab.onCreated(function() { + this.hasMore = new ReactiveVar(true); + this.limit = new ReactiveVar(50); + return this.autorun(() => { + const mentionedMessageFind = MentionedMessage.find({ rid: this.data.rid }); + return this.subscribe('mentionedMessages', this.data.rid, this.limit.get(), () => { + if (mentionedMessageFind.count() < this.limit.get()) { + return this.hasMore.set(false); + } + }); + }); +}); + +Template.mentionsFlexTab.events({ + 'click .message-cog'(e, t) { + e.stopPropagation(); + e.preventDefault(); + const message_id = $(e.currentTarget).closest('.message').attr('id'); + RocketChat.MessageAction.hideDropDown(); + t.$(`\#${ message_id } .message-dropdown`).remove(); + const message = MentionedMessage.findOne(message_id); + const actions = RocketChat.MessageAction.getButtons(message, 'mentions'); + const el = Blaze.toHTMLWithData(Template.messageDropdown, { + actions + }); + t.$(`\#${ message_id } .message-cog-container`).append(el); + const dropDown = t.$(`\#${ message_id } .message-dropdown`); + return dropDown.show(); + }, + 'scroll .content': _.throttle(function(e, instance) { + if (e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight && instance.hasMore.get()) { + return instance.limit.set(instance.limit.get() + 50); + } + }, 200) +}); diff --git a/packages/rocketchat-mentions-flextab/package.js b/packages/rocketchat-mentions-flextab/package.js index 127ecc9f6997a23195a2c4076ebd7f8c0849cefe..4f38223f73ae80521663cdd2fc775416404359c5 100644 --- a/packages/rocketchat-mentions-flextab/package.js +++ b/packages/rocketchat-mentions-flextab/package.js @@ -9,7 +9,6 @@ Package.onUse(function(api) { api.use([ 'mongo', 'ecmascript', - 'coffeescript', 'underscore', 'less', 'rocketchat:lib' @@ -18,15 +17,15 @@ Package.onUse(function(api) { api.use('templating', 'client'); api.addFiles([ - 'client/lib/MentionedMessage.coffee', + 'client/lib/MentionedMessage.js', 'client/views/stylesheets/mentionsFlexTab.less', 'client/views/mentionsFlexTab.html', - 'client/views/mentionsFlexTab.coffee', - 'client/actionButton.coffee', - 'client/tabBar.coffee' + 'client/views/mentionsFlexTab.js', + 'client/actionButton.js', + 'client/tabBar.js' ], 'client'); api.addFiles([ - 'server/publications/mentionedMessages.coffee' + 'server/publications/mentionedMessages.js' ], 'server'); }); diff --git a/packages/rocketchat-mentions-flextab/server/publications/mentionedMessages.coffee b/packages/rocketchat-mentions-flextab/server/publications/mentionedMessages.coffee deleted file mode 100644 index 72a557e2df8547cc83ad88e5db2f1d8ed441f68e..0000000000000000000000000000000000000000 --- a/packages/rocketchat-mentions-flextab/server/publications/mentionedMessages.coffee +++ /dev/null @@ -1,25 +0,0 @@ -Meteor.publish 'mentionedMessages', (rid, limit=50) -> - unless this.userId - return this.ready() - - publication = @ - - user = RocketChat.models.Users.findOneById this.userId - unless user - return this.ready() - - cursorHandle = RocketChat.models.Messages.findVisibleByMentionAndRoomId(user.username, rid, { sort: { ts: -1 }, limit: limit }).observeChanges - added: (_id, record) -> - record.mentionedList = true - publication.added('rocketchat_mentioned_message', _id, record) - - changed: (_id, record) -> - record.mentionedList = true - publication.changed('rocketchat_mentioned_message', _id, record) - - removed: (_id) -> - publication.removed('rocketchat_mentioned_message', _id) - - @ready() - @onStop -> - cursorHandle.stop() diff --git a/packages/rocketchat-mentions-flextab/server/publications/mentionedMessages.js b/packages/rocketchat-mentions-flextab/server/publications/mentionedMessages.js new file mode 100644 index 0000000000000000000000000000000000000000..994454f9fd86b080875e92bc8118277975df9934 --- /dev/null +++ b/packages/rocketchat-mentions-flextab/server/publications/mentionedMessages.js @@ -0,0 +1,32 @@ +Meteor.publish('mentionedMessages', function(rid, limit = 50) { + if (!this.userId) { + return this.ready(); + } + const publication = this; + const user = RocketChat.models.Users.findOneById(this.userId); + if (!user) { + return this.ready(); + } + const cursorHandle = RocketChat.models.Messages.findVisibleByMentionAndRoomId(user.username, rid, { + sort: { + ts: -1 + }, + limit + }).observeChanges({ + added(_id, record) { + record.mentionedList = true; + return publication.added('rocketchat_mentioned_message', _id, record); + }, + changed(_id, record) { + record.mentionedList = true; + return publication.changed('rocketchat_mentioned_message', _id, record); + }, + removed(_id) { + return publication.removed('rocketchat_mentioned_message', _id); + } + }); + this.ready(); + return this.onStop(function() { + return cursorHandle.stop(); + }); +}); diff --git a/packages/rocketchat-message-star/client/actionButton.coffee b/packages/rocketchat-message-star/client/actionButton.coffee deleted file mode 100644 index c0b873a6db8c7d4ffcd1649287db1d4762bd4389..0000000000000000000000000000000000000000 --- a/packages/rocketchat-message-star/client/actionButton.coffee +++ /dev/null @@ -1,83 +0,0 @@ -import toastr from 'toastr' -Meteor.startup -> - RocketChat.MessageAction.addButton - id: 'star-message' - icon: 'icon-star-empty' - i18nLabel: 'Star_Message' - context: [ - 'starred' - 'message' - 'message-mobile' - ] - action: (event, instance) -> - message = @_arguments[1] - message.starred = Meteor.userId() - Meteor.call 'starMessage', message, (error, result) -> - if error - return handleError(error) - validation: (message) -> - if not RocketChat.models.Subscriptions.findOne({ rid: message.rid })? - return false - - return RocketChat.settings.get('Message_AllowStarring') and not message.starred - order: 10 - - RocketChat.MessageAction.addButton - id: 'unstar-message' - icon: 'icon-star' - i18nLabel: 'Unstar_Message' - context: [ - 'starred' - 'message' - 'message-mobile' - ] - action: (event, instance) -> - message = @_arguments[1] - message.starred = false - Meteor.call 'starMessage', message, (error, result) -> - if error - return handleError(error) - validation: (message) -> - if not RocketChat.models.Subscriptions.findOne({ rid: message.rid })? - return false - - return RocketChat.settings.get('Message_AllowStarring') and message.starred - order: 10 - - RocketChat.MessageAction.addButton - id: 'jump-to-star-message' - icon: 'icon-right-hand' - i18nLabel: 'Jump_to_message' - context: [ - 'starred' - ] - action: (event, instance) -> - message = @_arguments[1] - RocketChat.MessageAction.hideDropDown() - RoomHistoryManager.getSurroundingMessages(message, 50) - validation: (message) -> - if not RocketChat.models.Subscriptions.findOne({ rid: message.rid })? - return false - - return true - order: 100 - - RocketChat.MessageAction.addButton - id: 'permalink-star' - icon: 'icon-link' - i18nLabel: 'Permalink' - classes: 'clipboard' - context: [ - 'starred' - ] - action: (event, instance) -> - message = @_arguments[1] - RocketChat.MessageAction.hideDropDown() - $(event.currentTarget).attr('data-clipboard-text', RocketChat.MessageAction.getPermaLink(message._id)); - toastr.success(TAPi18n.__('Copied')) - validation: (message) -> - if not RocketChat.models.Subscriptions.findOne({ rid: message.rid })? - return false - - return true - order: 101 diff --git a/packages/rocketchat-message-star/client/actionButton.js b/packages/rocketchat-message-star/client/actionButton.js new file mode 100644 index 0000000000000000000000000000000000000000..64faa7b24bad0a06d5a4ed31201cabe92d651393 --- /dev/null +++ b/packages/rocketchat-message-star/client/actionButton.js @@ -0,0 +1,87 @@ +import toastr from 'toastr'; +Meteor.startup(function() { + RocketChat.MessageAction.addButton({ + id: 'star-message', + icon: 'icon-star-empty', + i18nLabel: 'Star_Message', + context: ['starred', 'message', 'message-mobile'], + action() { + const message = this._arguments[1]; + message.starred = Meteor.userId(); + return Meteor.call('starMessage', message, function(error) { + if (error) { + return handleError(error); + } + }); + }, + validation(message) { + if (RocketChat.models.Subscriptions.findOne({ rid: message.rid }) == null && RocketChat.settings.get('Message_AllowStarring')) { + return false; + } + + return !_.findWhere(message.starred, {_id: Meteor.userId()}); + }, + order: 10 + }); + RocketChat.MessageAction.addButton({ + id: 'unstar-message', + icon: 'icon-star', + i18nLabel: 'Unstar_Message', + context: ['starred', 'message', 'message-mobile'], + action() { + const message = this._arguments[1]; + message.starred = false; + return Meteor.call('starMessage', message, function(error) { + if (error) { + return handleError(error); + } + }); + }, + validation(message) { + if (RocketChat.models.Subscriptions.findOne({ rid: message.rid }) == null && RocketChat.settings.get('Message_AllowStarring')) { + return false; + } + + return Boolean(_.findWhere(message.starred, {_id: Meteor.userId()})); + }, + order: 10 + }); + RocketChat.MessageAction.addButton({ + id: 'jump-to-star-message', + icon: 'icon-right-hand', + i18nLabel: 'Jump_to_message', + context: ['starred'], + action() { + const message = this._arguments[1]; + RocketChat.MessageAction.hideDropDown(); + return RoomHistoryManager.getSurroundingMessages(message, 50); + }, + validation(message) { + if (RocketChat.models.Subscriptions.findOne({ rid: message.rid }) == null) { + return false; + } + return true; + }, + order: 100 + }); + return RocketChat.MessageAction.addButton({ + id: 'permalink-star', + icon: 'icon-link', + i18nLabel: 'Permalink', + classes: 'clipboard', + context: ['starred'], + action() { + const message = this._arguments[1]; + RocketChat.MessageAction.hideDropDown(); + $(event.currentTarget).attr('data-clipboard-text', RocketChat.MessageAction.getPermaLink(message._id)); + return toastr.success(TAPi18n.__('Copied')); + }, + validation(message) { + if (RocketChat.models.Subscriptions.findOne({ rid: message.rid }) == null) { + return false; + } + return true; + }, + order: 101 + }); +}); diff --git a/packages/rocketchat-message-star/client/lib/StarredMessage.coffee b/packages/rocketchat-message-star/client/lib/StarredMessage.coffee deleted file mode 100644 index 10798a3a464b65df232b04fcb03eab3f3a07e24d..0000000000000000000000000000000000000000 --- a/packages/rocketchat-message-star/client/lib/StarredMessage.coffee +++ /dev/null @@ -1 +0,0 @@ -@StarredMessage = new Mongo.Collection 'rocketchat_starred_message' diff --git a/packages/rocketchat-message-star/client/lib/StarredMessage.js b/packages/rocketchat-message-star/client/lib/StarredMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..cd014935ac3f37de474a8b0dc32a8491de9055d9 --- /dev/null +++ b/packages/rocketchat-message-star/client/lib/StarredMessage.js @@ -0,0 +1 @@ +this.StarredMessage = new Mongo.Collection('rocketchat_starred_message'); diff --git a/packages/rocketchat-message-star/client/starMessage.coffee b/packages/rocketchat-message-star/client/starMessage.coffee deleted file mode 100644 index d6b22b9488a88e1b5c343cce0223c6d9211fd34f..0000000000000000000000000000000000000000 --- a/packages/rocketchat-message-star/client/starMessage.coffee +++ /dev/null @@ -1,15 +0,0 @@ -Meteor.methods - starMessage: (message) -> - if not Meteor.userId() - return false - - if not RocketChat.models.Subscriptions.findOne({ rid: message.rid })? - return false - - if not RocketChat.settings.get 'Message_AllowStarring' - return false - - ChatMessage.update - _id: message._id - , - $set: { starred: !!message.starred } diff --git a/packages/rocketchat-message-star/client/starMessage.js b/packages/rocketchat-message-star/client/starMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..cae539c7eaa17823e19674dfa744c7c6bfe76883 --- /dev/null +++ b/packages/rocketchat-message-star/client/starMessage.js @@ -0,0 +1,20 @@ +Meteor.methods({ + starMessage(message) { + if (!Meteor.userId()) { + return false; + } + if (RocketChat.models.Subscriptions.findOne({ rid: message.rid }) == null) { + return false; + } + if (!RocketChat.settings.get('Message_AllowStarring')) { + return false; + } + return ChatMessage.update({ + _id: message._id + }, { + $set: { + starred: !!message.starred + } + }); + } +}); diff --git a/packages/rocketchat-message-star/client/tabBar.coffee b/packages/rocketchat-message-star/client/tabBar.js similarity index 68% rename from packages/rocketchat-message-star/client/tabBar.coffee rename to packages/rocketchat-message-star/client/tabBar.js index cab66321912a2664ecd4425ff5a1a10599bb0e2b..85f32f93349ae28cfcbd2afe183cb656316583cd 100644 --- a/packages/rocketchat-message-star/client/tabBar.coffee +++ b/packages/rocketchat-message-star/client/tabBar.js @@ -1,9 +1,10 @@ -Meteor.startup -> - RocketChat.TabBar.addButton({ +Meteor.startup(function() { + return RocketChat.TabBar.addButton({ groups: ['channel', 'group', 'direct'], id: 'starred-messages', i18nTitle: 'Starred_Messages', icon: 'icon-star', template: 'starredMessages', order: 3 - }) + }); +}); diff --git a/packages/rocketchat-message-star/client/views/starredMessages.coffee b/packages/rocketchat-message-star/client/views/starredMessages.coffee deleted file mode 100644 index 9b34d62693657bbd9864ef50e7d1b8cc139e06df..0000000000000000000000000000000000000000 --- a/packages/rocketchat-message-star/client/views/starredMessages.coffee +++ /dev/null @@ -1,40 +0,0 @@ -Template.starredMessages.helpers - hasMessages: -> - return StarredMessage.find({ rid: @rid }, { sort: { ts: -1 } }).count() > 0 - - messages: -> - return StarredMessage.find { rid: @rid }, { sort: { ts: -1 } } - - message: -> - return _.extend(this, { customClass: 'starred' }) - - hasMore: -> - return Template.instance().hasMore.get() - -Template.starredMessages.onCreated -> - @hasMore = new ReactiveVar true - @limit = new ReactiveVar 50 - @autorun => - sub = @subscribe 'starredMessages', @data.rid, @limit.get() - if sub.ready() - if StarredMessage.find({ rid: @data.rid }).count() < @limit.get() - @hasMore.set false - -Template.starredMessages.events - 'click .message-cog': (e, t) -> - e.stopPropagation() - e.preventDefault() - message_id = $(e.currentTarget).closest('.message').attr('id') - RocketChat.MessageAction.hideDropDown() - t.$("\##{message_id} .message-dropdown").remove() - message = StarredMessage.findOne message_id - actions = RocketChat.MessageAction.getButtons message, 'starred' - el = Blaze.toHTMLWithData Template.messageDropdown, { actions: actions } - t.$("\##{message_id} .message-cog-container").append el - dropDown = t.$("\##{message_id} .message-dropdown") - dropDown.show() - - 'scroll .content': _.throttle (e, instance) -> - if e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight - instance.limit.set(instance.limit.get() + 50) - , 200 diff --git a/packages/rocketchat-message-star/client/views/starredMessages.js b/packages/rocketchat-message-star/client/views/starredMessages.js new file mode 100644 index 0000000000000000000000000000000000000000..eed8d3d0bacaefbbc01b893e24e3386047d5ce7e --- /dev/null +++ b/packages/rocketchat-message-star/client/views/starredMessages.js @@ -0,0 +1,66 @@ +/*globals StarredMessage */ +Template.starredMessages.helpers({ + hasMessages() { + return StarredMessage.find({ + rid: this.rid + }, { + sort: { + ts: -1 + } + }).count() > 0; + }, + messages() { + return StarredMessage.find({ + rid: this.rid + }, { + sort: { + ts: -1 + } + }); + }, + message() { + return _.extend(this, { + customClass: 'starred' + }); + }, + hasMore() { + return Template.instance().hasMore.get(); + } +}); + +Template.starredMessages.onCreated(function() { + this.hasMore = new ReactiveVar(true); + this.limit = new ReactiveVar(50); + this.autorun(() => { + const sub = this.subscribe('starredMessages', this.data.rid, this.limit.get()); + const findStarredMessage = StarredMessage.find({ rid: this.data.rid }); + if (sub.ready()) { + if (findStarredMessage.count() < this.limit.get()) { + return this.hasMore.set(false); + } + } + }); +}); + +Template.starredMessages.events({ + 'click .message-cog'(e, t) { + e.stopPropagation(); + e.preventDefault(); + const message_id = $(e.currentTarget).closest('.message').attr('id'); + RocketChat.MessageAction.hideDropDown(); + t.$(`\#${ message_id } .message-dropdown`).remove(); + const message = StarredMessage.findOne(message_id); + const actions = RocketChat.MessageAction.getButtons(message, 'starred'); + const el = Blaze.toHTMLWithData(Template.messageDropdown, { + actions + }); + t.$(`\#${ message_id } .message-cog-container`).append(el); + const dropDown = t.$(`\#${ message_id } .message-dropdown`); + return dropDown.show(); + }, + 'scroll .content': _.throttle(function(e, instance) { + if (e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight) { + return instance.limit.set(instance.limit.get() + 50); + } + }, 200) +}); diff --git a/packages/rocketchat-message-star/package.js b/packages/rocketchat-message-star/package.js index c9bcefca8cb120edd5802a439676777e88904809..b0e1f3e6b6bbeae56abb0a94790dc968946119e3 100644 --- a/packages/rocketchat-message-star/package.js +++ b/packages/rocketchat-message-star/package.js @@ -8,7 +8,6 @@ Package.describe({ Package.onUse(function(api) { api.use([ 'mongo', - 'coffeescript', 'ecmascript', 'underscore', 'less', @@ -18,19 +17,19 @@ Package.onUse(function(api) { api.use('templating', 'client'); api.addFiles([ - 'client/lib/StarredMessage.coffee', - 'client/actionButton.coffee', - 'client/starMessage.coffee', - 'client/tabBar.coffee', + 'client/lib/StarredMessage.js', + 'client/actionButton.js', + 'client/starMessage.js', + 'client/tabBar.js', 'client/views/starredMessages.html', - 'client/views/starredMessages.coffee', + 'client/views/starredMessages.js', 'client/views/stylesheets/messagestar.less' ], 'client'); api.addFiles([ - 'server/settings.coffee', - 'server/starMessage.coffee', - 'server/publications/starredMessages.coffee', - 'server/startup/indexes.coffee' + 'server/settings.js', + 'server/starMessage.js', + 'server/publications/starredMessages.js', + 'server/startup/indexes.js' ], 'server'); }); diff --git a/packages/rocketchat-message-star/server/publications/starredMessages.coffee b/packages/rocketchat-message-star/server/publications/starredMessages.coffee deleted file mode 100644 index 51d664d7653035ed39bf3d9dcee23267e501e0a5..0000000000000000000000000000000000000000 --- a/packages/rocketchat-message-star/server/publications/starredMessages.coffee +++ /dev/null @@ -1,23 +0,0 @@ -Meteor.publish 'starredMessages', (rid, limit=50) -> - unless this.userId - return this.ready() - - publication = @ - - user = RocketChat.models.Users.findOneById this.userId - unless user - return this.ready() - - cursorHandle = RocketChat.models.Messages.findStarredByUserAtRoom(this.userId, rid, { sort: { ts: -1 }, limit: limit }).observeChanges - added: (_id, record) -> - publication.added('rocketchat_starred_message', _id, record) - - changed: (_id, record) -> - publication.changed('rocketchat_starred_message', _id, record) - - removed: (_id) -> - publication.removed('rocketchat_starred_message', _id) - - @ready() - @onStop -> - cursorHandle.stop() diff --git a/packages/rocketchat-message-star/server/publications/starredMessages.js b/packages/rocketchat-message-star/server/publications/starredMessages.js new file mode 100644 index 0000000000000000000000000000000000000000..15446bebeef5fef3110d9a978160dedb8397f450 --- /dev/null +++ b/packages/rocketchat-message-star/server/publications/starredMessages.js @@ -0,0 +1,30 @@ +Meteor.publish('starredMessages', function(rid, limit = 50) { + if (!this.userId) { + return this.ready(); + } + const publication = this; + const user = RocketChat.models.Users.findOneById(this.userId); + if (!user) { + return this.ready(); + } + const cursorHandle = RocketChat.models.Messages.findStarredByUserAtRoom(this.userId, rid, { + sort: { + ts: -1 + }, + limit + }).observeChanges({ + added(_id, record) { + return publication.added('rocketchat_starred_message', _id, record); + }, + changed(_id, record) { + return publication.changed('rocketchat_starred_message', _id, record); + }, + removed(_id) { + return publication.removed('rocketchat_starred_message', _id); + } + }); + this.ready(); + return this.onStop(function() { + return cursorHandle.stop(); + }); +}); diff --git a/packages/rocketchat-message-star/server/settings.coffee b/packages/rocketchat-message-star/server/settings.coffee deleted file mode 100644 index de6bb651c8f7d570b0146559cef38109a4f3b726..0000000000000000000000000000000000000000 --- a/packages/rocketchat-message-star/server/settings.coffee +++ /dev/null @@ -1,2 +0,0 @@ -Meteor.startup -> - RocketChat.settings.add 'Message_AllowStarring', true, { type: 'boolean', group: 'Message', public: true } diff --git a/packages/rocketchat-message-star/server/settings.js b/packages/rocketchat-message-star/server/settings.js new file mode 100644 index 0000000000000000000000000000000000000000..4a8fa47af713b9fcec53d4d25cdc9b7606abf971 --- /dev/null +++ b/packages/rocketchat-message-star/server/settings.js @@ -0,0 +1,7 @@ +Meteor.startup(function() { + return RocketChat.settings.add('Message_AllowStarring', true, { + type: 'boolean', + group: 'Message', + 'public': true + }); +}); diff --git a/packages/rocketchat-message-star/server/starMessage.coffee b/packages/rocketchat-message-star/server/starMessage.coffee deleted file mode 100644 index 030fa908162809354ad6ff34a9ca66fe48784731..0000000000000000000000000000000000000000 --- a/packages/rocketchat-message-star/server/starMessage.coffee +++ /dev/null @@ -1,14 +0,0 @@ -Meteor.methods - starMessage: (message) -> - if not Meteor.userId() - throw new Meteor.Error('error-invalid-user', "Invalid user", { method: 'starMessage' }) - - if not RocketChat.settings.get 'Message_AllowStarring' - throw new Meteor.Error 'error-action-not-allowed', 'Message starring not allowed', { method: 'pinMessage', action: 'Message_starring' } - - room = RocketChat.models.Rooms.findOneById(message.rid) - - if Array.isArray(room.usernames) && room.usernames.indexOf(Meteor.user().username) is -1 - return false - - RocketChat.models.Messages.updateUserStarById(message._id, Meteor.userId(), message.starred) diff --git a/packages/rocketchat-message-star/server/starMessage.js b/packages/rocketchat-message-star/server/starMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..3ed7ee3bd8b6d85828fe0b5459ab5e0f3e5ad56f --- /dev/null +++ b/packages/rocketchat-message-star/server/starMessage.js @@ -0,0 +1,21 @@ +Meteor.methods({ + starMessage(message) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'starMessage' + }); + } + if (!RocketChat.settings.get('Message_AllowStarring')) { + throw new Meteor.Error('error-action-not-allowed', 'Message starring not allowed', { + method: 'pinMessage', + action: 'Message_starring' + }); + } + const room = RocketChat.models.Rooms.findOneById(message.rid); + if (Array.isArray(room.usernames) && room.usernames.indexOf(Meteor.user().username) === -1) { + return false; + } + return RocketChat.models.Messages.updateUserStarById(message._id, Meteor.userId(), message.starred); + } +}); + diff --git a/packages/rocketchat-message-star/server/startup/indexes.coffee b/packages/rocketchat-message-star/server/startup/indexes.coffee deleted file mode 100644 index 519eb19b596f6e1b31f27770cf0fedbb6e6fead5..0000000000000000000000000000000000000000 --- a/packages/rocketchat-message-star/server/startup/indexes.coffee +++ /dev/null @@ -1,3 +0,0 @@ -Meteor.startup -> - Meteor.defer -> - RocketChat.models.Messages.tryEnsureIndex { 'starred._id': 1 }, { sparse: 1 } diff --git a/packages/rocketchat-message-star/server/startup/indexes.js b/packages/rocketchat-message-star/server/startup/indexes.js new file mode 100644 index 0000000000000000000000000000000000000000..f1946d6f4eb17f89ccc13dbcc355a7c3bc61065d --- /dev/null +++ b/packages/rocketchat-message-star/server/startup/indexes.js @@ -0,0 +1,9 @@ +Meteor.startup(function() { + return Meteor.defer(function() { + return RocketChat.models.Messages.tryEnsureIndex({ + 'starred._id': 1 + }, { + sparse: 1 + }); + }); +}); diff --git a/packages/rocketchat-oembed/client/baseWidget.coffee b/packages/rocketchat-oembed/client/baseWidget.coffee deleted file mode 100644 index 09a090712d9da95784da1947701f41e47b82ad71..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oembed/client/baseWidget.coffee +++ /dev/null @@ -1,21 +0,0 @@ -Template.oembedBaseWidget.helpers - template: -> - if this._overrideTemplate - return this._overrideTemplate - - if this.headers?.contentType?.match(/image\/.*/)? - return 'oembedImageWidget' - - if this.headers?.contentType?.match(/audio\/.*/)? - return 'oembedAudioWidget' - - if this.headers?.contentType?.match(/video\/.*/)? or this.meta?.twitterPlayerStreamContentType?.match(/video\/.*/)? - return 'oembedVideoWidget' - - if this.meta?.oembedHtml? - return 'oembedFrameWidget' - - if this.meta?.sandstorm?.grain? - return 'oembedSandstormGrain' - - return 'oembedUrlWidget' diff --git a/packages/rocketchat-oembed/client/baseWidget.js b/packages/rocketchat-oembed/client/baseWidget.js new file mode 100644 index 0000000000000000000000000000000000000000..1b8c95278b2dec361e915573d9004d23a1abb7cf --- /dev/null +++ b/packages/rocketchat-oembed/client/baseWidget.js @@ -0,0 +1,28 @@ +Template.oembedBaseWidget.helpers({ + template() { + let contentType; + if (this.headers) { + contentType = this.headers.contentType; + } + + if (this._overrideTemplate) { + return this._overrideTemplate; + } + if (this.headers && contentType && contentType.match(/image\/.*/)) { + return 'oembedImageWidget'; + } + if (this.headers && contentType && contentType.match(/audio\/.*/)) { + return 'oembedAudioWidget'; + } + if ((this.headers && contentType && contentType.match(/video\/.*/)) || (this.meta && this.meta.twitterPlayerStreamContentType && this.meta.twitterPlayerStreamContentType.match(/video\/.*/))) { + return 'oembedVideoWidget'; + } + if (this.meta && this.meta.oembedHtml) { + return 'oembedFrameWidget'; + } + if (this.meta && this.meta.sandstorm && this.meta.sandstorm.grain) { + return 'oembedSandstormGrain'; + } + return 'oembedUrlWidget'; + } +}); diff --git a/packages/rocketchat-oembed/client/oembedAudioWidget.coffee b/packages/rocketchat-oembed/client/oembedAudioWidget.coffee deleted file mode 100644 index 03756dc6da528f9c39a491c4f01a39f5beeb6cd6..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oembed/client/oembedAudioWidget.coffee +++ /dev/null @@ -1,7 +0,0 @@ -Template.oembedAudioWidget.helpers - - collapsed: -> - if this.collapsed? - return this.collapsed - else - return Meteor.user()?.settings?.preferences?.collapseMediaByDefault is true diff --git a/packages/rocketchat-oembed/client/oembedAudioWidget.js b/packages/rocketchat-oembed/client/oembedAudioWidget.js new file mode 100644 index 0000000000000000000000000000000000000000..23bc9a4d7c125f78174f5f9faaa5f69b28dc9d61 --- /dev/null +++ b/packages/rocketchat-oembed/client/oembedAudioWidget.js @@ -0,0 +1,10 @@ +Template.oembedAudioWidget.helpers({ + collapsed() { + if (this.collapsed) { + return this.collapsed; + } else { + const user = Meteor.user(); + return user && user.settings && user.settings.preferences && user.settings.preferences.collapseMediaByDefault === true; + } + } +}); diff --git a/packages/rocketchat-oembed/client/oembedFrameWidget.coffee b/packages/rocketchat-oembed/client/oembedFrameWidget.coffee deleted file mode 100644 index d589454c1e873d8eef823460a9cd2fb1ddaf4d6d..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oembed/client/oembedFrameWidget.coffee +++ /dev/null @@ -1,7 +0,0 @@ -Template.oembedFrameWidget.helpers - - collapsed: -> - if this.collapsed? - return this.collapsed - else - return Meteor.user()?.settings?.preferences?.collapseMediaByDefault is true diff --git a/packages/rocketchat-oembed/client/oembedFrameWidget.js b/packages/rocketchat-oembed/client/oembedFrameWidget.js new file mode 100644 index 0000000000000000000000000000000000000000..e47f62edca66dee2e1bad2cd5094a5e4b359bfcd --- /dev/null +++ b/packages/rocketchat-oembed/client/oembedFrameWidget.js @@ -0,0 +1,10 @@ +Template.oembedFrameWidget.helpers({ + collapsed() { + if (this.collapsed) { + return this.collapsed; + } else { + const user = Meteor.user(); + return user && user.settings && user.settings.preferences && user.settings.preferences.collapseMediaByDefault === true; + } + } +}); diff --git a/packages/rocketchat-oembed/client/oembedImageWidget.coffee b/packages/rocketchat-oembed/client/oembedImageWidget.coffee deleted file mode 100644 index 794442111178561f07682578c9fd2129440835ca..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oembed/client/oembedImageWidget.coffee +++ /dev/null @@ -1,17 +0,0 @@ -Template.oembedImageWidget.helpers - loadImage: -> - - if Meteor.user()?.settings?.preferences?.autoImageLoad is false and this.downloadImages? is not true - return false - - if Meteor.Device.isPhone() and Meteor.user()?.settings?.preferences?.saveMobileBandwidth and this.downloadImages? is not true - return false - - return true - - - collapsed: -> - if this.collapsed? - return this.collapsed - else - return Meteor.user()?.settings?.preferences?.collapseMediaByDefault is true diff --git a/packages/rocketchat-oembed/client/oembedImageWidget.js b/packages/rocketchat-oembed/client/oembedImageWidget.js new file mode 100644 index 0000000000000000000000000000000000000000..82c71ae1f415e8a529c9dd02f9b65c61c76f682b --- /dev/null +++ b/packages/rocketchat-oembed/client/oembedImageWidget.js @@ -0,0 +1,21 @@ +Template.oembedImageWidget.helpers({ + loadImage() { + const user = Meteor.user(); + + if (user && user.settings && user.settings.preferences && user.settings.preferences.autoImageLoad === false && this.downloadImages == null) { + return false; + } + if (Meteor.Device.isPhone() && user && user.settings && user.settings.preferences && user.settings.preferences.saveMobileBandwidth && this.downloadImages == null) { + return false; + } + return true; + }, + collapsed() { + if (this.collapsed != null) { + return this.collapsed; + } else { + const user = Meteor.user(); + return user && user.settings && user.settings.preferences && user.settings.preferences.collapseMediaByDefault === true; + } + } +}); diff --git a/packages/rocketchat-oembed/client/oembedSandstormGrain.coffee b/packages/rocketchat-oembed/client/oembedSandstormGrain.coffee deleted file mode 100644 index fd78824a49be8751a1bdc43da997b2fda0576e56..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oembed/client/oembedSandstormGrain.coffee +++ /dev/null @@ -1,17 +0,0 @@ -Template.oembedSandstormGrain.helpers - token: -> - return @meta.sandstorm.grain.token - appTitle: -> - return @meta.sandstorm.grain.appTitle.defaultText - grainTitle: -> - return @meta.sandstorm.grain.grainTitle - appIconUrl: -> - return @meta.sandstorm.grain.appIconUrl - descriptor: -> - return @meta.sandstorm.grain.descriptor -window.sandstormOembed = (e) -> - e = e or window.event - src = e.target or e.srcElement - token = src.getAttribute "data-token" - descriptor = src.getAttribute "data-descriptor" - Meteor.call "sandstormOffer", token, descriptor diff --git a/packages/rocketchat-oembed/client/oembedSandstormGrain.js b/packages/rocketchat-oembed/client/oembedSandstormGrain.js new file mode 100644 index 0000000000000000000000000000000000000000..0b1fbd548fda7b16038f8e4bb5f87fe5544243f8 --- /dev/null +++ b/packages/rocketchat-oembed/client/oembedSandstormGrain.js @@ -0,0 +1,25 @@ +Template.oembedSandstormGrain.helpers({ + token() { + return this.meta.sandstorm.grain.token; + }, + appTitle() { + return this.meta.sandstorm.grain.appTitle.defaultText; + }, + grainTitle() { + return this.meta.sandstorm.grain.grainTitle; + }, + appIconUrl() { + return this.meta.sandstorm.grain.appIconUrl; + }, + descriptor() { + return this.meta.sandstorm.grain.descriptor; + } +}); + +window.sandstormOembed = function(e) { + e = e || window.event; + const src = e.target || e.srcElement; + const token = src.getAttribute('data-token'); + const descriptor = src.getAttribute('data-descriptor'); + return Meteor.call('sandstormOffer', token, descriptor); +}; diff --git a/packages/rocketchat-oembed/client/oembedUrlWidget.coffee b/packages/rocketchat-oembed/client/oembedUrlWidget.coffee deleted file mode 100644 index 5b160cfb235aa38d3e0e1f1ac7fa8d46dfb0d8f9..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oembed/client/oembedUrlWidget.coffee +++ /dev/null @@ -1,57 +0,0 @@ -getTitle = (self) -> - if not self.meta? - return - - return self.meta.ogTitle or self.meta.twitterTitle or self.meta.title or self.meta.pageTitle - -getDescription = (self) -> - if not self.meta? - return - - description = self.meta.ogDescription or self.meta.twitterDescription or self.meta.description - if not description? - return - - return _.unescape description.replace /(^[“\s]*)|([â€\s]*$)/g, '' - - -Template.oembedUrlWidget.helpers - description: -> - description = getDescription this - return Blaze._escape(description) if _.isString description - - title: -> - title = getTitle this - return Blaze._escape(title) if _.isString title - - target: -> - if not this.parsedUrl?.host || !document?.location?.host || this.parsedUrl.host isnt document.location.host - return '_blank' - - image: -> - if not this.meta? - return - - decodedOgImage = @meta.ogImage?.replace?(/&/g, '&') - - url = this.meta.msapplicationTileImage or decodedOgImage or this.meta.twitterImage - - if not url? - return - - if url.indexOf('//') is 0 - url = "#{this.parsedUrl.protocol}#{url}" - - else if url.indexOf('/') is 0 and this.parsedUrl?.host? - url = "#{this.parsedUrl.protocol}//#{this.parsedUrl.host}#{url}" - - return url - - show: -> - return getDescription(this)? or getTitle(this)? - - collapsed: -> - if this.collapsed? - return this.collapsed - else - return Meteor.user()?.settings?.preferences?.collapseMediaByDefault is true diff --git a/packages/rocketchat-oembed/client/oembedUrlWidget.js b/packages/rocketchat-oembed/client/oembedUrlWidget.js new file mode 100644 index 0000000000000000000000000000000000000000..110f0c96e70dc6986f1f052bdf84c984b44034d5 --- /dev/null +++ b/packages/rocketchat-oembed/client/oembedUrlWidget.js @@ -0,0 +1,67 @@ +const getTitle = function(self) { + if (self.meta == null) { + return; + } + return self.meta.ogTitle || self.meta.twitterTitle || self.meta.title || self.meta.pageTitle; +}; + +const getDescription = function(self) { + if (self.meta == null) { + return; + } + const description = self.meta.ogDescription || self.meta.twitterDescription || self.meta.description; + if (description == null) { + return; + } + return _.unescape(description.replace(/(^[“\s]*)|([â€\s]*$)/g, '')); +}; + +Template.oembedUrlWidget.helpers({ + description() { + const description = getDescription(this); + if (_.isString(description)) { + return Blaze._escape(description); + } + }, + title() { + const title = getTitle(this); + if (_.isString(title)) { + return Blaze._escape(title); + } + }, + target() { + if (!(this.parsedUrl && this.parsedUrl.host) || !(document && document.location && document.location.host) || (this.parsedUrl && this.parsedUrl.host !== document.location.host)) { + return '_blank'; + } + }, + image() { + if (this.meta == null) { + return; + } + let decodedOgImage; + if (this.meta.ogImage && this.meta.ogImage.replace) { + decodedOgImage = this.meta.ogImage.replace(/&/g, '&'); + } + let url = this.meta.msapplicationTileImage || decodedOgImage || this.meta.twitterImage; + if (url == null) { + return; + } + if (url.indexOf('//') === 0) { + url = `${ this.parsedUrl.protocol }${ url }`; + } else if (url.indexOf('/') === 0 && (this.parsedUrl && this.parsedUrl.host)) { + url = `${ this.parsedUrl.protocol }//${ this.parsedUrl.host }${ url }`; + } + return url; + }, + show() { + return (getDescription(this) != null) || (getTitle(this) != null); + }, + collapsed() { + if (this.collapsed != null) { + return this.collapsed; + } else { + const user = Meteor.user(); + return user && user.settings && user.settings.preferences && user.settings.preferences.collapseMediaByDefault === true; + } + } +}); diff --git a/packages/rocketchat-oembed/client/oembedVideoWidget.coffee b/packages/rocketchat-oembed/client/oembedVideoWidget.coffee deleted file mode 100644 index 399e57e57defedf7e3c81d49f8b96d840708b179..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oembed/client/oembedVideoWidget.coffee +++ /dev/null @@ -1,22 +0,0 @@ -getTitle = (self) -> - if not self.meta? - return - - return self.meta.ogTitle or self.meta.twitterTitle or self.meta.title or self.meta.pageTitle - - -Template.oembedVideoWidget.helpers - url: -> - return @meta?.twitterPlayerStream or @url - - contentType: -> - return @meta?.twitterPlayerStreamContentType or @headers?.contentType - - title: -> - return getTitle @ - - collapsed: -> - if this.collapsed? - return this.collapsed - else - return Meteor.user()?.settings?.preferences?.collapseMediaByDefault is true diff --git a/packages/rocketchat-oembed/client/oembedVideoWidget.js b/packages/rocketchat-oembed/client/oembedVideoWidget.js new file mode 100644 index 0000000000000000000000000000000000000000..a825ab5c976becac156d2c12a8511597887388b2 --- /dev/null +++ b/packages/rocketchat-oembed/client/oembedVideoWidget.js @@ -0,0 +1,35 @@ +const getTitle = function(self) { + if (self.meta == null) { + return; + } + return self.meta.ogTitle || self.meta.twitterTitle || self.meta.title || self.meta.pageTitle; +}; + +Template.oembedVideoWidget.helpers({ + url() { + if (this.meta && this.meta.twitterPlayerStream) { + return this.meta.twitterPlayerStream; + } else if (this.url) { + return this.url; + } + }, + contentType() { + if (this.meta && this.meta.twitterPlayerStreamContentType) { + return this.meta.twitterPlayerStreamContentType; + } else if (this.headers && this.headers.contentType) { + return this.headers.contentType; + } + }, + title() { + return getTitle(this); + }, + collapsed() { + if (this.collapsed) { + return this.collapsed; + } else { + const user = Meteor.user(); + return user && user.settings && user.settings.preferences && user.settings.preferences.collapseMediaByDefault === true; + } + } + +}); diff --git a/packages/rocketchat-oembed/client/oembedYoutubeWidget.coffee b/packages/rocketchat-oembed/client/oembedYoutubeWidget.coffee deleted file mode 100644 index c848a17f455d561af92d62c73dd98b59547c01c0..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oembed/client/oembedYoutubeWidget.coffee +++ /dev/null @@ -1,7 +0,0 @@ -Template.oembedYoutubeWidget.helpers - - collapsed: -> - if this.collapsed? - return this.collapsed - else - return Meteor.user()?.settings?.preferences?.collapseMediaByDefault is true diff --git a/packages/rocketchat-oembed/client/oembedYoutubeWidget.js b/packages/rocketchat-oembed/client/oembedYoutubeWidget.js new file mode 100644 index 0000000000000000000000000000000000000000..0e2ce33d4e49930feb25caee56c714ba3f8b64d8 --- /dev/null +++ b/packages/rocketchat-oembed/client/oembedYoutubeWidget.js @@ -0,0 +1,10 @@ +Template.oembedYoutubeWidget.helpers({ + collapsed() { + if (this.collapsed) { + return this.collapsed; + } else { + const user = Meteor.user(); + return user && user.settings && user.settings.preferences && user.settings.preferences.collapseMediaByDefault === true; + } + } +}); diff --git a/packages/rocketchat-oembed/package.js b/packages/rocketchat-oembed/package.js index 093b324f689276e437cb0e012f34ce61be228d13..9f776d12a208fd0937fbb2f605876eb9f8eb7cf2 100644 --- a/packages/rocketchat-oembed/package.js +++ b/packages/rocketchat-oembed/package.js @@ -17,40 +17,39 @@ Package.onUse(function(api) { 'http', 'templating', 'ecmascript', - 'coffeescript', 'underscore', 'konecty:change-case', 'rocketchat:lib' ]); api.addFiles('client/baseWidget.html', 'client'); - api.addFiles('client/baseWidget.coffee', 'client'); + api.addFiles('client/baseWidget.js', 'client'); api.addFiles('client/oembedImageWidget.html', 'client'); - api.addFiles('client/oembedImageWidget.coffee', 'client'); + api.addFiles('client/oembedImageWidget.js', 'client'); api.addFiles('client/oembedAudioWidget.html', 'client'); - api.addFiles('client/oembedAudioWidget.coffee', 'client'); + api.addFiles('client/oembedAudioWidget.js', 'client'); api.addFiles('client/oembedVideoWidget.html', 'client'); - api.addFiles('client/oembedVideoWidget.coffee', 'client'); + api.addFiles('client/oembedVideoWidget.js', 'client'); api.addFiles('client/oembedYoutubeWidget.html', 'client'); - api.addFiles('client/oembedYoutubeWidget.coffee', 'client'); + api.addFiles('client/oembedYoutubeWidget.js', 'client'); api.addFiles('client/oembedUrlWidget.html', 'client'); - api.addFiles('client/oembedUrlWidget.coffee', 'client'); + api.addFiles('client/oembedUrlWidget.js', 'client'); api.addFiles('client/oembedFrameWidget.html', 'client'); - api.addFiles('client/oembedFrameWidget.coffee', 'client'); + api.addFiles('client/oembedFrameWidget.js', 'client'); api.addFiles('client/oembedSandstormGrain.html', 'client'); - api.addFiles('client/oembedSandstormGrain.coffee', 'client'); + api.addFiles('client/oembedSandstormGrain.js', 'client'); - api.addFiles('server/server.coffee', 'server'); - api.addFiles('server/providers.coffee', 'server'); + api.addFiles('server/server.js', 'server'); + api.addFiles('server/providers.js', 'server'); api.addFiles('server/jumpToMessage.js', 'server'); - api.addFiles('server/models/OEmbedCache.coffee', 'server'); + api.addFiles('server/models/OEmbedCache.js', 'server'); api.export('OEmbed', 'server'); }); diff --git a/packages/rocketchat-oembed/server/jumpToMessage.js b/packages/rocketchat-oembed/server/jumpToMessage.js index 0f674e461bfac4e01faf6421c23e88aabd910da0..2cb673cb3a9b32a1dc51713f9a0a26dbb656e67e 100644 --- a/packages/rocketchat-oembed/server/jumpToMessage.js +++ b/packages/rocketchat-oembed/server/jumpToMessage.js @@ -17,7 +17,7 @@ RocketChat.callbacks.add('beforeSaveMessage', (msg) => { msg.attachments.push({ 'text' : jumpToMessage.msg, 'translations': jumpToMessage.translations, - 'author_name' : jumpToMessage.u.username, + 'author_name' : jumpToMessage.alias || jumpToMessage.u.username, 'author_icon' : getAvatarUrlFromUsername(jumpToMessage.u.username), 'message_link' : item.url, 'attachments' : jumpToMessage.attachments || [], diff --git a/packages/rocketchat-oembed/server/models/OEmbedCache.coffee b/packages/rocketchat-oembed/server/models/OEmbedCache.coffee deleted file mode 100644 index cdf8fbfbf27f6908485b6a500b34cadba1a4f299..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oembed/server/models/OEmbedCache.coffee +++ /dev/null @@ -1,30 +0,0 @@ -RocketChat.models.OEmbedCache = new class extends RocketChat.models._Base - constructor: -> - super('oembed_cache') - @tryEnsureIndex { 'updatedAt': 1 } - - - # FIND ONE - findOneById: (_id, options) -> - query = - _id: _id - - return @findOne query, options - - - # INSERT - createWithIdAndData: (_id, data) -> - record = - _id: _id - data: data - updatedAt: new Date - - record._id = @insert record - return record - - # REMOVE - removeAfterDate: (date) -> - query = - updatedAt: - $lte: date - @remove query diff --git a/packages/rocketchat-oembed/server/models/OEmbedCache.js b/packages/rocketchat-oembed/server/models/OEmbedCache.js new file mode 100644 index 0000000000000000000000000000000000000000..f0b451247f9e02f007b614c8f69c87dac61364ad --- /dev/null +++ b/packages/rocketchat-oembed/server/models/OEmbedCache.js @@ -0,0 +1,38 @@ + +RocketChat.models.OEmbedCache = new class extends RocketChat.models._Base { + constructor() { + super('oembed_cache'); + this.tryEnsureIndex({ 'updatedAt': 1 }); + } + + //FIND ONE + findOneById(_id, options) { + const query = { + _id + }; + return this.findOne(query, options); + } + + //INSERT + createWithIdAndData(_id, data) { + const record = { + _id, + data, + updatedAt: new Date + }; + record._id = this.insert(record); + return record; + } + + //REMOVE + removeAfterDate(date) { + const query = { + updatedAt: { + $lte: date + } + }; + return this.remove(query); + } +}; + + diff --git a/packages/rocketchat-oembed/server/providers.coffee b/packages/rocketchat-oembed/server/providers.coffee deleted file mode 100644 index 2a4b3002e1b7e8b377becd07a3cb5211fa71e20f..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oembed/server/providers.coffee +++ /dev/null @@ -1,84 +0,0 @@ -URL = Npm.require('url') -QueryString = Npm.require('querystring') - -class Providers - providers: [] - - @getConsumerUrl: (provider, url) -> - urlObj = URL.parse provider.endPoint, true - urlObj.query['url'] = url - delete urlObj.search - return URL.format urlObj - - registerProvider: (provider) -> - this.providers.push(provider) - - getProviders: () -> - return this.providers - - getProviderForUrl: (url) -> - return _.find this.providers, (provider) -> - candidate = _.find provider.urls, (re) -> - return re.test url - return candidate? - -providers = new Providers() -providers.registerProvider - urls: [new RegExp('https?://soundcloud.com/\\S+')] - endPoint: 'https://soundcloud.com/oembed?format=json&maxheight=150' -providers.registerProvider - urls: [new RegExp('https?://vimeo.com/[^/]+'), new RegExp('https?://vimeo.com/channels/[^/]+/[^/]+'), new RegExp('https://vimeo.com/groups/[^/]+/videos/[^/]+')] - endPoint: 'https://vimeo.com/api/oembed.json?maxheight=200' -providers.registerProvider - urls: [new RegExp('https?://www.youtube.com/\\S+'), new RegExp('https?://youtu.be/\\S+')] - endPoint: 'https://www.youtube.com/oembed?maxheight=200' -providers.registerProvider - urls: [new RegExp('https?://www.rdio.com/\\S+'), new RegExp('https?://rd.io/\\S+')] - endPoint: 'https://www.rdio.com/api/oembed/?format=json&maxheight=150' -providers.registerProvider - urls: [new RegExp('https?://www.slideshare.net/[^/]+/[^/]+')] - endPoint: 'https://www.slideshare.net/api/oembed/2?format=json&maxheight=200' -providers.registerProvider - urls: [new RegExp('https?://www.dailymotion.com/video/\\S+')] - endPoint: 'https://www.dailymotion.com/services/oembed?maxheight=200' - -RocketChat.oembed = {} -RocketChat.oembed.providers = providers - -RocketChat.callbacks.add 'oembed:beforeGetUrlContent', (data) -> - if data.parsedUrl? - url = URL.format data.parsedUrl - provider = providers.getProviderForUrl url - if provider? - consumerUrl = Providers.getConsumerUrl provider, url - consumerUrl = URL.parse consumerUrl, true - _.extend data.parsedUrl, consumerUrl - data.urlObj.port = consumerUrl.port - data.urlObj.hostname = consumerUrl.hostname - data.urlObj.pathname = consumerUrl.pathname - data.urlObj.query = consumerUrl.query - delete data.urlObj.search - delete data.urlObj.host - - return data -, RocketChat.callbacks.priority.MEDIUM, 'oembed-providers-before' - -RocketChat.callbacks.add 'oembed:afterParseContent', (data) -> - if data.parsedUrl?.query? - queryString = data.parsedUrl.query - if _.isString data.parsedUrl.query - queryString = QueryString.parse data.parsedUrl.query - if queryString.url? - url = queryString.url - provider = providers.getProviderForUrl url - if provider? - if data.content?.body? - try - metas = JSON.parse data.content.body; - _.each metas, (value, key) -> - if _.isString value - data.meta[changeCase.camelCase('oembed_' + key)] = value - data.meta['oembedUrl'] = url - - return data -, RocketChat.callbacks.priority.MEDIUM, 'oembed-providers-after' diff --git a/packages/rocketchat-oembed/server/providers.js b/packages/rocketchat-oembed/server/providers.js new file mode 100644 index 0000000000000000000000000000000000000000..4d5c59ffce9ac52af2e866572f202c5b175e2740 --- /dev/null +++ b/packages/rocketchat-oembed/server/providers.js @@ -0,0 +1,120 @@ +/*globals changeCase */ + + +const URL = Npm.require('url'); + +const QueryString = Npm.require('querystring'); + +class Providers { + constructor() { + this.providers = []; + } + + static getConsumerUrl(provider, url) { + const urlObj = URL.parse(provider.endPoint, true); + urlObj.query['url'] = url; + delete urlObj.search; + return URL.format(urlObj); + } + + registerProvider(provider) { + return this.providers.push(provider); + } + + getProviders() { + return this.providers; + } + + getProviderForUrl(url) { + return _.find(this.providers, function(provider) { + const candidate = _.find(provider.urls, function(re) { + return re.test(url); + }); + return candidate != null; + }); + } +} + +const providers = new Providers(); + +providers.registerProvider({ + urls: [new RegExp('https?://soundcloud.com/\\S+')], + endPoint: 'https://soundcloud.com/oembed?format=json&maxheight=150' +}); + +providers.registerProvider({ + urls: [new RegExp('https?://vimeo.com/[^/]+'), new RegExp('https?://vimeo.com/channels/[^/]+/[^/]+'), new RegExp('https://vimeo.com/groups/[^/]+/videos/[^/]+')], + endPoint: 'https://vimeo.com/api/oembed.json?maxheight=200' +}); + +providers.registerProvider({ + urls: [new RegExp('https?://www.youtube.com/\\S+'), new RegExp('https?://youtu.be/\\S+')], + endPoint: 'https://www.youtube.com/oembed?maxheight=200' +}); + +providers.registerProvider({ + urls: [new RegExp('https?://www.rdio.com/\\S+'), new RegExp('https?://rd.io/\\S+')], + endPoint: 'https://www.rdio.com/api/oembed/?format=json&maxheight=150' +}); + +providers.registerProvider({ + urls: [new RegExp('https?://www.slideshare.net/[^/]+/[^/]+')], + endPoint: 'https://www.slideshare.net/api/oembed/2?format=json&maxheight=200' +}); + +providers.registerProvider({ + urls: [new RegExp('https?://www.dailymotion.com/video/\\S+')], + endPoint: 'https://www.dailymotion.com/services/oembed?maxheight=200' +}); + +RocketChat.oembed = {}; + +RocketChat.oembed.providers = providers; + +RocketChat.callbacks.add('oembed:beforeGetUrlContent', function(data) { + if (data.parsedUrl != null) { + const url = URL.format(data.parsedUrl); + const provider = providers.getProviderForUrl(url); + if (provider != null) { + let consumerUrl = Providers.getConsumerUrl(provider, url); + consumerUrl = URL.parse(consumerUrl, true); + _.extend(data.parsedUrl, consumerUrl); + data.urlObj.port = consumerUrl.port; + data.urlObj.hostname = consumerUrl.hostname; + data.urlObj.pathname = consumerUrl.pathname; + data.urlObj.query = consumerUrl.query; + delete data.urlObj.search; + delete data.urlObj.host; + } + } + return data; +}, RocketChat.callbacks.priority.MEDIUM, 'oembed-providers-before'); + +RocketChat.callbacks.add('oembed:afterParseContent', function(data) { + if (data.parsedUrl && data.parsedUrl.query) { + let queryString = data.parsedUrl.query; + if (_.isString(data.parsedUrl.query)) { + queryString = QueryString.parse(data.parsedUrl.query); + } + if (queryString.url != null) { + const url = queryString.url; + const provider = providers.getProviderForUrl(url); + if (provider != null) { + if (data.content && data.content.body) { + try { + const metas = JSON.parse(data.content.body); + _.each(metas, function(value, key) { + if (_.isString(value)) { + return data.meta[changeCase.camelCase(`oembed_${ key }`)] = value; + } + }); + data.meta['oembedUrl'] = url; + } catch (error) { + console.log(error); + } + } + } + } + } + return data; +}, RocketChat.callbacks.priority.MEDIUM, 'oembed-providers-after'); diff --git a/packages/rocketchat-oembed/server/server.coffee b/packages/rocketchat-oembed/server/server.coffee deleted file mode 100644 index 63ab940c3d55066bd09dec74a182843f222772af..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oembed/server/server.coffee +++ /dev/null @@ -1,256 +0,0 @@ -URL = Npm.require('url') -querystring = Npm.require('querystring') -request = HTTPInternals.NpmModules.request.module -iconv = Npm.require('iconv-lite') -ipRangeCheck = Npm.require('ip-range-check') -he = Npm.require('he') -jschardet = Npm.require('jschardet') - -OEmbed = {} - -# Detect encoding -# Priority: -# Detected == HTTP Header > Detected == HTML meta > HTTP Header > HTML meta > Detected > Default (utf-8) -# See also: https://www.w3.org/International/questions/qa-html-encoding-declarations.en#quickanswer -getCharset = (contentType, body) -> - contentType = contentType || '' - binary = body.toString('binary') - - detected = jschardet.detect(binary) - if detected.confidence > 0.8 - detectedCharset = detected.encoding.toLowerCase() - - m1 = contentType.match(/charset=([\w\-]+)/i) - if m1 - httpHeaderCharset = m1[1].toLowerCase() - - m2 = binary.match(/<meta\b[^>]*charset=["']?([\w\-]+)/i) - if m2 - htmlMetaCharset = m2[1].toLowerCase() - - if detectedCharset - if detectedCharset == httpHeaderCharset - result = httpHeaderCharset - else if detectedCharset == htmlMetaCharset - result = htmlMetaCharset - - unless result - result = httpHeaderCharset || htmlMetaCharset || detectedCharset - - return result || 'utf-8' - -toUtf8 = (contentType, body) -> - return iconv.decode(body, getCharset(contentType, body)) - -getUrlContent = (urlObj, redirectCount = 5, callback) -> - if _.isString(urlObj) - urlObj = URL.parse urlObj - - parsedUrl = _.pick urlObj, ['host', 'hash', 'pathname', 'protocol', 'port', 'query', 'search', 'hostname'] - - ignoredHosts = RocketChat.settings.get('API_EmbedIgnoredHosts').replace(/\s/g, '').split(',') or [] - if parsedUrl.hostname in ignoredHosts or ipRangeCheck(parsedUrl.hostname, ignoredHosts) - return callback() - - safePorts = RocketChat.settings.get('API_EmbedSafePorts').replace(/\s/g, '').split(',') or [] - if parsedUrl.port and safePorts.length > 0 and parsedUrl.port not in safePorts - return callback() - - data = RocketChat.callbacks.run 'oembed:beforeGetUrlContent', - urlObj: urlObj - parsedUrl: parsedUrl - - if data.attachments? - return callback null, data - - url = URL.format data.urlObj - opts = - url: url - strictSSL: !RocketChat.settings.get 'Allow_Invalid_SelfSigned_Certs' - gzip: true - maxRedirects: redirectCount - headers: - 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36' - - headers = null - statusCode = null - error = null - chunks = [] - chunksTotalLength = 0 - - stream = request opts - stream.on 'response', (response) -> - statusCode = response.statusCode - headers = response.headers - if response.statusCode isnt 200 - return stream.abort() - - stream.on 'data', (chunk) -> - chunks.push chunk - chunksTotalLength += chunk.length - if chunksTotalLength > 250000 - stream.abort() - - stream.on 'end', Meteor.bindEnvironment -> - if error? - return callback null, { - error: error - parsedUrl: parsedUrl - } - - buffer = Buffer.concat(chunks) - - callback null, { - headers: headers - body: toUtf8(headers['content-type'], buffer) - parsedUrl: parsedUrl - statusCode: statusCode - } - - stream.on 'error', (err) -> - error = err - -OEmbed.getUrlMeta = (url, withFragment) -> - getUrlContentSync = Meteor.wrapAsync getUrlContent - - urlObj = URL.parse url - - if withFragment? - queryStringObj = querystring.parse urlObj.query - queryStringObj._escaped_fragment_ = '' - urlObj.query = querystring.stringify queryStringObj - - path = urlObj.pathname - if urlObj.query? - path += '?' + urlObj.query - - urlObj.path = path - - content = getUrlContentSync urlObj, 5 - if !content - return - - if content.attachments? - return content - - metas = undefined - - if content?.body? - metas = {} - content.body.replace /<title[^>]*>([^<]*)<\/title>/gmi, (meta, title) -> - metas.pageTitle ?= he.unescape title - - content.body.replace /<meta[^>]*(?:name|property)=[']([^']*)['][^>]*\scontent=[']([^']*)['][^>]*>/gmi, (meta, name, value) -> - metas[changeCase.camelCase(name)] ?= he.unescape value - - content.body.replace /<meta[^>]*(?:name|property)=["]([^"]*)["][^>]*\scontent=["]([^"]*)["][^>]*>/gmi, (meta, name, value) -> - metas[changeCase.camelCase(name)] ?= he.unescape value - - content.body.replace /<meta[^>]*\scontent=[']([^']*)['][^>]*(?:name|property)=[']([^']*)['][^>]*>/gmi, (meta, value, name) -> - metas[changeCase.camelCase(name)] ?= he.unescape value - - content.body.replace /<meta[^>]*\scontent=["]([^"]*)["][^>]*(?:name|property)=["]([^"]*)["][^>]*>/gmi, (meta, value, name) -> - metas[changeCase.camelCase(name)] ?= he.unescape value - - - if metas.fragment is '!' and not withFragment? - return OEmbed.getUrlMeta url, true - - headers = undefined - - if content?.headers? - headers = {} - for header, value of content.headers - headers[changeCase.camelCase(header)] = value - - if content?.statusCode isnt 200 - return data - - data = RocketChat.callbacks.run 'oembed:afterParseContent', - meta: metas - headers: headers - parsedUrl: content.parsedUrl - content: content - - return data - -OEmbed.getUrlMetaWithCache = (url, withFragment) -> - cache = RocketChat.models.OEmbedCache.findOneById url - if cache? - return cache.data - - data = OEmbed.getUrlMeta url, withFragment - - if data? - try - RocketChat.models.OEmbedCache.createWithIdAndData url, data - catch e - console.error 'OEmbed duplicated record', url - - return data - - return - -getRelevantHeaders = (headersObj) -> - headers = {} - for key, value of headersObj - if key.toLowerCase() in ['contenttype', 'contentlength'] and value?.trim() isnt '' - headers[key] = value - - if Object.keys(headers).length > 0 - return headers - return - -getRelevantMetaTags = (metaObj) -> - tags = {} - for key, value of metaObj - if /^(og|fb|twitter|oembed|msapplication).+|description|title|pageTitle$/.test(key.toLowerCase()) and value?.trim() isnt '' - tags[key] = value - - if Object.keys(tags).length > 0 - return tags - return - -OEmbed.rocketUrlParser = (message) -> - if Array.isArray message.urls - attachments = [] - changed = false - message.urls.forEach (item) -> - if item.ignoreParse is true then return - if item.url.startsWith "grain://" - changed = true - item.meta = - sandstorm: - grain: item.sandstormViewInfo - return - - if not /^https?:\/\//i.test item.url then return - - data = OEmbed.getUrlMetaWithCache item.url - - if data? - if data.attachments - attachments = _.union attachments, data.attachments - else - if data.meta? - item.meta = getRelevantMetaTags data.meta - - if data.headers? - item.headers = getRelevantHeaders data.headers - - item.parsedUrl = data.parsedUrl - changed = true - - if attachments.length - RocketChat.models.Messages.setMessageAttachments message._id, attachments - - if changed is true - RocketChat.models.Messages.setUrlsById message._id, message.urls - - return message - -RocketChat.settings.get 'API_Embed', (key, value) -> - if value - RocketChat.callbacks.add 'afterSaveMessage', OEmbed.rocketUrlParser, RocketChat.callbacks.priority.LOW, 'API_Embed' - else - RocketChat.callbacks.remove 'afterSaveMessage', 'API_Embed' diff --git a/packages/rocketchat-oembed/server/server.js b/packages/rocketchat-oembed/server/server.js new file mode 100644 index 0000000000000000000000000000000000000000..36085f1597f7efd1a42b1e83334ba4985402b15d --- /dev/null +++ b/packages/rocketchat-oembed/server/server.js @@ -0,0 +1,300 @@ +/*globals HTTPInternals, changeCase */ +const URL = Npm.require('url'); + +const querystring = Npm.require('querystring'); + +const request = HTTPInternals.NpmModules.request.module; + +const iconv = Npm.require('iconv-lite'); + +const ipRangeCheck = Npm.require('ip-range-check'); + +const he = Npm.require('he'); + +const jschardet = Npm.require('jschardet'); + +const OEmbed = {}; + +// Detect encoding +// Priority: +// Detected == HTTP Header > Detected == HTML meta > HTTP Header > HTML meta > Detected > Default (utf-8) +// See also: https://www.w3.org/International/questions/qa-html-encoding-declarations.en#quickanswer +const getCharset = function(contentType, body) { + let detectedCharset; + let httpHeaderCharset; + let htmlMetaCharset; + let result; + + contentType = contentType || ''; + + const binary = body.toString('binary'); + const detected = jschardet.detect(binary); + if (detected.confidence > 0.8) { + detectedCharset = detected.encoding.toLowerCase(); + } + const m1 = contentType.match(/charset=([\w\-]+)/i); + if (m1) { + httpHeaderCharset = m1[1].toLowerCase(); + } + const m2 = binary.match(/<meta\b[^>]*charset=["']?([\w\-]+)/i); + if (m2) { + htmlMetaCharset = m2[1].toLowerCase(); + } + if (detectedCharset) { + if (detectedCharset === httpHeaderCharset) { + result = httpHeaderCharset; + } else if (detectedCharset === htmlMetaCharset) { + result = htmlMetaCharset; + } + } + if (!result) { + result = httpHeaderCharset || htmlMetaCharset || detectedCharset; + } + return result || 'utf-8'; +}; + +const toUtf8 = function(contentType, body) { + return iconv.decode(body, getCharset(contentType, body)); +}; + +const getUrlContent = function(urlObj, redirectCount = 5, callback) { + + if (_.isString(urlObj)) { + urlObj = URL.parse(urlObj); + } + + const parsedUrl = _.pick(urlObj, ['host', 'hash', 'pathname', 'protocol', 'port', 'query', 'search', 'hostname']); + const ignoredHosts = RocketChat.settings.get('API_EmbedIgnoredHosts').replace(/\s/g, '').split(',') || []; + if (ignoredHosts.includes(parsedUrl.hostname) || ipRangeCheck(parsedUrl.hostname, ignoredHosts)) { + return callback(); + } + + const safePorts = RocketChat.settings.get('API_EmbedSafePorts').replace(/\s/g, '').split(',') || []; + if (parsedUrl.port && safePorts.length > 0 && (!safePorts.includes(parsedUrl.port))) { + return callback(); + } + + const data = RocketChat.callbacks.run('oembed:beforeGetUrlContent', { + urlObj, + parsedUrl + }); + if (data.attachments != null) { + return callback(null, data); + } + const url = URL.format(data.urlObj); + const opts = { + url, + strictSSL: !RocketChat.settings.get('Allow_Invalid_SelfSigned_Certs'), + gzip: true, + maxRedirects: redirectCount, + headers: { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36' + } + }; + let headers = null; + let statusCode = null; + let error = null; + const chunks = []; + let chunksTotalLength = 0; + const stream = request(opts); + stream.on('response', function(response) { + statusCode = response.statusCode; + headers = response.headers; + if (response.statusCode !== 200) { + return stream.abort(); + } + }); + stream.on('data', function(chunk) { + chunks.push(chunk); + chunksTotalLength += chunk.length; + if (chunksTotalLength > 250000) { + return stream.abort(); + } + }); + stream.on('end', Meteor.bindEnvironment(function() { + if (error != null) { + return callback(null, { + error, + parsedUrl + }); + } + const buffer = Buffer.concat(chunks); + return callback(null, { + headers, + body: toUtf8(headers['content-type'], buffer), + parsedUrl, + statusCode + }); + })); + return stream.on('error', function(err) { + return error = err; + }); +}; + +OEmbed.getUrlMeta = function(url, withFragment) { + const getUrlContentSync = Meteor.wrapAsync(getUrlContent); + const urlObj = URL.parse(url); + if (withFragment != null) { + const queryStringObj = querystring.parse(urlObj.query); + queryStringObj._escaped_fragment_ = ''; + urlObj.query = querystring.stringify(queryStringObj); + let path = urlObj.pathname; + if (urlObj.query != null) { + path += `?${ urlObj.query }`; + } + urlObj.path = path; + } + const content = getUrlContentSync(urlObj, 5); + if (!content) { + return; + } + if (content.attachments != null) { + return content; + } + let metas = undefined; + if (content && content.body) { + metas = {}; + content.body.replace(/<title[^>]*>([^<]*)<\/title>/gmi, function(meta, title) { + return metas.pageTitle != null ? metas.pageTitle : metas.pageTitle = he.unescape(title); + }); + content.body.replace(/<meta[^>]*(?:name|property)=[']([^']*)['][^>]*\scontent=[']([^']*)['][^>]*>/gmi, function(meta, name, value) { + let name1; + return metas[name1 = changeCase.camelCase(name)] != null ? metas[name1] : metas[name1] = he.unescape(value); + }); + content.body.replace(/<meta[^>]*(?:name|property)=["]([^"]*)["][^>]*\scontent=["]([^"]*)["][^>]*>/gmi, function(meta, name, value) { + let name1; + return metas[name1 = changeCase.camelCase(name)] != null ? metas[name1] : metas[name1] = he.unescape(value); + }); + content.body.replace(/<meta[^>]*\scontent=[']([^']*)['][^>]*(?:name|property)=[']([^']*)['][^>]*>/gmi, function(meta, value, name) { + let name1; + return metas[name1 = changeCase.camelCase(name)] != null ? metas[name1] : metas[name1] = he.unescape(value); + }); + content.body.replace(/<meta[^>]*\scontent=["]([^"]*)["][^>]*(?:name|property)=["]([^"]*)["][^>]*>/gmi, function(meta, value, name) { + let name1; + return metas[name1 = changeCase.camelCase(name)] != null ? metas[name1] : metas[name1] = he.unescape(value); + }); + if (metas.fragment === '!' && (withFragment == null)) { + return OEmbed.getUrlMeta(url, true); + } + } + let headers = undefined; + let data = undefined; + + + if (content && content.headers) { + headers = {}; + const headerObj = content.headers; + Object.keys(headerObj).forEach((header) => { + headers[changeCase.camelCase(header)] = headerObj[header]; + }); + } + if (content && content.statusCode !== 200) { + return data; + } + data = RocketChat.callbacks.run('oembed:afterParseContent', { + meta: metas, + headers, + parsedUrl: content.parsedUrl, + content + }); + return data; +}; + +OEmbed.getUrlMetaWithCache = function(url, withFragment) { + const cache = RocketChat.models.OEmbedCache.findOneById(url); + if (cache != null) { + return cache.data; + } + const data = OEmbed.getUrlMeta(url, withFragment); + if (data != null) { + try { + RocketChat.models.OEmbedCache.createWithIdAndData(url, data); + } catch (_error) { + console.error('OEmbed duplicated record', url); + } + return data; + } +}; + +const getRelevantHeaders = function(headersObj) { + const headers = {}; + Object.keys(headersObj).forEach((key) => { + const value = headersObj[key]; + const lowerCaseKey = key.toLowerCase(); + if ((lowerCaseKey === 'contenttype' || lowerCaseKey === 'contentlength') && (value && value.trim() !== '')) { + headers[key] = value; + } + }); + + if (Object.keys(headers).length > 0) { + return headers; + } +}; + +const getRelevantMetaTags = function(metaObj) { + const tags = {}; + Object.keys(metaObj).forEach((key) => { + const value = metaObj[key]; + if (/^(og|fb|twitter|oembed|msapplication).+|description|title|pageTitle$/.test(key.toLowerCase()) && (value && value.trim() !== '')) { + tags[key] = value; + } + }); + + if (Object.keys(tags).length > 0) { + return tags; + } +}; + +OEmbed.rocketUrlParser = function(message) { + if (Array.isArray(message.urls)) { + let attachments = []; + let changed = false; + message.urls.forEach(function(item) { + if (item.ignoreParse === true) { + return; + } + if (item.url.startsWith('grain://')) { + changed = true; + item.meta = { + sandstorm: { + grain: item.sandstormViewInfo + } + }; + return; + } + if (!/^https?:\/\//i.test(item.url)) { + return; + } + const data = OEmbed.getUrlMetaWithCache(item.url); + if (data != null) { + if (data.attachments) { + return attachments = _.union(attachments, data.attachments); + } else { + if (data.meta != null) { + item.meta = getRelevantMetaTags(data.meta); + } + if (data.headers != null) { + item.headers = getRelevantHeaders(data.headers); + } + item.parsedUrl = data.parsedUrl; + return changed = true; + } + } + }); + if (attachments.length) { + RocketChat.models.Messages.setMessageAttachments(message._id, attachments); + } + if (changed === true) { + RocketChat.models.Messages.setUrlsById(message._id, message.urls); + } + } + return message; +}; + +RocketChat.settings.get('API_Embed', function(key, value) { + if (value) { + return RocketChat.callbacks.add('afterSaveMessage', OEmbed.rocketUrlParser, RocketChat.callbacks.priority.LOW, 'API_Embed'); + } else { + return RocketChat.callbacks.remove('afterSaveMessage', 'API_Embed'); + } +}); 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-theme/client/imports/base.less b/packages/rocketchat-theme/client/imports/base.less index b6c1c363391881cba1418190784327394f966294..8340766472acf14937af5cb6208a00cfa47ef90e 100644 --- a/packages/rocketchat-theme/client/imports/base.less +++ b/packages/rocketchat-theme/client/imports/base.less @@ -1302,7 +1302,7 @@ label.required::after { &.toggle { font-size: 0; - > span { + > label { display: inline-block; width: calc(~"100% - 40px"); font-size: 14px; @@ -3054,6 +3054,7 @@ label.required::after { max-height: 200px; max-width: 100%; opacity: 0; + cursor: pointer; } } @@ -4996,7 +4997,7 @@ a + br.only-after-a { border-width: 0; } - .stream-info { + .users-typing { display: none; } 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-admin/client/users/adminUsers.html b/packages/rocketchat-ui-admin/client/users/adminUsers.html index bf7827daadfb322ef84868aad08e9a253838842a..46e17936f746a749fb7b3898226e5a855316bb94 100644 --- a/packages/rocketchat-ui-admin/client/users/adminUsers.html +++ b/packages/rocketchat-ui-admin/client/users/adminUsers.html @@ -29,6 +29,8 @@ <th class="content-background-color border-component-color" width="34%">{{_ "Name"}}</th> <th class="content-background-color border-component-color" width="33%">{{_ "Username"}}</th> <th class="content-background-color border-component-color" width="33%">{{_ "Email"}}</th> + <th class="content-background-color border-component-color" width="33%">{{_ "Roles"}}</th> + <th class="content-background-color border-component-color" width="33%">{{_ "Status"}}</th> </tr> </thead> <tbody> @@ -42,10 +44,13 @@ <td class="border-component-color">{{name}}</td> <td class="border-component-color">{{username}}</td> <td class="border-component-color">{{emailAddress}}</td> + <td class="border-component-color">{{roles}}</td> + <td class="border-component-color">{{status}}</td> </tr> {{/each}} </tbody> </table> + {{#if hasMore}} <button class="button secondary load-more {{isLoading}}">{{_ "Load_more"}}</button> {{/if}} @@ -58,3 +63,4 @@ {{/with}} </div> </template> + diff --git a/packages/rocketchat-ui-flextab/client/tabs/membersList.js b/packages/rocketchat-ui-flextab/client/tabs/membersList.js index c9e35fcd0a9268277c062c10a648c9d71a6731de..f6a92be62667b6b182830e6d04e8fdcb69a9b394 100644 --- a/packages/rocketchat-ui-flextab/client/tabs/membersList.js +++ b/packages/rocketchat-ui-flextab/client/tabs/membersList.js @@ -53,7 +53,11 @@ Template.membersList.helpers({ }; }); - users = _.sortBy(users, u => u.user.username); + if (RocketChat.settings.get('UI_Use_Real_Name')) { + users = _.sortBy(users, u => u.user.name); + } else { + users = _.sortBy(users, u => u.user.username); + } // show online users first. // sortBy is stable, so we can do this users = _.sortBy(users, u => u.status == null); diff --git a/packages/rocketchat-ui-flextab/client/tabs/userInfo.html b/packages/rocketchat-ui-flextab/client/tabs/userInfo.html index 7b5d52788e1fb21bc1a74bd877ec80e9a4afb5cd..4b1dfaffa3c5c667ad58b27cb6a5a19d9a365817 100644 --- a/packages/rocketchat-ui-flextab/client/tabs/userInfo.html +++ b/packages/rocketchat-ui-flextab/client/tabs/userInfo.html @@ -41,67 +41,69 @@ </div> {{/with}} <nav> - {{#if user.active}} - {{> videoButtons}} - {{/if}} - {{#if isDirect}} - {{#if isBlocker}} - <button class='button button-block tertiary unblock-user'><span><i class='icon-block'></i> {{_ "Unblock_User"}}</span></button> - {{else}} - <button class='button button-block danger block-user'><span><i class='icon-block'></i> {{_ "Block_User"}}</span></button> + {{#unless isSelf user.username}} + {{#if user.active}} + {{> videoButtons}} {{/if}} - {{/if}} - - {{#if showAll}} - {{#if canDirectMessage user.username}} - <button class='button button-block primary pvt-msg'><span><i class='icon-chat'></i> {{_ "Conversation"}}</span></button> {{/if}} - {{#if canSetOwner}} - {{#if isOwner}} - <button class="button button-block danger unset-owner"><span>{{_ "Remove_as_owner"}}</span></button> + {{#if isDirect}} + {{#if isBlocker}} + <button class='button button-block tertiary unblock-user'><span><i class='icon-block'></i> {{_ "Unblock_User"}}</span></button> {{else}} - <button class="button button-block tertiary set-owner"><span>{{_ "Set_as_owner"}}</span></button> + <button class='button button-block danger block-user'><span><i class='icon-block'></i> {{_ "Block_User"}}</span></button> {{/if}} {{/if}} - {{#if canSetModerator}} - {{#if isModerator}} - <button class="button button-block danger unset-moderator"><span>{{_ "Remove_as_moderator"}}</span></button> - {{else}} - <button class="button button-block tertiary set-moderator"><span>{{_ "Set_as_moderator"}}</span></button> + + {{#if showAll}} + {{#if canDirectMessage user.username}} + <button class='button button-block primary pvt-msg'><span><i class='icon-chat'></i> {{_ "Conversation"}}</span></button> {{/if}} + {{#if canSetOwner}} + {{#if isOwner}} + <button class="button button-block danger unset-owner"><span>{{_ "Remove_as_owner"}}</span></button> + {{else}} + <button class="button button-block tertiary set-owner"><span>{{_ "Set_as_owner"}}</span></button> + {{/if}} {{/if}} - {{/if}} - {{#if canMuteUser}} - {{#if userMuted}} - <button class="button button-block secondary unmute-user primary"><span>{{_ "Unmute_user"}}</span></button> - {{else}} - <button class="button button-block danger mute-user"><span>{{_ "Mute_user"}}</span></button> + {{#if canSetModerator}} + {{#if isModerator}} + <button class="button button-block danger unset-moderator"><span>{{_ "Remove_as_moderator"}}</span></button> + {{else}} + <button class="button button-block tertiary set-moderator"><span>{{_ "Set_as_moderator"}}</span></button> + {{/if}} + {{/if}} + {{#if canMuteUser}} + {{#if userMuted}} + <button class="button button-block secondary unmute-user primary"><span>{{_ "Unmute_user"}}</span></button> + {{else}} + <button class="button button-block danger mute-user"><span>{{_ "Mute_user"}}</span></button> + {{/if}} + {{/if}} + {{#if canRemoveUser}} + <button class="button button-block danger remove-user"><span>{{_ "Remove_from_room"}}</span></button> {{/if}} {{/if}} - {{#if canRemoveUser}} - <button class="button button-block danger remove-user"><span>{{_ "Remove_from_room"}}</span></button> - {{/if}} - {{/if}} - {{#unless hideAdminControls}} - {{#if hasPermission 'edit-other-user-info'}} - <button class='button button-block primary edit-user'><span><i class='icon-edit'></i> {{_ "Edit"}}</span></button> - {{/if}} - {{#if hasPermission 'assign-admin-role'}} - {{#if hasAdminRole}} - <button class='button button-block danger remove-admin'><span><i class='icon-shield'></i> {{_ "Remove_Admin"}}</span></button> - {{else}} - <button class='button button-block secondary make-admin'><span><i class='icon-shield'></i> {{_ "Make_Admin"}}</span></button> + {{#unless hideAdminControls}} + {{#if hasPermission 'edit-other-user-info'}} + <button class='button button-block primary edit-user'><span><i class='icon-edit'></i> {{_ "Edit"}}</span></button> {{/if}} - {{/if}} - {{#if hasPermission 'edit-other-user-active-status'}} - {{#if active}} - <button class='button button-block danger deactivate'><span><i class='icon-block'></i> {{_ "Deactivate"}}</span></button> - {{else}} - <button class='button button-block secondary activate'><span><i class='icon-ok-circled'></i> {{_ "Activate"}}</span></button> + {{#if hasPermission 'assign-admin-role'}} + {{#if hasAdminRole}} + <button class='button button-block danger remove-admin'><span><i class='icon-shield'></i> {{_ "Remove_Admin"}}</span></button> + {{else}} + <button class='button button-block secondary make-admin'><span><i class='icon-shield'></i> {{_ "Make_Admin"}}</span></button> + {{/if}} {{/if}} - {{/if}} - {{#if hasPermission 'delete-user'}} - <button class='button button-block danger delete'><span><i class='icon-trash'></i> {{_ "Delete"}}</span></button> - {{/if}} + {{#if hasPermission 'edit-other-user-active-status'}} + {{#if active}} + <button class='button button-block danger deactivate'><span><i class='icon-block'></i> {{_ "Deactivate"}}</span></button> + {{else}} + <button class='button button-block secondary activate'><span><i class='icon-ok-circled'></i> {{_ "Activate"}}</span></button> + {{/if}} + {{/if}} + {{#if hasPermission 'delete-user'}} + <button class='button button-block danger delete'><span><i class='icon-trash'></i> {{_ "Delete"}}</span></button> + {{/if}} + {{/unless}} {{/unless}} {{#if showAll}} diff --git a/packages/rocketchat-ui-flextab/client/tabs/userInfo.js b/packages/rocketchat-ui-flextab/client/tabs/userInfo.js index 2ec2dbc6cd05cb344c1be576b4afb34f4bb85da3..e64f60b0e5fce222d385a1dff8d200d79d46a0aa 100644 --- a/packages/rocketchat-ui-flextab/client/tabs/userInfo.js +++ b/packages/rocketchat-ui-flextab/client/tabs/userInfo.js @@ -47,6 +47,11 @@ Template.userInfo.helpers({ return RocketChat.authz.hasAllPermission('create-d') && user && user.username !== username; }, + isSelf(username) { + const user = Meteor.user(); + return user && user.username === username; + }, + linkedinUsername() { const user = Template.instance().user.get(); if (user && user.services && user.services.linkedin && user.services.linkedin.publicProfileUrl) { diff --git a/packages/rocketchat-ui-master/client/main.js b/packages/rocketchat-ui-master/client/main.js index ba187b5e968688c9a3c04bfb68820d774b7b5ba7..9e91eea36872af81eab8b181c983ef637197b076 100644 --- a/packages/rocketchat-ui-master/client/main.js +++ b/packages/rocketchat-ui-master/client/main.js @@ -110,7 +110,7 @@ Template.main.helpers({ return RocketChat.settings.get('Site_Name'); }, logged() { - if (Meteor.userId() != null || (RocketChat.settings.get('Accounts_AllowAnonymousAccess') === true && Session.get('forceLogin') !== true)) { + if (Meteor.userId() != null || (RocketChat.settings.get('Accounts_AllowAnonymousRead') === true && Session.get('forceLogin') !== true)) { $('html').addClass('noscroll').removeClass('scroll'); return true; } else { @@ -134,7 +134,7 @@ Template.main.helpers({ return ready; }, hasUsername() { - return (Meteor.userId() != null && Meteor.user().username != null) || (Meteor.userId() == null && RocketChat.settings.get('Accounts_AllowAnonymousAccess') === true); + return (Meteor.userId() != null && Meteor.user().username != null) || (Meteor.userId() == null && RocketChat.settings.get('Accounts_AllowAnonymousRead') === true); }, requirePasswordChange() { const user = Meteor.user(); diff --git a/packages/rocketchat-ui-message/client/messageBox.coffee b/packages/rocketchat-ui-message/client/messageBox.coffee index 0052f3046146981a2e81895ecaec7b6f854b9b29..2587fda0c301ecbd128e41861c4d0fd4e683bed1 100644 --- a/packages/rocketchat-ui-message/client/messageBox.coffee +++ b/packages/rocketchat-ui-message/client/messageBox.coffee @@ -124,8 +124,11 @@ Template.messageBox.helpers showSandstorm: -> return Meteor.settings.public.sandstorm && !Meteor.isCordova - isAnonymous: -> - return not Meteor.userId()? and RocketChat.settings.get('Accounts_AllowAnonymousAccess') is true + 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/) @@ -186,6 +189,15 @@ Template.messageBox.events 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') diff --git a/packages/rocketchat-ui-message/client/messageBox.html b/packages/rocketchat-ui-message/client/messageBox.html index 02e98fe5a1072bbbcf279385ab5004ff0a8dd038..d8f6c14df52208a1db06d2f7f28347a61793892d 100644 --- a/packages/rocketchat-ui-message/client/messageBox.html +++ b/packages/rocketchat-ui-message/client/messageBox.html @@ -143,9 +143,12 @@ <button class="button join"><span><i class="icon-login"></i> {{_ "join"}}</span></button> </div> {{/if}} - {{#if isAnonymous}} + {{#if anonymousRead}} <div> - <button class="button register"><span><i class="icon-login"></i> {{_ "Register_or_login_to_send_messages"}}</span></button> + <button class="button register"><span>{{_ "Sign_in_to_start_talking"}}</span></button> + {{#if anonymousWrite}} + <button class="button register-anonymous"><span>{{_ "Or_talk_as_anonymous"}}</span></button> + {{/if}} </div> {{/if}} {{/with}} diff --git a/packages/rocketchat-ui-sidenav/client/accountBox.js b/packages/rocketchat-ui-sidenav/client/accountBox.js index d8dacd1406b2ab4408aa904f5410f1aa0f6a5e49..9825f9e1d68fe0f9c7f87af895993e723619d3dc 100644 --- a/packages/rocketchat-ui-sidenav/client/accountBox.js +++ b/packages/rocketchat-ui-sidenav/client/accountBox.js @@ -1,8 +1,9 @@ Template.accountBox.helpers({ myUserInfo() { - if (Meteor.user() == null && RocketChat.settings.get('Accounts_AllowAnonymousAccess')) { + if (Meteor.user() == null && RocketChat.settings.get('Accounts_AllowAnonymousRead')) { return { name: t('Anonymous'), + fname: t('Anonymous'), status: 'online', visualStatus: t('online'), username: 'anonymous' @@ -24,12 +25,12 @@ Template.accountBox.helpers({ break; } return { - name: Session.get(`user_${ username }_name`), + name: Session.get(`user_${ username }_name`) || username, status: Session.get(`user_${ username }_status`), visualStatus, _id: Meteor.userId(), username, - fname: name + fname: name || username }; }, @@ -50,7 +51,7 @@ Template.accountBox.events({ }, 'click .account-box'() { - if (Meteor.userId() == null && RocketChat.settings.get('Accounts_AllowAnonymousAccess')) { + if (Meteor.userId() == null && RocketChat.settings.get('Accounts_AllowAnonymousRead')) { return; } diff --git a/packages/rocketchat-ui-sidenav/client/createCombinedFlex.html b/packages/rocketchat-ui-sidenav/client/createCombinedFlex.html index acc8e3230984e4dfb34f29eef7bb3475fb993f7c..bce0f8330976a13384dc4316d9910a7e5d816c74 100644 --- a/packages/rocketchat-ui-sidenav/client/createCombinedFlex.html +++ b/packages/rocketchat-ui-sidenav/client/createCombinedFlex.html @@ -8,18 +8,18 @@ <div class="wrapper"> <h4>{{_ "Create_new" }}</h4> <div class="input-line no-icon"> - <span>{{_ "Name"}}</span> + <label for="channel-name">{{_ "Name"}}</label> <input type="text" id="channel-name" class="required" dir="auto" placeholder="{{_ 'Enter_name_here'}}"> </div> <div class="input-line toggle"> - <span>{{_ "Private"}}</span> + <label for="channel-type">{{_ "Private"}}</label> <div class="input checkbox toggle"> <input type="checkbox" id="channel-type" {{privateSwitchDisabled}} {{privateSwitchChecked}}> <label class="color-tertiary-font-color" for="channel-type"></label> </div> </div> <div class="input-line toggle"> - <span>{{_ "Read_only_channel"}}</span> + <label for="channel-ro">{{_ "Read_only_channel"}}</label> <div class="input checkbox toggle"> <input type="checkbox" id="channel-ro"> <label class="color-tertiary-font-color" for="channel-ro"></label> diff --git a/packages/rocketchat-ui-sidenav/client/createCombinedFlex.js b/packages/rocketchat-ui-sidenav/client/createCombinedFlex.js index 8f17225e968420d337256d9252b5384f2462cfa2..4d4c16a39ab6aa1f09141d12e04aa8043134df95 100644 --- a/packages/rocketchat-ui-sidenav/client/createCombinedFlex.js +++ b/packages/rocketchat-ui-sidenav/client/createCombinedFlex.js @@ -111,7 +111,6 @@ Template.createCombinedFlex.events({ if (!err) { return Meteor.call(createRoute, name, instance.selectedUsers.get(), readOnly, function(err, result) { if (err) { - console.log(err); if (err.error === 'error-invalid-name') { instance.error.set({ invalid: true }); return; @@ -137,7 +136,6 @@ Template.createCombinedFlex.events({ return FlowRouter.go(successRoute, { name }, FlowRouter.current().queryParams); }); } else { - console.log(err); return instance.error.set({ fields: err }); } } diff --git a/packages/rocketchat-ui-sidenav/client/directMessages.js b/packages/rocketchat-ui-sidenav/client/directMessages.js index ae73370b526ffb36c996db5696b38c796370cbb3..68e09b80d82ec80f74255cb75e581a4b8f745ed0 100644 --- a/packages/rocketchat-ui-sidenav/client/directMessages.js +++ b/packages/rocketchat-ui-sidenav/client/directMessages.js @@ -7,6 +7,7 @@ Template.directMessages.helpers({ rooms() { const query = { t: { $in: ['d']}, f: { $ne: true }, open: true }; + const sort = { 't': 1 }; if (Meteor.user() && Meteor.user().settings && Meteor.user().settings.preferences && Meteor.user().settings.preferences.unreadRoomsMode) { query.$or = [ @@ -15,6 +16,11 @@ Template.directMessages.helpers({ ]; } - return ChatSubscription.find(query, { sort: { 't': 1, 'name': 1 }}); + if (RocketChat.settings.get('UI_Use_Real_Name')) { + sort.fname = 1; + } + sort.name = 1; + + return ChatSubscription.find(query, { sort }); } }); diff --git a/packages/rocketchat-ui-sidenav/client/toolbar.js b/packages/rocketchat-ui-sidenav/client/toolbar.js index baa495d6a69592686b4be5aef3bab53282e997c4..c044c98dd280310d214e7feca0dd901559b3e1c2 100644 --- a/packages/rocketchat-ui-sidenav/client/toolbar.js +++ b/packages/rocketchat-ui-sidenav/client/toolbar.js @@ -158,7 +158,11 @@ Template.toolbar.helpers({ query.t = 'd'; } - query.name = new RegExp((RegExp.escape(filterText)), 'i'); + const searchQuery = new RegExp((RegExp.escape(filterText)), 'i'); + query.$or = [ + { name: searchQuery }, + { fname: searchQuery } + ]; resultsFromClient = collection.find(query, {limit: 20, sort: {unread: -1, ls: -1}}).fetch(); diff --git a/packages/rocketchat-ui/client/lib/accounts.js b/packages/rocketchat-ui/client/lib/accounts.js index 8d220aefcd8140538f81c5458fcf6c45ebb28ebd..5045fbff9dccef2d6b019959464fec6bd73cd9af 100644 --- a/packages/rocketchat-ui/client/lib/accounts.js +++ b/packages/rocketchat-ui/client/lib/accounts.js @@ -3,6 +3,7 @@ Accounts.onEmailVerificationLink(function(token, done) { Accounts.verifyEmail(token, function(error) { if (error == null) { toastr.success(t('Email_verified')); + Meteor.call('afterVerifyEmail'); } return done(); }); 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/collections.js b/packages/rocketchat-ui/client/lib/collections.js index 06b777c5ee6ce25be22d4c0d17ab8c807d07b393..3d95243fbce0653d373cbb94a9534095436fb176 100644 --- a/packages/rocketchat-ui/client/lib/collections.js +++ b/packages/rocketchat-ui/client/lib/collections.js @@ -17,7 +17,7 @@ RocketChat.models.Messages = _.extend({}, RocketChat.models.Messages, this.ChatM Meteor.startup(() => { Tracker.autorun(() => { - if (!Meteor.userId() && RocketChat.settings.get('Accounts_AllowAnonymousAccess') === true) { + if (!Meteor.userId() && RocketChat.settings.get('Accounts_AllowAnonymousRead') === true) { this.CachedChatRoom.init(); this.CachedChatSubscription.ready.set(true); } diff --git a/packages/rocketchat-ui/client/lib/fireEvent.js b/packages/rocketchat-ui/client/lib/fireEvent.js index 4238ca12b395c2b54968b72c4b9859c823937d66..a3456d3e61a6d235a9b1d180b6c1be4c54ea2718 100644 --- a/packages/rocketchat-ui/client/lib/fireEvent.js +++ b/packages/rocketchat-ui/client/lib/fireEvent.js @@ -1,10 +1,18 @@ -window.fireGlobalEvent = (eventName, params) => { +window.fireGlobalEvent = function _fireGlobalEvent(eventName, params) { window.dispatchEvent(new CustomEvent(eventName, {detail: params})); - if (RocketChat.settings.get('Iframe_Integration_send_enable') === true) { - parent.postMessage({ - eventName, - data: params - }, RocketChat.settings.get('Iframe_Integration_send_target_origin')); - } + Tracker.autorun((computation) => { + const enabled = RocketChat.settings.get('Iframe_Integration_send_enable'); + if (enabled === undefined) { + return; + } + computation.stop(); + if (enabled) { + parent.postMessage({ + eventName, + data: params + }, RocketChat.settings.get('Iframe_Integration_send_target_origin')); + } + }); }; + diff --git a/packages/rocketchat-ui/client/views/app/room.coffee b/packages/rocketchat-ui/client/views/app/room.coffee index 05f1646ef9ce023abf9a20278b2fd7e39625a9a0..edd6c1b8a5dc159718a37d7c0c5df6738a96ec65 100644 --- a/packages/rocketchat-ui/client/views/app/room.coffee +++ b/packages/rocketchat-ui/client/views/app/room.coffee @@ -181,7 +181,7 @@ Template.room.helpers if room.t isnt 'c' return true - if RocketChat.settings.get('Accounts_AllowAnonymousAccess') is true + if RocketChat.settings.get('Accounts_AllowAnonymousRead') is true return true if RocketChat.authz.hasAllPermission('preview-c-room') diff --git a/packages/rocketchat-webrtc/WebRTCClass.coffee b/packages/rocketchat-webrtc/WebRTCClass.coffee new file mode 100644 index 0000000000000000000000000000000000000000..a957e411d7cc480832d41bd9cdc3ce66a4715c50 --- /dev/null +++ b/packages/rocketchat-webrtc/WebRTCClass.coffee @@ -0,0 +1,823 @@ +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/server/methods/afterVerifyEmail.js b/server/methods/afterVerifyEmail.js new file mode 100644 index 0000000000000000000000000000000000000000..7b92f596167bb016ec8df36ede3bee8d8f40cd06 --- /dev/null +++ b/server/methods/afterVerifyEmail.js @@ -0,0 +1,20 @@ +Meteor.methods({ + afterVerifyEmail() { + const userId = Meteor.userId(); + + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'afterVerifyEmail' + }); + } + + const user = RocketChat.models.Users.findOneById(userId); + + const verifiedEmail = _.find(user.emails, (email) => email.verified); + + if (verifiedEmail) { + RocketChat.models.Roles.addUserRoles(user._id, 'user'); + RocketChat.models.Roles.removeUserRoles(user._id, 'anonymous'); + } + } +}); diff --git a/server/methods/canAccessRoom.js b/server/methods/canAccessRoom.js index 8c4a04ad98a97e31fd6907f00879ec05de5b0641..dbb1d468b448a67a6dfac2a5c72b648996756b66 100644 --- a/server/methods/canAccessRoom.js +++ b/server/methods/canAccessRoom.js @@ -5,7 +5,7 @@ Meteor.methods({ let user; - if (!userId && RocketChat.settings.get('Accounts_AllowAnonymousAccess') === false) { + if (!userId && RocketChat.settings.get('Accounts_AllowAnonymousRead') === false) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'canAccessRoom' }); diff --git a/server/methods/getUsernameSuggestion.js b/server/methods/getUsernameSuggestion.js index 30440f1513975c99ca3d758d6f4da27efb97bad3..265342330055420b71f316b1068329a233129faf 100644 --- a/server/methods/getUsernameSuggestion.js +++ b/server/methods/getUsernameSuggestion.js @@ -92,7 +92,7 @@ function generateSuggestion(user) { } if (usernames.length === 0 || usernames[0].length === 0) { - usernames.push('user'); + usernames.push(RocketChat.settings.get('Accounts_DefaultUsernamePrefixSuggestion')); } let index = 0; diff --git a/server/methods/loadHistory.js b/server/methods/loadHistory.js index 82f9d01590bb2cd8e8c0c1c9787ea2d67f28851d..01d1720ec5c0d85b759ae4f32c31baecc91d35b6 100644 --- a/server/methods/loadHistory.js +++ b/server/methods/loadHistory.js @@ -21,7 +21,7 @@ Meteor.methods({ loadHistory(rid, end, limit = 20, ls) { check(rid, String); - if (!Meteor.userId() && RocketChat.settings.get('Accounts_AllowAnonymousAccess') === false) { + if (!Meteor.userId() && RocketChat.settings.get('Accounts_AllowAnonymousRead') === false) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'loadHistory' }); @@ -34,7 +34,7 @@ Meteor.methods({ return false; } - const canAnonymous = RocketChat.settings.get('Accounts_AllowAnonymousAccess'); + const canAnonymous = RocketChat.settings.get('Accounts_AllowAnonymousRead'); const canPreview = RocketChat.authz.hasPermission(fromId, 'preview-c-room'); if (room.t === 'c' && !canAnonymous && !canPreview && room.usernames.indexOf(room.username) === -1) { return false; diff --git a/server/methods/registerUser.js b/server/methods/registerUser.js index a4e45bf0fa871bcf5ff7f6b9cae90ec716367751..17c0ceb71b149548bce2379f44efd857692ab760 100644 --- a/server/methods/registerUser.js +++ b/server/methods/registerUser.js @@ -1,11 +1,25 @@ Meteor.methods({ registerUser(formData) { - check(formData, Match.ObjectIncluding({ - email: String, - pass: String, - name: String, - secretURL: Match.Optional(String) - })); + const AllowAnonymousRead = RocketChat.settings.get('Accounts_AllowAnonymousRead'); + const AllowAnonymousWrite = RocketChat.settings.get('Accounts_AllowAnonymousWrite'); + if (AllowAnonymousRead === true && AllowAnonymousWrite === true && formData.email == null) { + const userId = Accounts.insertUserDoc({}, { + globalRoles: [ + 'anonymous' + ] + }); + + const { id, token } = Accounts._loginUser(this, userId); + + return { id, token }; + } else { + check(formData, Match.ObjectIncluding({ + email: String, + pass: String, + name: String, + secretURL: Match.Optional(String) + })); + } if (RocketChat.settings.get('Accounts_RegistrationForm') === 'Disabled') { throw new Meteor.Error('error-user-registration-disabled', 'User registration is disabled', { method: 'registerUser' }); diff --git a/server/methods/saveUserProfile.js b/server/methods/saveUserProfile.js index a70d7aab30f521a45c23302c785036ef1f7ba7a8..7f78f1c99e399c142121f52807efd991316c58aa 100644 --- a/server/methods/saveUserProfile.js +++ b/server/methods/saveUserProfile.js @@ -31,17 +31,6 @@ Meteor.methods({ } return true; } - if ((settings.newPassword) && RocketChat.settings.get('Accounts_AllowPasswordChange') === true) { - if (!checkPassword(user, settings.typedPassword)) { - throw new Meteor.Error('error-invalid-password', 'Invalid password', { - method: 'saveUserProfile' - }); - } - - Accounts.setPassword(Meteor.userId(), settings.newPassword, { - logout: false - }); - } if (settings.realname) { RocketChat.setRealName(Meteor.userId(), settings.realname); @@ -61,6 +50,19 @@ Meteor.methods({ Meteor.call('setEmail', settings.email); } + // Should be the last chack to prevent error when trying to check password for users without password + if ((settings.newPassword) && RocketChat.settings.get('Accounts_AllowPasswordChange') === true) { + if (!checkPassword(user, settings.typedPassword)) { + throw new Meteor.Error('error-invalid-password', 'Invalid password', { + method: 'saveUserProfile' + }); + } + + Accounts.setPassword(Meteor.userId(), settings.newPassword, { + logout: false + }); + } + RocketChat.models.Users.setProfile(Meteor.userId(), {}); RocketChat.saveCustomFields(Meteor.userId(), customFields); diff --git a/server/publications/room.js b/server/publications/room.js index 8ca4f3248a97d30264116aa1873220e1963fcd46..76a2f29ed1b841cb8978a884574b8363902ef1a0 100644 --- a/server/publications/room.js +++ b/server/publications/room.js @@ -40,7 +40,7 @@ const roomMap = (record) => { Meteor.methods({ 'rooms/get'(updatedAt) { if (!Meteor.userId()) { - if (RocketChat.settings.get('Accounts_AllowAnonymousAccess') === true) { + if (RocketChat.settings.get('Accounts_AllowAnonymousRead') === true) { return RocketChat.models.Rooms.findByDefaultAndTypes(true, ['c'], options).fetch(); } return []; @@ -59,7 +59,7 @@ Meteor.methods({ }, getRoomByTypeAndName(type, name) { - if (!Meteor.userId() && RocketChat.settings.get('Accounts_AllowAnonymousAccess') === false) { + if (!Meteor.userId() && RocketChat.settings.get('Accounts_AllowAnonymousRead') === false) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getRoomByTypeAndName' }); } diff --git a/server/publications/spotlight.js b/server/publications/spotlight.js index ceccb8f4c9e62b36e679cc3c5463ac24e6d6846f..babbe04290c01aa51d517110d8f6af10b63f755f 100644 --- a/server/publications/spotlight.js +++ b/server/publications/spotlight.js @@ -19,7 +19,7 @@ Meteor.methods({ const regex = new RegExp(s.trim(s.escapeRegExp(text)), 'i'); if (this.userId == null) { - if (RocketChat.settings.get('Accounts_AllowAnonymousAccess') === true) { + if (RocketChat.settings.get('Accounts_AllowAnonymousRead') === true) { result.rooms = RocketChat.models.Rooms.findByNameAndTypeNotDefault(regex, 'c', roomOptions).fetch(); } return result; diff --git a/server/startup/migrations/v093.js b/server/startup/migrations/v093.js new file mode 100644 index 0000000000000000000000000000000000000000..ee2b645e1130f299b33b71b237c37b3e1c54313c --- /dev/null +++ b/server/startup/migrations/v093.js @@ -0,0 +1,32 @@ +RocketChat.Migrations.add({ + version: 93, + up() { + + if (RocketChat && RocketChat.models && RocketChat.models.Settings) { + const setting = RocketChat.models.Settings.findOne({ _id: 'Accounts_AllowAnonymousAccess' }); + if (setting && setting.value === true) { + RocketChat.models.Settings.update({ _id: 'Accounts_AllowAnonymousRead' }, { $set: { value: setting.value } }); + } + } + + const query = { + _id: { + $in: [ + 'view-c-room', + 'view-history', + 'view-joined-room', + 'view-p-room', + 'preview-c-room' + ] + } + }; + + const update = { + $addToSet: { + roles: 'anonymous' + } + }; + + RocketChat.models.Permissions.update(query, update, { multi: true }); + } +}); diff --git a/tests/end-to-end/api/01-users.js b/tests/end-to-end/api/01-users.js index e5fa8fc3eb083a1168949bd1086d603ea945844c..6d8e42bc3755dff0e20a9876c81f5d8accba1b98 100644 --- a/tests/end-to-end/api/01-users.js +++ b/tests/end-to-end/api/01-users.js @@ -5,7 +5,6 @@ import {getCredentials, api, login, request, credentials, apiEmail, apiUsername, targetUser, log} from '../../data/api-data.js'; import {adminEmail, password} from '../../data/user.js'; import {imgURL} from '../../data/interactions.js'; -import supertest from 'supertest'; describe('Users', function() { this.retries(0); @@ -14,133 +13,257 @@ describe('Users', function() { it('/users.create', (done) => { request.post(api('users.create')) - .set(credentials) - .send({ - email: apiEmail, - name: apiUsername, - username: apiUsername, - password, - active: true, - roles: ['user'], - joinDefaultChannels: true, - verified:true - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.deep.property('user.username', apiUsername); - expect(res.body).to.have.deep.property('user.emails[0].address', apiEmail); - expect(res.body).to.have.deep.property('user.active', true); - expect(res.body).to.have.deep.property('user.name', apiUsername); - targetUser._id = res.body.user._id; - }) - .end(done); + .set(credentials) + .send({ + email: apiEmail, + name: apiUsername, + username: apiUsername, + password, + active: true, + roles: ['user'], + joinDefaultChannels: true, + verified:true + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.deep.property('user.username', apiUsername); + expect(res.body).to.have.deep.property('user.emails[0].address', apiEmail); + expect(res.body).to.have.deep.property('user.active', true); + expect(res.body).to.have.deep.property('user.name', apiUsername); + targetUser._id = res.body.user._id; + }) + .end(done); }); it('/users.info', (done) => { request.get(api('users.info')) - .set(credentials) - .query({ - userId: targetUser._id - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.deep.property('user.username', apiUsername); - expect(res.body).to.have.deep.property('user.emails[0].address', apiEmail); - expect(res.body).to.have.deep.property('user.active', true); - expect(res.body).to.have.deep.property('user.name', apiUsername); - }) - .end(done); + .set(credentials) + .query({ + userId: targetUser._id + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.deep.property('user.username', apiUsername); + expect(res.body).to.have.deep.property('user.emails[0].address', apiEmail); + expect(res.body).to.have.deep.property('user.active', true); + expect(res.body).to.have.deep.property('user.name', apiUsername); + }) + .end(done); }); it('/users.getPresence', (done) => { request.get(api('users.getPresence')) - .set(credentials) - .query({ - userId: targetUser._id - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.deep.property('presence', 'offline'); - }) - .end(done); + .set(credentials) + .query({ + userId: targetUser._id + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.deep.property('presence', 'offline'); + }) + .end(done); }); it('/users.list', (done) => { request.get(api('users.list')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('count'); - expect(res.body).to.have.property('total'); - }) - .end(done); + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + }) + .end(done); }); it.skip('/users.list', (done) => { - //filtering user list + //filtering user list request.get(api('users.list')) - .set(credentials) - .query({ - name: { '$regex': 'g' } - }) - .field('username', 1) - .sort('createdAt', -1) - .expect(log) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('count'); - expect(res.body).to.have.property('total'); - }) - .end(done); + .set(credentials) + .query({ + name: { '$regex': 'g' } + }) + .field('username', 1) + .sort('createdAt', -1) + .expect(log) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + }) + .end(done); }); it.skip('/users.setAvatar', (done) => { request.post(api('users.setAvatar')) - .set(credentials) - .attach('avatarUrl', imgURL) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); + .set(credentials) + .attach('avatarUrl', imgURL) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); }); it('/users.update', (done) => { request.post(api('users.update')) + .set(credentials) + .send({ + userId: targetUser._id, + data :{ + email: apiEmail, + name: `edited${ apiUsername }`, + username: `edited${ apiUsername }`, + password, + active: true, + roles: ['user'] + } + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.deep.property('user.username', `edited${ apiUsername }`); + expect(res.body).to.have.deep.property('user.emails[0].address', apiEmail); + expect(res.body).to.have.deep.property('user.active', true); + expect(res.body).to.have.deep.property('user.name', `edited${ apiUsername }`); + }) + .end(done); + }); + + describe('/users.createToken', () => { + let user; + beforeEach((done) => { + const username = `user.test.${ Date.now() }`; + const email = `${ username }@rocket.chat`; + request.post(api('users.create')) .set(credentials) + .send({ email, name: username, username, password }) + .end((err, res) => { + user = res.body.user; + done(); + }); + }); + + let userCredentials; + beforeEach((done) => { + request.post(api('login')) .send({ - userId: targetUser._id, - data :{ - email: apiEmail, - name: `edited${ apiUsername }`, - username: `edited${ apiUsername }`, - password, - active: true, - roles: ['user'] - } + user: user.username, + password }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.deep.property('user.username', `edited${ apiUsername }`); - expect(res.body).to.have.deep.property('user.emails[0].address', apiEmail); - expect(res.body).to.have.deep.property('user.active', true); - expect(res.body).to.have.deep.property('user.name', `edited${ apiUsername }`); + userCredentials = {}; + userCredentials['X-Auth-Token'] = res.body.data.authToken; + userCredentials['X-User-Id'] = res.body.data.userId; }) .end(done); + }); + afterEach(done => { + request.post(api('users.delete')).set(credentials).send({ + userId: user._id + }).end(done); + user = undefined; + }); + + describe('logged as admin', () => { + it('should return the user id and a new token', (done) => { + request.post(api('users.createToken')) + .set(credentials) + .send({ + username: user.username + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.deep.property('data.userId', user._id); + expect(res.body).to.have.deep.property('data.authToken'); + }) + .end(done); + }); + }); + + describe('logged as itself', () => { + it('should return the user id and a new token', (done) => { + request.post(api('users.createToken')) + .set(userCredentials) + .send({ + username: user.username + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.deep.property('data.userId', user._id); + expect(res.body).to.have.deep.property('data.authToken'); + }) + .end(done); + }); + }); + + describe('As an user not allowed', () => { + it('should return 401 unauthorized', (done) => { + request.post(api('users.createToken')) + .set(userCredentials) + .send({ + username: 'rocket.cat' + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('errorType'); + expect(res.body).to.have.property('error'); + }) + .end(done); + }); + }); + + describe('Not logged in', () => { + it('should return 401 unauthorized', (done) => { + request.post(api('users.createToken')) + .send({ + username: user.username + }) + .expect('Content-Type', 'application/json') + .expect(401) + .expect((res) => { + expect(res.body).to.have.property('message'); + }) + .end(done); + }); + }); + + describe('Testing if the returned token is valid', (done) => { + it('should return 200', (done) => { + return request.post(api('users.createToken')) + .set(credentials) + .send({ username: user.username }) + .expect('Content-Type', 'application/json') + .end((err, res) => { + return err ? done () : request.get(api('me')) + .set({ 'X-Auth-Token': `${ res.body.data.authToken }`, 'X-User-Id': res.body.data.userId }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); + }); + }); }); }); diff --git a/tests/end-to-end/ui/07-emoji.js b/tests/end-to-end/ui/07-emoji.js index 39e27f94669f879bfd90d37d957a879da731762d..2038bc8c46298c7aba2ec5cccc6036c9f720d15c 100644 --- a/tests/end-to-end/ui/07-emoji.js +++ b/tests/end-to-end/ui/07-emoji.js @@ -127,7 +127,7 @@ describe('emoji', ()=> { }); it('the value on the message input should be the same as the emoji clicked', ()=> { - mainContent.messageInput.getValue().should.equal(':smile:'); + mainContent.messageInput.getValue().should.equal(':smile: '); }); it('send the emoji', ()=> {