diff --git a/.docker/Dockerfile b/.docker/Dockerfile index 4729c9a9f8d90c9cb2d044e8737f7c2366cb7528..630daf7c4baf37c3726d6f65077a55a5a6f83dcb 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -1,6 +1,6 @@ FROM rocketchat/base:4 -ENV RC_VERSION 0.56.0 +ENV RC_VERSION 0.57.1 MAINTAINER buildmaster@rocket.chat diff --git a/.editorconfig b/.editorconfig index 923a1d93ce60351107f201ffeac119a406c357a0..f2f826b09d65b272446d9976a5ddfc7b9ab770d8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,7 +8,7 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.{js,coffee,html,less,json}] +[*.{js,coffee,html,less,css,json}] indent_style = tab [*.i18n.json] diff --git a/.eslintrc b/.eslintrc index bbae28a07d6049da66a14d2b0a70b8b4bbe49749..6e652c53b845df96529d8203991591aa339e3068 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,10 @@ { "parserOptions": { "sourceType": "module", - "ecmaVersion": 2017 + "ecmaVersion": 2017, + "ecmaFeatures": { + "experimentalObjectRestSpread" : true, + } }, "env": { "browser": true, @@ -24,6 +27,7 @@ "no-delete-var": 2, "no-dupe-keys": 2, "no-dupe-args": 2, + "no-dupe-class-members": 2, "no-duplicate-case": 2, "no-empty": 2, "no-empty-character-class": 2, @@ -105,6 +109,7 @@ "EJSON" : false, "Email" : false, "FlowRouter" : false, + "FileUpload" : false, "HTTP" : false, "getNextAgent" : false, "handleError" : false, @@ -128,7 +133,6 @@ "ReactiveVar" : false, "RocketChat" : true, "RocketChatFile" : false, - "RocketChatFileAvatarInstance": false, "RoomHistoryManager" : false, "RoomManager" : false, "s" : false, diff --git a/.meteor/packages b/.meteor/packages index 9c3ec1031737afe626f26299515963b49da8c251..48ba111363e4f226345ff2553e79c9de7b8e5fbf 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -12,7 +12,6 @@ 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.3 ejson@1.0.13 @@ -34,7 +33,6 @@ service-configuration@1.0.11 session@1.1.7 shell-server@0.2.3 spacebars -standard-minifier-css@1.3.4 standard-minifier-js@2.0.0 tracker@1.1.3 @@ -132,7 +130,7 @@ rocketchat:videobridge rocketchat:webrtc rocketchat:wordpress rocketchat:message-snippet -rocketchat:google-natural-language +#rocketchat:google-natural-language rocketchat:drupal rocketchat:monitoring #rocketchat:chatops @@ -172,3 +170,5 @@ underscorestring:underscore.string yasaricli:slugify yasinuslu:blaze-meta deepwell:bootstrap-datepicker2 +rocketchat:postcss +communecter:account diff --git a/.meteor/versions b/.meteor/versions index 67e75bb0fc3d0e429392123df1749c372a7a8764..7bd3d0c43f9be2f6648281ca0a9005adee84f5ca 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -23,6 +23,7 @@ callback-hook@1.0.10 cfs:http-methods@0.0.32 check@1.2.5 coffeescript@1.12.3_1 +communecter:account@0.0.1 dandv:caret-position@2.1.1 ddp@1.2.5 ddp-client@1.3.4 @@ -102,11 +103,9 @@ oauth1@1.1.11 oauth2@1.1.11 observe-sequence@1.0.16 ordered-dict@1.0.9 -ostrio:cookies@2.2.0 +ostrio:cookies@2.2.1 pauli:accounts-linkedin@2.1.2 pauli:linkedin-oauth@1.1.0 -peerlibrary:aws-sdk@2.4.9_1 -peerlibrary:blocking@0.5.2 percolate:synced-cron@1.3.2 promise@0.8.8 raix:eventemitter@0.1.3 @@ -148,7 +147,6 @@ rocketchat:file@0.0.1 rocketchat:file-upload@0.0.1 rocketchat:github-enterprise@0.0.1 rocketchat:gitlab@0.0.1 -rocketchat:google-natural-language@0.0.1 rocketchat:highlight-words@0.0.1 rocketchat:i18n@0.0.1 rocketchat:iframe-login@1.0.0 @@ -183,6 +181,7 @@ rocketchat:oauth2-server@2.0.0 rocketchat:oauth2-server-config@1.0.0 rocketchat:oembed@0.0.1 rocketchat:otr@0.0.1 +rocketchat:postcss@1.0.0 rocketchat:push-notifications@0.0.1 rocketchat:reactions@0.0.1 rocketchat:sandstorm@0.0.1 @@ -232,7 +231,6 @@ smoral:sweetalert@1.1.1 spacebars@1.0.15 spacebars-compiler@1.1.2 srp@1.0.10 -standard-minifier-css@1.3.4 standard-minifier-js@2.0.0 steffo:meteor-accounts-saml@0.0.1 tap:i18n@1.8.2 diff --git a/.postcssrc b/.postcssrc new file mode 100644 index 0000000000000000000000000000000000000000..99dbadbdf3f381ab2e1e2d110e2d347c658f3eb4 --- /dev/null +++ b/.postcssrc @@ -0,0 +1,20 @@ +{ + "plugins": { + "postcss-smart-import": {}, + "postcss-cssnext": { + "browsers": [ + "ie > 10", + "last 2 Edge versions", + "last 2 Firefox versions", + "last 1 FirefoxAndroid versions", + "last 2 Chrome versions", + "last 1 ChromeAndroid versions", + "last 2 Safari versions", + "last 2 Opera versions", + "last 2 iOS versions", + "last 1 Android version" + ] + } + }, + "excludedPackages": ["deepwell:bootstrap-datepicker2", "smoral:sweetalert"] +} diff --git a/.sandstorm/sandstorm-pkgdef.capnp b/.sandstorm/sandstorm-pkgdef.capnp index 7d4ff40e2bfb6760d26172959bd1a93ada993b04..6050747e0d0d7a69df8f42cbc1d842116513ecc8 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"), + appMarketingVersion = (defaultText = "0.57.1"), # Human-readable representation of appVersion. Should match the way you # identify versions of your app in documentation and marketing. diff --git a/.snapcraft/resources/restoredb b/.snapcraft/resources/restoredb index cedf9e1358d886211a96f6b39d9836b3f74ca55d..0a204e8377097d3d923a30864233267c77e6ff53 100755 --- a/.snapcraft/resources/restoredb +++ b/.snapcraft/resources/restoredb @@ -29,7 +29,7 @@ function ask_backup { read choice [[ "${choice,,}" = n* ]] && return - [[ "${choice,,}" = y* ]] && backupdb.sh && return + [[ "${choice,,}" = y* ]] && backupdb && return exit } diff --git a/.stylelintignore b/.stylelintignore index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8e94b289684f497f254db6d7bd54ea32a5daa9e3 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -0,0 +1,2 @@ +packages/rocketchat-theme/client/vendor/fontello/css/fontello.css +packages/meteor-autocomplete/client/autocomplete.css diff --git a/.travis.yml b/.travis.yml index 7d8b89102247b956faa546df60cc57ebdd113723..948ec8f880c734bcd2ac68fbce88a8e903c15f03 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,6 +50,7 @@ before_script: - |- mongo --eval 'rs.initiate({_id:"rs0", members: [{"_id":1, "host":"localhost:27017"}]})' - meteor npm run lint +- meteor npm run testunit - meteor npm run stylelint - travis_retry meteor build --headless /tmp/build - mkdir /tmp/build-test diff --git a/.travis/setartname.sh b/.travis/setartname.sh index 349280f8205507de7c6d6a9f7bcadef84c7ee169..38253aac315fc379ee566bea36777720e092310c 100755 --- a/.travis/setartname.sh +++ b/.travis/setartname.sh @@ -1 +1,6 @@ -export ARTIFACT_NAME="$(meteor npm run version --silent)" +if [[ $TRAVIS_TAG ]] + then + export ARTIFACT_NAME="$(meteor npm run version --silent)" +else + export ARTIFACT_NAME="$(meteor npm run version --silent).$TRAVIS_BUILD_NUMBER" +fi diff --git a/.travis/snap.sh b/.travis/snap.sh index fe55eb907ec3cf9876e421c49220d22b0bd4fb64..691611020d01e2377c516838655bccac08caba3b 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 + RC_VERSION=0.57.1 fi echo "Preparing to trigger a snap release for $CHANNEL channel" diff --git a/HISTORY.md b/HISTORY.md index 4e732d59c737c668eb5679b4047dc6e80db3df0d..068a0db2f67d2e9c3d48adeeca79334bf1941794 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,261 @@ + +## 0.57.1 (2017-07-06) +- :hand: [#7428](https://github.com/RocketChat/Rocket.Chat/pull/7428) Fix migration of avatars from version 0.57.0 + +
+Others + +- [#7428](https://github.com/RocketChat/Rocket.Chat/pull/7428) Run avatar migration on startup +
+ + + + +# 0.57.0 (2017-07-03) + +### Breaking Changes + +- :hand: [#7095](https://github.com/RocketChat/Rocket.Chat/pull/7095) Internal hubot does not load [hubot-scripts](https://github.com/github/hubot-scripts) anymore. + +### New Features + +- [#7085](https://github.com/RocketChat/Rocket.Chat/pull/7085) API method and REST Endpoint for getting a single message by id +- [#6919](https://github.com/RocketChat/Rocket.Chat/pull/6919) Feature/delete any message permission +- [#6938](https://github.com/RocketChat/Rocket.Chat/pull/6938) Improve CI/Docker build/release +- [#7059](https://github.com/RocketChat/Rocket.Chat/pull/7059) Increase unread message count on [@here](https://github.com/here) mention +- [#6921](https://github.com/RocketChat/Rocket.Chat/pull/6921) LDAP: Use variables in User_Data_FieldMap for name mapping +- [#6857](https://github.com/RocketChat/Rocket.Chat/pull/6857) Make channel/group delete call answer to roomName +- [#7080](https://github.com/RocketChat/Rocket.Chat/pull/7080) Migration to add tags to email header and footer +- [#6788](https://github.com/RocketChat/Rocket.Chat/pull/6788) New avatar storage types +- [#6690](https://github.com/RocketChat/Rocket.Chat/pull/6690) Show full name in mentions if use full name setting enabled +- [#6953](https://github.com/RocketChat/Rocket.Chat/pull/6953) Show info about multiple instances at admin page +- [#6605](https://github.com/RocketChat/Rocket.Chat/pull/6605) Start running unit tests +- [#7311](https://github.com/RocketChat/Rocket.Chat/pull/7311) Force use of MongoDB for spotlight queries + +### Bug Fixes + +- [#7025](https://github.com/RocketChat/Rocket.Chat/pull/7025) Add and to header and footer +- [#7084](https://github.com/RocketChat/Rocket.Chat/pull/7084) Add option to ignore TLS in SMTP server settings +- [#7072](https://github.com/RocketChat/Rocket.Chat/pull/7072) Add support for carriage return in markdown code blocks +- [#6910](https://github.com/RocketChat/Rocket.Chat/pull/6910) Allow image insert from slack through slackbridge +- [#6904](https://github.com/RocketChat/Rocket.Chat/pull/6904) Bugs in `isUserFromParams` helper +- [#6840](https://github.com/RocketChat/Rocket.Chat/pull/6840) Check that username is not in the room when being muted / unmuted +- [#7103](https://github.com/RocketChat/Rocket.Chat/pull/7103) clipboard (permalink, copy, pin, star buttons) +- [#7030](https://github.com/RocketChat/Rocket.Chat/pull/7030) do only store password if LDAP_Login_Fallback is on +- [#7105](https://github.com/RocketChat/Rocket.Chat/pull/7105) edit button on firefox +- [#6935](https://github.com/RocketChat/Rocket.Chat/pull/6935) Error when trying to show preview of undefined filetype +- [#7045](https://github.com/RocketChat/Rocket.Chat/pull/7045) Fix avatar upload via users.setAvatar REST endpoint +- [#6950](https://github.com/RocketChat/Rocket.Chat/pull/6950) Fix badge counter on iOS push notifications +- [#7121](https://github.com/RocketChat/Rocket.Chat/pull/7121) fix bug in preview image +- [#6972](https://github.com/RocketChat/Rocket.Chat/pull/6972) Fix error handling for non-valid avatar URL +- [#6974](https://github.com/RocketChat/Rocket.Chat/pull/6974) Fix login with Meteor saving an object as email address +- [#7104](https://github.com/RocketChat/Rocket.Chat/pull/7104) Fix missing CSS files on production builds +- [#6986](https://github.com/RocketChat/Rocket.Chat/pull/6986) Fix the other tests failing due chimp update +- [#7049](https://github.com/RocketChat/Rocket.Chat/pull/7049) Improve Tests +- [#6968](https://github.com/RocketChat/Rocket.Chat/pull/6968) make channels.create API check for create-c +- [#7044](https://github.com/RocketChat/Rocket.Chat/pull/7044) New screen sharing Chrome extension checking method +- [#6999](https://github.com/RocketChat/Rocket.Chat/pull/6999) overlapping text for users-typing-message +- [#7014](https://github.com/RocketChat/Rocket.Chat/pull/7014) Parse HTML on admin setting's descriptions +- [#6997](https://github.com/RocketChat/Rocket.Chat/pull/6997) Parse markdown links last +- [#7033](https://github.com/RocketChat/Rocket.Chat/pull/7033) Prevent Ctrl key on message field from reloading messages list +- [#6912](https://github.com/RocketChat/Rocket.Chat/pull/6912) Remove room from roomPick setting +- [#6961](https://github.com/RocketChat/Rocket.Chat/pull/6961) SAML: Only set KeyDescriptor when non empty +- [#7023](https://github.com/RocketChat/Rocket.Chat/pull/7023) Sidenav roomlist +- [#6913](https://github.com/RocketChat/Rocket.Chat/pull/6913) Slackbridge text replacements +- [#6903](https://github.com/RocketChat/Rocket.Chat/pull/6903) Updating Incoming Integration Post As Field Not Allowed +- [#6947](https://github.com/RocketChat/Rocket.Chat/pull/6947) Use AWS Signature Version 4 signed URLs for uploads +- [#7012](https://github.com/RocketChat/Rocket.Chat/pull/7012) video message recording dialog is shown in an incorrect position +- [#7157](https://github.com/RocketChat/Rocket.Chat/pull/7157) Fix all reactions having the same username +- [#7215](https://github.com/RocketChat/Rocket.Chat/pull/7215/) Fix the Zapier oAuth return url to the new one +- [#7209](https://github.com/RocketChat/Rocket.Chat/pull/7209) "requirePasswordChange" property not being saved when set to false +- [#7208](https://github.com/RocketChat/Rocket.Chat/pull/7208) Fix oembed previews not being shown +- [#7200](https://github.com/RocketChat/Rocket.Chat/pull/7200) Fix editing others messages +- [#7160](https://github.com/RocketChat/Rocket.Chat/pull/7160) Removing the kadira package install from example build script. +- [#7345](https://github.com/RocketChat/Rocket.Chat/pull/7345) click on image in a message +- [#7207](https://github.com/RocketChat/Rocket.Chat/pull/7207) Fix Block Delete Message After (n) Minutes +- [#7320](https://github.com/RocketChat/Rocket.Chat/pull/7320) Fix jump to unread button +- [#7321](https://github.com/RocketChat/Rocket.Chat/pull/7321) Fix Secret Url +- [#7358](https://github.com/RocketChat/Rocket.Chat/pull/7358) Fix user's customFields not being saved correctly +- [#7352](https://github.com/RocketChat/Rocket.Chat/pull/7352) Improve avatar migration +- [#7304](https://github.com/RocketChat/Rocket.Chat/pull/7304) Proxy upload to correct instance +- [#7379](https://github.com/RocketChat/Rocket.Chat/pull/7379) Message being displayed unescaped + + +
+Others + +- [#7094](https://github.com/RocketChat/Rocket.Chat/pull/7094) [FIX]Fix the failing tests +- [#7092](https://github.com/RocketChat/Rocket.Chat/pull/7092) [FIX]Fixed typo hmtl -> html +- [#7145](https://github.com/RocketChat/Rocket.Chat/pull/7145) Convert file unsubscribe.coffee to js +- [#7146](https://github.com/RocketChat/Rocket.Chat/pull/7146) Convert hipchat importer to js +- [#7022](https://github.com/RocketChat/Rocket.Chat/pull/7022) Convert irc package to js +- [#7096](https://github.com/RocketChat/Rocket.Chat/pull/7096) Convert Livechat from Coffeescript to JavaScript +- [#6936](https://github.com/RocketChat/Rocket.Chat/pull/6936) Convert meteor-autocomplete package to js +- [#7017](https://github.com/RocketChat/Rocket.Chat/pull/7017) Convert oauth2-server-config package to js +- [#6795](https://github.com/RocketChat/Rocket.Chat/pull/6795) Convert Ui Account Package to Js +- [#6911](https://github.com/RocketChat/Rocket.Chat/pull/6911) Convert ui-admin package to js +- [#6775](https://github.com/RocketChat/Rocket.Chat/pull/6775) Convert WebRTC Package to Js +- [#7018](https://github.com/RocketChat/Rocket.Chat/pull/7018) converted rocketchat-importer +- [#6836](https://github.com/RocketChat/Rocket.Chat/pull/6836) converted rocketchat-ui coffee to js part 2 +- [#6976](https://github.com/RocketChat/Rocket.Chat/pull/6976) fix the crashing tests +- [#7055](https://github.com/RocketChat/Rocket.Chat/pull/7055) Ldap: User_Data_FieldMap description +- [#7114](https://github.com/RocketChat/Rocket.Chat/pull/7114) LingoHub based on develop +- [#7005](https://github.com/RocketChat/Rocket.Chat/pull/7005) LingoHub based on develop +- [#6978](https://github.com/RocketChat/Rocket.Chat/pull/6978) LingoHub based on develop +- [#7062](https://github.com/RocketChat/Rocket.Chat/pull/7062) Remove Useless Jasmine Tests +- [#6914](https://github.com/RocketChat/Rocket.Chat/pull/6914) Rocketchat ui message +- [#7006](https://github.com/RocketChat/Rocket.Chat/pull/7006) Rocketchat ui3 +- [#6987](https://github.com/RocketChat/Rocket.Chat/pull/6987) rocketchat-importer-slack coffee to js +- [#6735](https://github.com/RocketChat/Rocket.Chat/pull/6735) rocketchat-lib[4] coffee to js +- [#7154](https://github.com/RocketChat/Rocket.Chat/pull/7154) Remove missing CoffeeScript dependencies +- [#7308](https://github.com/RocketChat/Rocket.Chat/pull/7308) Escape error messages +- [#7102](https://github.com/RocketChat/Rocket.Chat/pull/7102) add server methods getRoomNameById +
+ + +
+Details + +## 0.57.0-rc.3 (2017-06-28) + + +### New Features + +- [#7311](https://github.com/RocketChat/Rocket.Chat/pull/7311) Force use of MongoDB for spotlight queries + + +### Bug Fixes + +- [#7345](https://github.com/RocketChat/Rocket.Chat/pull/7345) click on image in a message +- [#7207](https://github.com/RocketChat/Rocket.Chat/pull/7207) Fix Block Delete Message After (n) Minutes +- [#7320](https://github.com/RocketChat/Rocket.Chat/pull/7320) Fix jump to unread button +- [#7321](https://github.com/RocketChat/Rocket.Chat/pull/7321) Fix Secret Url +- [#7358](https://github.com/RocketChat/Rocket.Chat/pull/7358) Fix user's customFields not being saved correctly +- [#7352](https://github.com/RocketChat/Rocket.Chat/pull/7352) Improve avatar migration +- [#7304](https://github.com/RocketChat/Rocket.Chat/pull/7304) Proxy upload to correct instance + + +
+Others + +- [#7308](https://github.com/RocketChat/Rocket.Chat/pull/7308) Escape error messages +
+ + + +## 0.57.0-rc.2 (2017-06-12) + + +### Bug Fixes + +- [#7215](https://github.com/RocketChat/Rocket.Chat/pull/7215/) Fix the Zapier oAuth return url to the new one +- [#7209](https://github.com/RocketChat/Rocket.Chat/pull/7209) "requirePasswordChange" property not being saved when set to false +- [#7208](https://github.com/RocketChat/Rocket.Chat/pull/7208) Fix oembed previews not being shown +- [#7200](https://github.com/RocketChat/Rocket.Chat/pull/7200) Fix editing others messages +- [#7160](https://github.com/RocketChat/Rocket.Chat/pull/7160) Removing the kadira package install from example build script. + + + +## 0.57.0-rc.1 (2017-06-02) + + +### Bug Fixes + +- [#7157](https://github.com/RocketChat/Rocket.Chat/pull/7157) Fix all reactions having the same username + + +
+Others + +- [#7154](https://github.com/RocketChat/Rocket.Chat/pull/7154) Remove missing CoffeeScript dependencies +
+ + + +## 0.57.0-rc.0 (2017-06-01) + + +### New Features + +- [#7085](https://github.com/RocketChat/Rocket.Chat/pull/7085) API method and REST Endpoint for getting a single message by id +- [#6919](https://github.com/RocketChat/Rocket.Chat/pull/6919) Feature/delete any message permission +- [#6938](https://github.com/RocketChat/Rocket.Chat/pull/6938) Improve CI/Docker build/release +- [#7059](https://github.com/RocketChat/Rocket.Chat/pull/7059) Increase unread message count on [@here](https://github.com/here) mention +- [#6921](https://github.com/RocketChat/Rocket.Chat/pull/6921) LDAP: Use variables in User_Data_FieldMap for name mapping +- [#6857](https://github.com/RocketChat/Rocket.Chat/pull/6857) Make channel/group delete call answer to roomName +- [#7080](https://github.com/RocketChat/Rocket.Chat/pull/7080) Migration to add tags to email header and footer +- [#6788](https://github.com/RocketChat/Rocket.Chat/pull/6788) New avatar storage types +- [#6690](https://github.com/RocketChat/Rocket.Chat/pull/6690) Show full name in mentions if use full name setting enabled +- [#6953](https://github.com/RocketChat/Rocket.Chat/pull/6953) Show info about multiple instances at admin page +- [#6605](https://github.com/RocketChat/Rocket.Chat/pull/6605) Start running unit tests + + +### Bug Fixes + +- [#7025](https://github.com/RocketChat/Rocket.Chat/pull/7025) Add and to header and footer +- [#7084](https://github.com/RocketChat/Rocket.Chat/pull/7084) Add option to ignore TLS in SMTP server settings +- [#7072](https://github.com/RocketChat/Rocket.Chat/pull/7072) Add support for carriage return in markdown code blocks +- [#6910](https://github.com/RocketChat/Rocket.Chat/pull/6910) Allow image insert from slack through slackbridge +- [#6904](https://github.com/RocketChat/Rocket.Chat/pull/6904) Bugs in `isUserFromParams` helper +- [#6840](https://github.com/RocketChat/Rocket.Chat/pull/6840) Check that username is not in the room when being muted / unmuted +- [#7103](https://github.com/RocketChat/Rocket.Chat/pull/7103) clipboard (permalink, copy, pin, star buttons) +- [#7030](https://github.com/RocketChat/Rocket.Chat/pull/7030) do only store password if LDAP_Login_Fallback is on +- [#7105](https://github.com/RocketChat/Rocket.Chat/pull/7105) edit button on firefox +- [#6935](https://github.com/RocketChat/Rocket.Chat/pull/6935) Error when trying to show preview of undefined filetype +- [#7045](https://github.com/RocketChat/Rocket.Chat/pull/7045) Fix avatar upload via users.setAvatar REST endpoint +- [#6950](https://github.com/RocketChat/Rocket.Chat/pull/6950) Fix badge counter on iOS push notifications +- [#7121](https://github.com/RocketChat/Rocket.Chat/pull/7121) fix bug in preview image +- [#6972](https://github.com/RocketChat/Rocket.Chat/pull/6972) Fix error handling for non-valid avatar URL +- [#6974](https://github.com/RocketChat/Rocket.Chat/pull/6974) Fix login with Meteor saving an object as email address +- [#7104](https://github.com/RocketChat/Rocket.Chat/pull/7104) Fix missing CSS files on production builds +- [#6986](https://github.com/RocketChat/Rocket.Chat/pull/6986) Fix the other tests failing due chimp update +- [#7049](https://github.com/RocketChat/Rocket.Chat/pull/7049) Improve Tests +- [#6968](https://github.com/RocketChat/Rocket.Chat/pull/6968) make channels.create API check for create-c +- [#7044](https://github.com/RocketChat/Rocket.Chat/pull/7044) New screen sharing Chrome extension checking method +- [#6999](https://github.com/RocketChat/Rocket.Chat/pull/6999) overlapping text for users-typing-message +- [#7014](https://github.com/RocketChat/Rocket.Chat/pull/7014) Parse HTML on admin setting's descriptions +- [#6997](https://github.com/RocketChat/Rocket.Chat/pull/6997) Parse markdown links last +- [#7033](https://github.com/RocketChat/Rocket.Chat/pull/7033) Prevent Ctrl key on message field from reloading messages list +- [#6912](https://github.com/RocketChat/Rocket.Chat/pull/6912) Remove room from roomPick setting +- [#6961](https://github.com/RocketChat/Rocket.Chat/pull/6961) SAML: Only set KeyDescriptor when non empty +- [#7023](https://github.com/RocketChat/Rocket.Chat/pull/7023) Sidenav roomlist +- [#6913](https://github.com/RocketChat/Rocket.Chat/pull/6913) Slackbridge text replacements +- [#6903](https://github.com/RocketChat/Rocket.Chat/pull/6903) Updating Incoming Integration Post As Field Not Allowed +- [#6947](https://github.com/RocketChat/Rocket.Chat/pull/6947) Use AWS Signature Version 4 signed URLs for uploads +- [#7012](https://github.com/RocketChat/Rocket.Chat/pull/7012) video message recording dialog is shown in an incorrect position + + +
+Others + +- [#7094](https://github.com/RocketChat/Rocket.Chat/pull/7094) [FIX]Fix the failing tests +- [#7092](https://github.com/RocketChat/Rocket.Chat/pull/7092) [FIX]Fixed typo hmtl -> html +- [#7145](https://github.com/RocketChat/Rocket.Chat/pull/7145) Convert file unsubscribe.coffee to js +- [#7146](https://github.com/RocketChat/Rocket.Chat/pull/7146) Convert hipchat importer to js +- [#7022](https://github.com/RocketChat/Rocket.Chat/pull/7022) Convert irc package to js +- [#7096](https://github.com/RocketChat/Rocket.Chat/pull/7096) Convert Livechat from Coffeescript to JavaScript +- [#6936](https://github.com/RocketChat/Rocket.Chat/pull/6936) Convert meteor-autocomplete package to js +- [#7017](https://github.com/RocketChat/Rocket.Chat/pull/7017) Convert oauth2-server-config package to js +- [#6795](https://github.com/RocketChat/Rocket.Chat/pull/6795) Convert Ui Account Package to Js +- [#6911](https://github.com/RocketChat/Rocket.Chat/pull/6911) Convert ui-admin package to js +- [#6775](https://github.com/RocketChat/Rocket.Chat/pull/6775) Convert WebRTC Package to Js +- [#7018](https://github.com/RocketChat/Rocket.Chat/pull/7018) converted rocketchat-importer +- [#6836](https://github.com/RocketChat/Rocket.Chat/pull/6836) converted rocketchat-ui coffee to js part 2 +- [#6976](https://github.com/RocketChat/Rocket.Chat/pull/6976) fix the crashing tests +- [#7055](https://github.com/RocketChat/Rocket.Chat/pull/7055) Ldap: User_Data_FieldMap description +- [#7114](https://github.com/RocketChat/Rocket.Chat/pull/7114) LingoHub based on develop +- [#7005](https://github.com/RocketChat/Rocket.Chat/pull/7005) LingoHub based on develop +- [#6978](https://github.com/RocketChat/Rocket.Chat/pull/6978) LingoHub based on develop +- [#7062](https://github.com/RocketChat/Rocket.Chat/pull/7062) Remove Useless Jasmine Tests +- [#6914](https://github.com/RocketChat/Rocket.Chat/pull/6914) Rocketchat ui message +- [#7006](https://github.com/RocketChat/Rocket.Chat/pull/7006) Rocketchat ui3 +- [#6987](https://github.com/RocketChat/Rocket.Chat/pull/6987) rocketchat-importer-slack coffee to js +- [#6735](https://github.com/RocketChat/Rocket.Chat/pull/6735) rocketchat-lib[4] coffee to js +
+ +
+ + # 0.56.0 (2017-05-15) @@ -51,7 +309,7 @@ - [#6780](https://github.com/RocketChat/Rocket.Chat/pull/6780) Convert Mailer Package to Js - [#6694](https://github.com/RocketChat/Rocket.Chat/pull/6694) Convert markdown to js - [#6689](https://github.com/RocketChat/Rocket.Chat/pull/6689) Convert Mentions-Flextab Package to Js -- [#6781](https://github.com/RocketChat/Rocket.Chat/pull/6781) Convert Message-Star Package to js +- [#6781](https://github.com/RocketChat/Rocket.Chat/pull/6781) Convert Message-Star Package to js - [#6688](https://github.com/RocketChat/Rocket.Chat/pull/6688) Convert Oembed Package to Js - [#6672](https://github.com/RocketChat/Rocket.Chat/pull/6672) Converted rocketchat-lib 3 - [#6654](https://github.com/RocketChat/Rocket.Chat/pull/6654) disable proxy configuration @@ -168,7 +426,7 @@ - [#6780](https://github.com/RocketChat/Rocket.Chat/pull/6780) Convert Mailer Package to Js - [#6694](https://github.com/RocketChat/Rocket.Chat/pull/6694) Convert markdown to js - [#6689](https://github.com/RocketChat/Rocket.Chat/pull/6689) Convert Mentions-Flextab Package to Js -- [#6781](https://github.com/RocketChat/Rocket.Chat/pull/6781) Convert Message-Star Package to js +- [#6781](https://github.com/RocketChat/Rocket.Chat/pull/6781) Convert Message-Star Package to js - [#6688](https://github.com/RocketChat/Rocket.Chat/pull/6688) Convert Oembed Package to Js - [#6672](https://github.com/RocketChat/Rocket.Chat/pull/6672) Converted rocketchat-lib 3 - [#6654](https://github.com/RocketChat/Rocket.Chat/pull/6654) disable proxy configuration diff --git a/client/lib/handleError.js b/client/lib/handleError.js index 703524a1bfdfaeea1efcb2fa13c032a20c883903..821d7e7cb0ddcff10f658dec8fd7d6fb8bf489f0 100644 --- a/client/lib/handleError.js +++ b/client/lib/handleError.js @@ -9,8 +9,8 @@ this.handleError = function(error, useToastr = true) { } if (useToastr) { - return toastr.error(TAPi18n.__(error.error, error.details), error.details && error.details.errorTitle ? TAPi18n.__(error.details.errorTitle) : null); + return toastr.error(_.escapeHTML(TAPi18n.__(error.error, error.details)), error.details && error.details.errorTitle ? _.escapeHTML(TAPi18n.__(error.details.errorTitle)) : null); } - return TAPi18n.__(error.error, error.details); + return _.escapeHTML(TAPi18n.__(error.error, error.details)); }; diff --git a/client/methods/deleteMessage.js b/client/methods/deleteMessage.js index e7acb1c30aa31b643ce644aaf8114c5d9409c819..1827726fb857c5fa00bd65f5f619f2eb6cf2e06f 100644 --- a/client/methods/deleteMessage.js +++ b/client/methods/deleteMessage.js @@ -1,5 +1,4 @@ import moment from 'moment'; -import toastr from 'toastr'; Meteor.methods({ deleteMessage(message) { @@ -11,28 +10,25 @@ 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 (message.ts) { - const msgTs = moment(message.ts); - if (msgTs) { - const currentTsDiff = moment().diff(msgTs, 'minutes'); - if (currentTsDiff > blockDeleteInMinutes) { - toastr.error(t('error-message-deleting-blocked')); - return false; - } - } + if (!forceDelete && _.isNumber(blockDeleteInMinutes) && blockDeleteInMinutes !== 0) { + const msgTs = moment(message.ts); + const currentTsDiff = moment().diff(msgTs, 'minutes'); + if (currentTsDiff > blockDeleteInMinutes) { + return false; } + + } Tracker.nonreactive(function() { diff --git a/client/notifications/UsersNameChanged.js b/client/notifications/UsersNameChanged.js index 77d153ba708d3660c0c736d48295ce81ad94c6f9..68f23ea3e1338d7d40cdafb5c04a1220b58ac31e 100644 --- a/client/notifications/UsersNameChanged.js +++ b/client/notifications/UsersNameChanged.js @@ -10,6 +10,18 @@ Meteor.startup(function() { multi: true }); + RocketChat.models.Messages.update({ + mentions: { + $elemMatch: { _id } + } + }, { + $set: { + 'mentions.$.name': name + } + }, { + multi: true + }); + RocketChat.models.Subscriptions.update({ name: username, t: 'd' diff --git a/example-build.sh b/example-build.sh index c7ce49820342af5a94e1888aa4dc4af1593bf2da..06ac62461246e226114e86e0f51e719d1a4d4e18 100755 --- a/example-build.sh +++ b/example-build.sh @@ -5,7 +5,7 @@ IFS=$'\n\t' # Build export NODE_ENV=production -meteor add rocketchat:internal-hubot meteorhacks:kadira +meteor add rocketchat:internal-hubot meteor build --server https://demo.rocket.chat --directory /var/www/rocket.chat # Run diff --git a/lib/fileUpload.js b/lib/fileUpload.js deleted file mode 100644 index fbb006f15b7ebdf3852aa7097f23d56144a10b37..0000000000000000000000000000000000000000 --- a/lib/fileUpload.js +++ /dev/null @@ -1,89 +0,0 @@ -/* globals UploadFS, FileUpload */ -import { Cookies } from 'meteor/ostrio:cookies'; - -if (UploadFS) { - const initFileStore = function() { - const cookie = new Cookies(); - if (Meteor.isClient) { - document.cookie = `rc_uid=${ escape(Meteor.userId()) }; path=/`; - document.cookie = `rc_token=${ escape(Accounts._storedLoginToken()) }; path=/`; - } - - Meteor.fileStore = new UploadFS.store.GridFS({ - collection: RocketChat.models.Uploads.model, - name: 'rocketchat_uploads', - collectionName: 'rocketchat_uploads', - filter: new UploadFS.Filter({ - onCheck: FileUpload.validateFileUpload - }), - transformWrite(readStream, writeStream, fileId, file) { - if (RocketChatFile.enabled === false || !/^image\/.+/.test(file.type)) { - return readStream.pipe(writeStream); - } - - let stream = undefined; - - const identify = function(err, data) { - if (err) { - return stream.pipe(writeStream); - } - - file.identify = { - format: data.format, - size: data.size - }; - - if (data.Orientation && !['', 'Unknown', 'Undefined'].includes(data.Orientation)) { - RocketChatFile.gm(stream).autoOrient().stream().pipe(writeStream); - } else { - stream.pipe(writeStream); - } - }; - - stream = RocketChatFile.gm(readStream).identify(identify).stream(); - }, - - onRead(fileId, file, req, res) { - if (RocketChat.settings.get('FileUpload_ProtectFiles')) { - let uid; - let token; - - if (req && req.headers && req.headers.cookie) { - const rawCookies = req.headers.cookie; - - if (rawCookies) { - uid = cookie.get('rc_uid', rawCookies) ; - token = cookie.get('rc_token', rawCookies); - } - } - - if (!uid) { - uid = req.query.rc_uid; - token = req.query.rc_token; - } - - if (!uid || !token || !RocketChat.models.Users.findOneByIdAndLoginToken(uid, token)) { - res.writeHead(403); - return false; - } - } - - res.setHeader('content-disposition', `attachment; filename="${ encodeURIComponent(file.name) }"`); - return true; - } - }); - }; - - Meteor.startup(function() { - if (Meteor.isServer) { - initFileStore(); - } else { - Tracker.autorun(function(c) { - if (Meteor.userId() && RocketChat.settings.cachedCollection.ready.get()) { - initFileStore(); - c.stop(); - } - }); - } - }); -} diff --git a/mocha.opts b/mocha.opts new file mode 100644 index 0000000000000000000000000000000000000000..88725803b547a695048999d8cd18f5edb32f5606 --- /dev/null +++ b/mocha.opts @@ -0,0 +1,3 @@ +--compilers js:babel-mocha-es6-compiler +--reporter spec +--ui bdd diff --git a/package.json b/package.json index 8a9b8c71e7eae1985d6e23265f1f32df5d236ea6..f40944af1f7b3b9eb9d6e4ce0f2a737d7aa8677d 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", + "version": "0.57.1", "author": { "name": "Rocket.Chat", "url": "https://rocket.chat/" @@ -28,6 +28,15 @@ "name": "Sing Li", "email": "sing.li@rocket.chat" }], + "mocha": { + "tests": [ + "packages/**/*.tests.js" + ], + "files": [ + "packages/**/*.js", + "!packages/**/*.tests.js" + ] + }, "keywords": [ "rocketchat", "rocket", @@ -37,12 +46,15 @@ "start": "meteor npm i && meteor", "lint": "eslint .", "lint-fix": "eslint . --fix", - "stylelint": "stylelint **/*.less", + "stylelint": "stylelint packages/**/*.{less,css}", "test": "node .scripts/start.js", "deploy": "npm run build && pm2 startOrRestart pm2.json", "chimp-watch": "chimp --ddp=http://localhost:3000 --watch --mocha --path=tests/end-to-end", "chimp-test": "chimp tests/chimp-config.js", "postinstall": "cd packages/rocketchat-katex && npm i", + "testunit-watch": "mocha --watch --opts ./mocha.opts \"`node -e \"console.log(require('./package.json').mocha.tests.join(' '))\"`\"", + "coverage": "nyc -r html mocha --opts ./mocha.opts \"`node -e \"console.log(require('./package.json').mocha.tests.join(' '))\"`\"", + "testunit": "mocha --opts ./mocha.opts \"`node -e \"console.log(require('./package.json').mocha.tests.join(' '))\"`\"", "version": "node .scripts/version.js", "set-version": "node .scripts/set-version.js", "release": "npm run set-version --silent" @@ -57,25 +69,31 @@ "email": "support@rocket.chat" }, "devDependencies": { - "chimp": "^0.48.0", + "babel-mocha-es6-compiler": "^0.1.0", + "babel-plugin-array-includes": "^2.0.3", + "chimp": "^0.49.0", + "conventional-changelog": "^1.1.3", "eslint": "^3.19.0", + "postcss-cssnext": "^2.11.0", + "postcss-smart-import": "^0.7.2", "stylelint": "^7.10.1", - "supertest": "^3.0.0", - "conventional-changelog": "^1.1.3" + "supertest": "^3.0.0" }, "dependencies": { + "@google-cloud/storage": "^1.1.1", + "aws-sdk": "^2.55.0", "babel-runtime": "^6.23.0", "bcrypt": "^1.0.2", - "codemirror": "^5.25.2", - "file-type": "^4.2.0", - "highlight.js": "^9.11.0", + "codemirror": "^5.26.0", + "file-type": "^4.3.0", + "highlight.js": "^9.12.0", "jquery": "^3.2.1", - "mime-db": "^1.27.0", - "mime-type": "^3.0.4", + "mime-db": "^1.28.0", + "mime-type": "^3.0.5", "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/account_communecter/README.md b/packages/account_communecter/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/packages/account_communecter/client/config.js b/packages/account_communecter/client/config.js new file mode 100644 index 0000000000000000000000000000000000000000..c03a7443bc1bd0a0a5d7adcbc52735bfd604559c --- /dev/null +++ b/packages/account_communecter/client/config.js @@ -0,0 +1,21 @@ +import { Accounts } from 'meteor/accounts-base'; + +Meteor.startup(function () { + +Meteor.loginAsPixel = function(email,password, callback) { + var loginRequest = {email: email, pwd: password}; + Accounts.callLoginMethod({ + methodArguments: [loginRequest], + userCallback: callback + }); +}; + +Meteor.loginWithPassword = function(email,password, callback) { + var loginRequest = {email: email, pwd: password}; + Accounts.callLoginMethod({ + methodArguments: [loginRequest], + userCallback: callback + }); +}; + +}); diff --git a/packages/account_communecter/package.js b/packages/account_communecter/package.js new file mode 100644 index 0000000000000000000000000000000000000000..a8cee6b253e54215542e67806a38bc4d429b3da1 --- /dev/null +++ b/packages/account_communecter/package.js @@ -0,0 +1,28 @@ +Package.describe({ + name: 'communecter:account', + version: '0.0.1', + // Brief, one-line summary of the package. + summary: '', + // URL to the Git repository containing the source code for this package. + git: '', + // By default, Meteor will default to using README.md for documentation. + // To avoid submitting documentation, set this field to null. + documentation: 'README.md' +}); + +Package.onUse(function(api) { + api.versionsFrom('1.4.2.3'); + api.use([ + 'ecmascript', + 'ejson', + 'underscore', + 'accounts-base' + ]); + + api.use('rocketchat:lib', 'server'); + api.use('rocketchat:logger', 'server'); + + api.add_files('server/config.js', 'server'); + api.add_files('client/config.js', 'client'); + +}); diff --git a/packages/account_communecter/server/config.js b/packages/account_communecter/server/config.js new file mode 100644 index 0000000000000000000000000000000000000000..f67142f1147a5db6c56bff5613df0e9406c01abd --- /dev/null +++ b/packages/account_communecter/server/config.js @@ -0,0 +1,110 @@ +import { Accounts } from 'meteor/accounts-base'; + +Accounts.registerLoginHandler(function(loginRequest) { + if(!loginRequest.email || !loginRequest.pwd) { + return null; + } + + const response = HTTP.call( 'POST', `${Meteor.settings.endpoint}/${Meteor.settings.module}/person/authenticate`, { + params: { + "email": loginRequest.email, + "pwd": loginRequest.pwd, + } + }); + console.log(response); + if(response && response.data && response.data.result === true && response.data.id){ + + let userId = null; + let retourId = null; + + if(response.data.id && response.data.id.$id){ + retourId = response.data.id.$id; + }else{ + retourId = response.data.id; + } + console.log(response.data.account.email); + + //ok valide + var userM = Meteor.users.findOne({'_id':retourId}); + console.log(userM); + if(userM){ + //Meteor.user existe + userId= userM._id; + //RocketChat._setRealName(userId, response.data.account.name); + Meteor.users.update(userId,{$set: {name: response.data.account.name, + emails: [{ address: response.data.account.email, verified: true }]}}); + + }else{ + //Meteor.user n'existe pas + //username ou emails + + const newUser = { + _id:retourId, + username: response.data.account.username, + name: response.data.account.name, + emails: [{ address: response.data.account.email, verified: true }], + createdAt: new Date(), + active: true, + type: 'user', + globalRoles: ['user'] + }; + + let roles = []; + + if (Match.test(newUser.globalRoles, [String]) && newUser.globalRoles.length > 0) { + roles = roles.concat(newUser.globalRoles); + } + + delete newUser.globalRoles; + + /*if (roles.length === 0) { + const hasAdmin = RocketChat.models.Users.findOne({ + roles: 'admin', + type: 'user' + }, { + fields: { + _id: 1 + } + }); + + if (hasAdmin) { + roles.push('user'); + } else { + roles.push('admin'); + } + }*/ + + + userId = Meteor.users.insert(newUser); + + if(response.data.account.email==='thomas.craipeau@gmail.com'){ + roles.push('admin'); + } + + RocketChat.authz.addUserRoles(retourId, roles); + } + + + const stampedToken = Accounts._generateStampedLoginToken(); + Meteor.users.update(userId, + {$push: {'services.resume.loginTokens': stampedToken}} + ); + this.setUserId(userId); + var userR = Meteor.users.findOne({'_id':userId}); + if(response.data.account.profilThumbImageUrl){ + RocketChat.setUserAvatar({ _id: userId, username: userR.username }, `${Meteor.settings.urlimage}${response.data.account.profilThumbImageUrl}`, '', 'url'); + } + console.log(userId); + return { + userId: userId, + token: stampedToken.token + } + }else{ + if(response && response.data && response.data.result === false){ + throw new Meteor.Error(Accounts.LoginCancelledError.numericError, response.data.msg); + } else if(response && response.data && response.data.result === true && response.data.msg){ + throw new Meteor.Error(response.data.msg); + } + + } +}); diff --git a/packages/meteor-accounts-saml/saml_utils.js b/packages/meteor-accounts-saml/saml_utils.js index 5d8396e88eca33163aea4136d608eed2b3ec1f3c..02660f2dcdc8657374445f65fcd176cb2efa7118 100644 --- a/packages/meteor-accounts-saml/saml_utils.js +++ b/packages/meteor-accounts-saml/saml_utils.js @@ -432,12 +432,38 @@ SAML.prototype.validateResponse = function(samlResponse, relayState, callback) { let decryptionCert; SAML.prototype.generateServiceProviderMetadata = function(callbackUrl) { - let keyDescriptor = null; - if (!decryptionCert) { decryptionCert = this.options.privateCert; } + if (!this.options.callbackUrl && !callbackUrl) { + throw new Error( + 'Unable to generate service provider metadata when callbackUrl option is not set'); + } + + const metadata = { + 'EntityDescriptor': { + '@xmlns': 'urn:oasis:names:tc:SAML:2.0:metadata', + '@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#', + '@entityID': this.options.issuer, + 'SPSSODescriptor': { + '@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol', + 'SingleLogoutService': { + '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + '@Location': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/`, + '@ResponseLocation': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/` + }, + 'NameIDFormat': this.options.identifierFormat, + 'AssertionConsumerService': { + '@index': '1', + '@isDefault': 'true', + '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + '@Location': callbackUrl + } + } + } + }; + if (this.options.privateKey) { if (!decryptionCert) { throw new Error( @@ -448,7 +474,7 @@ SAML.prototype.generateServiceProviderMetadata = function(callbackUrl) { decryptionCert = decryptionCert.replace(/-+END CERTIFICATE-+\r?\n?/, ''); decryptionCert = decryptionCert.replace(/\r\n/g, '\n'); - keyDescriptor = { + metadata['EntityDescriptor']['SPSSODescriptor']['KeyDescriptor'] = { 'ds:KeyInfo': { 'ds:X509Data': { 'ds:X509Certificate': { @@ -477,35 +503,6 @@ SAML.prototype.generateServiceProviderMetadata = function(callbackUrl) { }; } - if (!this.options.callbackUrl && !callbackUrl) { - throw new Error( - 'Unable to generate service provider metadata when callbackUrl option is not set'); - } - - const metadata = { - 'EntityDescriptor': { - '@xmlns': 'urn:oasis:names:tc:SAML:2.0:metadata', - '@xmlns:ds': 'http://www.w3.org/2000/09/xmldsig#', - '@entityID': this.options.issuer, - 'SPSSODescriptor': { - '@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol', - 'KeyDescriptor': keyDescriptor, - 'SingleLogoutService': { - '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', - '@Location': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/`, - '@ResponseLocation': `${ Meteor.absoluteUrl() }_saml/logout/${ this.options.provider }/` - }, - 'NameIDFormat': this.options.identifierFormat, - 'AssertionConsumerService': { - '@index': '1', - '@isDefault': 'true', - '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', - '@Location': callbackUrl - } - } - } - }; - return xmlbuilder.create(metadata).end({ pretty: true, indent: ' ', diff --git a/packages/meteor-autocomplete/autocomplete-client.coffee b/packages/meteor-autocomplete/autocomplete-client.coffee deleted file mode 100755 index b58875785139042817b8edf03b41f032651c88c2..0000000000000000000000000000000000000000 --- a/packages/meteor-autocomplete/autocomplete-client.coffee +++ /dev/null @@ -1,367 +0,0 @@ -AutoCompleteRecords = new Mongo.Collection("autocompleteRecords") - -isServerSearch = (rule) -> _.isString(rule.collection) - -validateRule = (rule) -> - if rule.subscription? and not Match.test(rule.collection, String) - throw new Error("Collection name must be specified as string for server-side search") - - # XXX back-compat message, to be removed - if rule.callback? - console.warn("autocomplete no longer supports callbacks; use event listeners instead.") - -isWholeField = (rule) -> - # either '' or null both count as whole field. - return !rule.token - -getRegExp = (rule) -> - unless isWholeField(rule) - # Expressions for the range from the last word break to the current cursor position - new RegExp('(^|\\b|\\s)' + rule.token + '([\\w.]*)$') - else - # Whole-field behavior - word characters or spaces - new RegExp('(^)(.*)$') - -getFindParams = (rule, filter, limit) -> - # This is a different 'filter' - the selector from the settings - # We need to extend so that we don't copy over rule.filter - selector = _.extend({}, rule.filter || {}) - options = { limit: limit } - - # Match anything, no sort, limit X - return [ selector, options ] unless filter - - if rule.sort and rule.field - sortspec = {} - # Only sort if there is a filter, for faster performance on a match of anything - sortspec[rule.field] = 1 - options.sort = sortspec - - if _.isFunction(rule.selector) - # Custom selector - _.extend(selector, rule.selector(filter)) - else - selector[rule.field] = { - $regex: if rule.matchAll then filter else "^" + filter - # default is case insensitive search - empty string is not the same as undefined! - $options: if (typeof rule.options is 'undefined') then 'i' else rule.options - } - - return [ selector, options ] - -getField = (obj, str) -> - obj = obj[key] for key in str.split(".") - return obj - -class @AutoComplete - - @KEYS: [ - 40, # DOWN - 38, # UP - 13, # ENTER - 27, # ESCAPE - 9 # TAB - ] - - constructor: (settings) -> - @limit = settings.limit || 5 - @position = settings.position || "bottom" - - @rules = settings.rules - validateRule(rule) for rule in @rules - - @expressions = (getRegExp(rule) for rule in @rules) - - @matched = -1 - @loaded = true - - # Reactive dependencies for current matching rule and filter - @ruleDep = new Deps.Dependency - @filterDep = new Deps.Dependency - @loadingDep = new Deps.Dependency - - # autosubscribe to the record set published by the server based on the filter - # This will tear down server subscriptions when they are no longer being used. - @sub = null - @comp = Deps.autorun => - # Stop any existing sub immediately, don't wait - @sub?.stop() - - return unless (rule = @matchedRule()) and (filter = @getFilter()) isnt null - - # subscribe only for server-side collections - unless isServerSearch(rule) - @setLoaded(true) # Immediately loaded - return - - [ selector, options ] = getFindParams(rule, filter, @limit) - - # console.debug 'Subscribing to <%s> in <%s>.<%s>', filter, rule.collection, rule.field - @setLoaded(false) - subName = rule.subscription || "autocomplete-recordset" - @sub = Meteor.subscribe(subName, - selector, options, rule.collection, => @setLoaded(true)) - - teardown: -> - # Stop the reactive computation we started for this autocomplete instance - @comp.stop() - - # reactive getters and setters for @filter and the currently matched rule - matchedRule: -> - @ruleDep.depend() - if @matched >= 0 then @rules[@matched] else null - - setMatchedRule: (i) -> - @matched = i - @ruleDep.changed() - - getFilter: -> - @filterDep.depend() - return @filter - - setFilter: (x) -> - @filter = x - @filterDep.changed() - return @filter - - isLoaded: -> - @loadingDep.depend() - return @loaded - - setLoaded: (val) -> - return if val is @loaded # Don't cause redraws unnecessarily - @loaded = val - @loadingDep.changed() - - onKeyUp: -> - return unless @$element # Don't try to do this while loading - startpos = @element.selectionStart - val = @getText().substring(0, startpos) - - ### - Matching on multiple expressions. - We always go from a matched state to an unmatched one - before going to a different matched one. - ### - i = 0 - breakLoop = false - while i < @expressions.length - matches = val.match(@expressions[i]) - - # matching -> not matching - if not matches and @matched is i - @setMatchedRule(-1) - breakLoop = true - - # not matching -> matching - if matches and @matched is -1 - @setMatchedRule(i) - breakLoop = true - - # Did filter change? - if matches and @filter isnt matches[2] - @setFilter(matches[2]) - breakLoop = true - - break if breakLoop - i++ - - onKeyDown: (e) -> - return if @matched is -1 or (@constructor.KEYS.indexOf(e.keyCode) < 0) - - switch e.keyCode - when 9, 13 # TAB, ENTER - if @select() # Don't jump fields or submit if select successful - e.preventDefault() - e.stopPropagation() - # preventDefault needed below to avoid moving cursor when selecting - when 40 # DOWN - e.preventDefault() - @next() - when 38 # UP - e.preventDefault() - @prev() - when 27 # ESCAPE - @$element.blur() - @hideList() - - return - - onFocus: -> - # We need to run onKeyUp after the focus resolves, - # or the caret position (selectionStart) will not be correct - Meteor.defer => @onKeyUp() - - onBlur: -> - # We need to delay this so click events work - # TODO this is a bit of a hack; see if we can't be smarter - Meteor.setTimeout => - @hideList() - , 500 - - onItemClick: (doc, e) => @processSelection(doc, @rules[@matched]) - - onItemHover: (doc, e) -> - @tmplInst.$(".-autocomplete-item").removeClass("selected") - $(e.target).closest(".-autocomplete-item").addClass("selected") - - filteredList: -> - # @ruleDep.depend() # optional as long as we use depend on filter, because list will always get re-rendered - filter = @getFilter() # Reactively depend on the filter - return null if @matched is -1 - - rule = @rules[@matched] - # Don't display list unless we have a token or a filter (or both) - # Single field: nothing displayed until something is typed - return null unless rule.token or filter - - [ selector, options ] = getFindParams(rule, filter, @limit) - - Meteor.defer => @ensureSelection() - - # if server collection, the server has already done the filtering work - return AutoCompleteRecords.find({}, options) if isServerSearch(rule) - - # Otherwise, search on client - return rule.collection.find(selector, options) - - isShowing: -> - rule = @matchedRule() - # Same rules as above - showing = rule? and (rule.token or @getFilter()) - - # Do this after the render - if showing - Meteor.defer => - @positionContainer() - @ensureSelection() - - return showing - - # Replace text with currently selected item - select: -> - node = @tmplInst.find(".-autocomplete-item.selected") - return false unless node? - doc = Blaze.getData(node) - return false unless doc # Don't select if nothing matched - - @processSelection(doc, @rules[@matched]) - return true - - processSelection: (doc, rule) -> - replacement = getField(doc, rule.field) - - unless isWholeField(rule) - @replace(replacement, rule) - @hideList() - - else - # Empty string or doesn't exist? - # Single-field replacement: replace whole field - @setText(replacement) - - # Field retains focus, but list is hidden unless another key is pressed - # Must be deferred or onKeyUp will trigger and match again - # TODO this is a hack; see above - @onBlur() - - @$element.trigger("autocompleteselect", doc) - return - - # Replace the appropriate region - replace: (replacement) -> - startpos = @element.selectionStart - fullStuff = @getText() - val = fullStuff.substring(0, startpos) - val = val.replace(@expressions[@matched], "$1" + @rules[@matched].token + replacement) - posfix = fullStuff.substring(startpos, fullStuff.length) - separator = (if posfix.match(/^\s/) then "" else " ") - finalFight = val + separator + posfix - @setText finalFight - - newPosition = val.length + 1 - @element.setSelectionRange(newPosition, newPosition) - return - - hideList: -> - @setMatchedRule(-1) - @setFilter(null) - - getText: -> - return @$element.val() || @$element.text() - - setText: (text) -> - if @$element.is("input,textarea") - @$element.val(text) - else - @$element.html(text) - - ### - Rendering functions - ### - positionContainer: -> - # First render; Pick the first item and set css whenever list gets shown - position = @$element.position() - - rule = @matchedRule() - - offset = getCaretCoordinates(@element, @element.selectionStart) - - # In whole-field positioning, we don't move the container and make it the - # full width of the field. - if rule? and isWholeField(rule) - pos = - left: position.left - width: @$element.outerWidth() # position.offsetWidth - else # Normal positioning, at token word - pos = - left: position.left + offset.left - - # Position menu from top (above) or from bottom of caret (below, default) - if @position is "top" - pos.bottom = @$element.offsetParent().height() - position.top - offset.top - else - pos.top = position.top + offset.top + parseInt(@$element.css('font-size')) - - @tmplInst.$(".-autocomplete-container").css(pos) - - ensureSelection : -> - # Re-render; make sure selected item is something in the list or none if list empty - selectedItem = @tmplInst.$(".-autocomplete-item.selected") - - unless selectedItem.length - # Select anything - @tmplInst.$(".-autocomplete-item:first-child").addClass("selected") - - # Select next item in list - next: -> - currentItem = @tmplInst.$(".-autocomplete-item.selected") - return unless currentItem.length # Don't try to iterate an empty list - currentItem.removeClass("selected") - - next = currentItem.next() - if next.length - next.addClass("selected") - else # End of list or lost selection; Go back to first item - @tmplInst.$(".-autocomplete-item:first-child").addClass("selected") - - # Select previous item in list - prev: -> - currentItem = @tmplInst.$(".-autocomplete-item.selected") - return unless currentItem.length # Don't try to iterate an empty list - currentItem.removeClass("selected") - - prev = currentItem.prev() - if prev.length - prev.addClass("selected") - else # Beginning of list or lost selection; Go to end of list - @tmplInst.$(".-autocomplete-item:last-child").addClass("selected") - - # This doesn't need to be reactive because list already changes reactively - # and will cause all of the items to re-render anyway - currentTemplate: -> @rules[@matched].template - -AutocompleteTest = - records: AutoCompleteRecords - getRegExp: getRegExp - getFindParams: getFindParams diff --git a/packages/meteor-autocomplete/autocomplete-server.coffee b/packages/meteor-autocomplete/autocomplete-server.coffee deleted file mode 100755 index 192d5479bb20a72935a8730bb583391fee7100a6..0000000000000000000000000000000000000000 --- a/packages/meteor-autocomplete/autocomplete-server.coffee +++ /dev/null @@ -1,27 +0,0 @@ -class Autocomplete - @publishCursor: (cursor, sub) -> - # This also attaches an onStop callback to sub, so we don't need to worry about that. - # https://github.com/meteor/meteor/blob/devel/packages/mongo/collection.js - Mongo.Collection._publishCursor(cursor, sub, "autocompleteRecords") - -Meteor.publish 'autocomplete-recordset', (selector, options, collName) -> - collection = global[collName] - unless collection - throw new Error(collName + ' is not defined on the global namespace of the server.') - - # This is a semi-documented Meteor feature: - # https://github.com/meteor/meteor/blob/devel/packages/mongo-livedata/collection.js - unless collection._isInsecure() - Meteor._debug(collName + ' is a secure collection, therefore no data was returned because the client could compromise security by subscribing to arbitrary server collections via the browser console. Please write your own publish function.') - return [] # We need this for the subscription to be marked ready - - # guard against client-side DOS: hard limit to 50 - options.limit = Math.min(50, Math.abs(options.limit)) if options.limit - - # Push this into our own collection on the client so they don't interfere with other publications of the named collection. - # This also stops the observer automatically when the subscription is stopped. - Autocomplete.publishCursor( collection.find(selector, options), this) - - # Mark the subscription ready after the initial addition of documents. - this.ready() - diff --git a/packages/meteor-autocomplete/client/autocomplete-client.js b/packages/meteor-autocomplete/client/autocomplete-client.js new file mode 100755 index 0000000000000000000000000000000000000000..809f2fa77043a736705412ca3b105f05abecb7e5 --- /dev/null +++ b/packages/meteor-autocomplete/client/autocomplete-client.js @@ -0,0 +1,449 @@ +/* globals Deps, getCaretCoordinates*/ +const AutoCompleteRecords = new Mongo.Collection('autocompleteRecords'); + +const isServerSearch = function(rule) { + return _.isString(rule.collection); +}; + +const validateRule = function(rule) { + if (rule.subscription != null && !Match.test(rule.collection, String)) { + throw new Error('Collection name must be specified as string for server-side search'); + } + // XXX back-compat message, to be removed + if (rule.callback) { + console.warn('autocomplete no longer supports callbacks; use event listeners instead.'); + } +}; + +const isWholeField = function(rule) { + // either '' or null both count as whole field. + return !rule.token; +}; + +const getRegExp = function(rule) { + if (!isWholeField(rule)) { + // Expressions for the range from the last word break to the current cursor position + return new RegExp(`(^|\\b|\\s)${ rule.token }([\\w.]*)$`); + } else { + // Whole-field behavior - word characters or spaces + return new RegExp('(^)(.*)$'); + } +}; + +const getFindParams = function(rule, filter, limit) { + // This is a different 'filter' - the selector from the settings + // We need to extend so that we don't copy over rule.filter + const selector = _.extend({}, rule.filter || {}); + const options = { + limit + }; + if (!filter) { + // Match anything, no sort, limit X + return [selector, options]; + } + if (rule.sort && rule.field) { + const sortspec = {}; + // Only sort if there is a filter, for faster performance on a match of anything + sortspec[rule.field] = 1; + options.sort = sortspec; + } + if (_.isFunction(rule.selector)) { + // Custom selector + _.extend(selector, rule.selector(filter)); + } else { + selector[rule.field] = { + $regex: rule.matchAll ? filter : `^${ filter }`, + // default is case insensitive search - empty string is not the same as undefined! + $options: typeof rule.options === 'undefined' ? 'i' : rule.options + }; + } + return [selector, options]; +}; + +const getField = function(obj, str) { + const string = str.split('.'); + string.forEach(key => { + obj = obj[key]; + }); + return obj; +}; + +this.AutoComplete = class { + constructor(settings) { + this.KEYS = [40, 38, 13, 27, 9]; + this.limit = settings.limit || 5; + this.position = settings.position || 'bottom'; + this.rules = settings.rules; + const rules = this.rules; + + Object.keys(rules).forEach(key => { + const rule = rules[key]; + validateRule(rule); + }); + + this.expressions = (() => { + return Object.keys(rules).map(key => { + const rule = rules[key]; + return getRegExp(rule); + }); + })(); + this.matched = -1; + this.loaded = true; + + // Reactive dependencies for current matching rule and filter + this.ruleDep = new Deps.Dependency; + this.filterDep = new Deps.Dependency; + this.loadingDep = new Deps.Dependency; + + // Autosubscribe to the record set published by the server based on the filter + // This will tear down server subscriptions when they are no longer being used. + this.sub = null; + this.comp = Deps.autorun(() => { + const rule = this.matchedRule(); + const filter = this.getFilter(); + if (this.sub) { + // Stop any existing sub immediately, don't wait + this.sub.stop(); + } + if (!(rule && filter)) { + return; + } + + // subscribe only for server-side collections + if (!isServerSearch(rule)) { + this.setLoaded(true); + return; + } + const params = getFindParams(rule, filter, this.limit); + const selector = params[0]; + const options = params[1]; + + // console.debug 'Subscribing to <%s> in <%s>.<%s>', filter, rule.collection, rule.field + this.setLoaded(false); + const subName = rule.subscription || 'autocomplete-recordset'; + this.sub = Meteor.subscribe(subName, selector, options, rule.collection, () => { + this.setLoaded(true); + }); + }); + } + + teardown() { + // Stop the reactive computation we started for this autocomplete instance + this.comp.stop(); + } + + matchedRule() { + // reactive getters and setters for @filter and the currently matched rule + this.ruleDep.depend(); + if (this.matched >= 0) { + return this.rules[this.matched]; + } else { + return null; + } + } + + setMatchedRule(i) { + this.matched = i; + this.ruleDep.changed(); + } + + getFilter() { + this.filterDep.depend(); + return this.filter; + } + + setFilter(x) { + this.filter = x; + this.filterDep.changed(); + return this.filter; + } + + isLoaded() { + this.loadingDep.depend(); + return this.loaded; + } + + setLoaded(val) { + if (val === this.loaded) { + return; //Don't cause redraws unnecessarily + } + this.loaded = val; + this.loadingDep.changed(); + } + + onKeyUp() { + if (!this.$element) { + return; //Don't try to do this while loading + } + const startpos = this.element.selectionStart; + const val = this.getText().substring(0, startpos); + + /* + Matching on multiple expressions. + We always go from a matched state to an unmatched one + before going to a different matched one. + */ + let i = 0; + let breakLoop = false; + while (i < this.expressions.length) { + const matches = val.match(this.expressions[i]); + + // matching -> not matching + if (!matches && this.matched === i) { + this.setMatchedRule(-1); + breakLoop = true; + } + + // not matching -> matching + if (matches && this.matched === -1) { + this.setMatchedRule(i); + breakLoop = true; + } + + // Did filter change? + if (matches && this.filter !== matches[2]) { + this.setFilter(matches[2]); + breakLoop = true; + } + if (breakLoop) { + break; + } + i++; + } + } + + onKeyDown(e) { + if (this.matched === -1 || (this.KEYS.indexOf(e.keyCode) < 0)) { + return; + } + switch (e.keyCode) { + case 9: //TAB + case 13: //ENTER + if (this.select()) { //Don't jump fields or submit if select successful + e.preventDefault(); + e.stopPropagation(); + } + break; + // preventDefault needed below to avoid moving cursor when selecting + case 40: //DOWN + e.preventDefault(); + this.next(); + break; + case 38: //UP + e.preventDefault(); + this.prev(); + break; + case 27: //ESCAPE + this.$element.blur(); + this.hideList(); + } + } + + onFocus() { + // We need to run onKeyUp after the focus resolves, + // or the caret position (selectionStart) will not be correct + Meteor.defer(() => this.onKeyUp()); + } + + onBlur() { + // We need to delay this so click events work + // TODO this is a bit of a hack, see if we can't be smarter + Meteor.setTimeout(() => { + this.hideList(); + }, 500); + } + + onItemClick(doc) { + this.processSelection(doc, this.rules[this.matched]); + } + + onItemHover(doc, e) { + this.tmplInst.$('.-autocomplete-item').removeClass('selected'); + $(e.target).closest('.-autocomplete-item').addClass('selected'); + } + + filteredList() { + // @ruleDep.depend() # optional as long as we use depend on filter, because list will always get re-rendered + const filter = this.getFilter(); //Reactively depend on the filter + if (this.matched === -1) { + return null; + } + const rule = this.rules[this.matched]; + + // Don't display list unless we have a token or a filter (or both) + // Single field: nothing displayed until something is typed + if (!(rule.token || filter)) { + return null; + } + const params = getFindParams(rule, filter, this.limit); + const selector = params[0]; + const options = params[1]; + Meteor.defer(() => this.ensureSelection()); + + // if server collection, the server has already done the filtering work + if (isServerSearch(rule)) { + return AutoCompleteRecords.find({}, options); + } + // Otherwise, search on client + return rule.collection.find(selector, options); + } + + isShowing() { + const rule = this.matchedRule(); + // Same rules as above + const showing = rule && (rule.token || this.getFilter()); + + // Do this after the render + if (showing) { + Meteor.defer(() => { + this.positionContainer(); + this.ensureSelection(); + }); + } + return showing; + } + + // Replace text with currently selected item + select() { + const node = this.tmplInst.find('.-autocomplete-item.selected'); + if (node == null) { + return false; + } + const doc = Blaze.getData(node); + if (!doc) { + return false; //Don't select if nothing matched + + } + this.processSelection(doc, this.rules[this.matched]); + return true; + } + + processSelection(doc, rule) { + const replacement = getField(doc, rule.field); + if (!isWholeField(rule)) { + this.replace(replacement, rule); + this.hideList(); + } else { + + // Empty string or doesn't exist? + // Single-field replacement: replace whole field + this.setText(replacement); + + // Field retains focus, but list is hidden unless another key is pressed + // Must be deferred or onKeyUp will trigger and match again + // TODO this is a hack; see above + this.onBlur(); + } + this.$element.trigger('autocompleteselect', doc); + } + + + // Replace the appropriate region + replace(replacement) { + const startpos = this.element.selectionStart; + const fullStuff = this.getText(); + let val = fullStuff.substring(0, startpos); + val = val.replace(this.expressions[this.matched], `$1${ this.rules[this.matched].token }${ replacement }`); + const posfix = fullStuff.substring(startpos, fullStuff.length); + const separator = (posfix.match(/^\s/) ? '' : ' '); + const finalFight = val + separator + posfix; + this.setText(finalFight); + const newPosition = val.length + 1; + this.element.setSelectionRange(newPosition, newPosition); + } + + hideList() { + this.setMatchedRule(-1); + this.setFilter(null); + } + + getText() { + return this.$element.val() || this.$element.text(); + } + + setText(text) { + if (this.$element.is('input,textarea')) { + this.$element.val(text); + } else { + this.$element.html(text); + } + } + + + /* + Rendering functions + */ + + positionContainer() { + // First render; Pick the first item and set css whenever list gets shown + let pos; + const position = this.$element.position(); + const rule = this.matchedRule(); + const offset = getCaretCoordinates(this.element, this.element.selectionStart); + + // In whole-field positioning, we don't move the container and make it the + // full width of the field. + if (rule && isWholeField(rule)) { + pos = { + left: position.left, + width: this.$element.outerWidth() //position.offsetWidth + }; + } else { //Normal positioning, at token word + pos = { left: position.left + offset.left }; + } + + // Position menu from top (above) or from bottom of caret (below, default) + if (this.position === 'top') { + pos.bottom = this.$element.offsetParent().height() - position.top - offset.top; + } else { + pos.top = position.top + offset.top + parseInt(this.$element.css('font-size')); + } + this.tmplInst.$('.-autocomplete-container').css(pos); + } + + ensureSelection() { + // Re-render; make sure selected item is something in the list or none if list empty + const selectedItem = this.tmplInst.$('.-autocomplete-item.selected'); + if (!selectedItem.length) { + // Select anything + this.tmplInst.$('.-autocomplete-item:first-child').addClass('selected'); + } + } + + // Select next item in list + next() { + const currentItem = this.tmplInst.$('.-autocomplete-item.selected'); + if (!currentItem.length) { + return; + } + currentItem.removeClass('selected'); + const next = currentItem.next(); + if (next.length) { + next.addClass('selected'); + } else { //End of list or lost selection; Go back to first item + this.tmplInst.$('.-autocomplete-item:first-child').addClass('selected'); + } + } + + //Select previous item in list + prev() { + const currentItem = this.tmplInst.$('.-autocomplete-item.selected'); + if (!currentItem.length) { + return; //Don't try to iterate an empty list + } + currentItem.removeClass('selected'); + const prev = currentItem.prev(); + if (prev.length) { + prev.addClass('selected'); + } else { //Beginning of list or lost selection; Go to end of list + this.tmplInst.$('.-autocomplete-item:last-child').addClass('selected'); + } + } + + // This doesn't need to be reactive because list already changes reactively + // and will cause all of the items to re-render anyway + currentTemplate() { + return this.rules[this.matched].template; + } + +}; diff --git a/packages/meteor-autocomplete/autocomplete.css b/packages/meteor-autocomplete/client/autocomplete.css similarity index 100% rename from packages/meteor-autocomplete/autocomplete.css rename to packages/meteor-autocomplete/client/autocomplete.css diff --git a/packages/meteor-autocomplete/inputs.html b/packages/meteor-autocomplete/client/inputs.html similarity index 100% rename from packages/meteor-autocomplete/inputs.html rename to packages/meteor-autocomplete/client/inputs.html diff --git a/packages/meteor-autocomplete/client/templates.js b/packages/meteor-autocomplete/client/templates.js new file mode 100755 index 0000000000000000000000000000000000000000..97b2e25971355c73ee7880e34c709a9d3746eeab --- /dev/null +++ b/packages/meteor-autocomplete/client/templates.js @@ -0,0 +1,80 @@ +/* globals AutoComplete */ +// Events on template instances, sent to the autocomplete class +const acEvents = { + 'keydown'(e, t) { + t.ac.onKeyDown(e); + }, + 'keyup'(e, t) { + t.ac.onKeyUp(e); + }, + 'focus'(e, t) { + t.ac.onFocus(e); + }, + 'blur'(e, t) { + t.ac.onBlur(e); + } +}; + +Template.inputAutocomplete.events(acEvents); + +Template.textareaAutocomplete.events(acEvents); + +const attributes = function() { + return _.omit(this, 'settings'); //Render all but the settings parameter + +}; + +const autocompleteHelpers = { + attributes, + autocompleteContainer: new Template('AutocompleteContainer', function() { + const ac = new AutoComplete(Blaze.getData().settings); + // Set the autocomplete object on the parent template instance + this.parentView.templateInstance().ac = ac; + + // Set nodes on render in the autocomplete class + this.onViewReady(function() { + ac.element = this.parentView.firstNode(); + ac.$element = $(ac.element); + }); + return Blaze.With(ac, function() { //eslint-disable-line + return Template._autocompleteContainer; + }); + }) +}; + +Template.inputAutocomplete.helpers(autocompleteHelpers); + +Template.textareaAutocomplete.helpers(autocompleteHelpers); + +Template._autocompleteContainer.rendered = function() { + this.data.tmplInst = this; +}; + +Template._autocompleteContainer.destroyed = function() { + // Meteor._debug "autocomplete destroyed" + this.data.teardown(); +}; + + +/* + List rendering helpers + */ + +Template._autocompleteContainer.events({ + // t.data is the AutoComplete instance; `this` is the data item + 'click .-autocomplete-item'(e, t) { + t.data.onItemClick(this, e); + }, + 'mouseenter .-autocomplete-item'(e, t) { + t.data.onItemHover(this, e); + } +}); + +Template._autocompleteContainer.helpers({ + empty() { + return this.filteredList().count() === 0; + }, + noMatchTemplate() { + return this.matchedRule().noMatchTemplate || Template._noMatch; + } +}); diff --git a/packages/meteor-autocomplete/package.js b/packages/meteor-autocomplete/package.js index 5a6912af07a94fb162a3a78c14c36212a2c23423..ab887e0432afc1dffa6ef851c651759c87b11481 100755 --- a/packages/meteor-autocomplete/package.js +++ b/packages/meteor-autocomplete/package.js @@ -7,21 +7,21 @@ Package.describe({ Package.onUse(function(api) { api.use(['blaze', 'templating', 'jquery'], 'client'); - api.use(['coffeescript', 'underscore', 'ecmascript']); // both + api.use(['underscore', 'ecmascript']); // both api.use(['mongo', 'ddp']); api.use('dandv:caret-position@2.1.0-3', 'client'); // Our files api.addFiles([ - 'autocomplete.css', - 'inputs.html', - 'autocomplete-client.coffee', - 'templates.coffee' + 'client/autocomplete.css', + 'client/inputs.html', + 'client/autocomplete-client.js', + 'client/templates.js' ], 'client'); api.addFiles([ - 'autocomplete-server.coffee' + 'server/autocomplete-server.js' ], 'server'); api.export('Autocomplete', 'server'); @@ -31,7 +31,6 @@ Package.onUse(function(api) { Package.onTest(function(api) { api.use('mizzao:autocomplete'); - api.use('coffeescript'); api.use('mongo'); api.use('tinytest'); diff --git a/packages/meteor-autocomplete/server/autocomplete-server.js b/packages/meteor-autocomplete/server/autocomplete-server.js new file mode 100755 index 0000000000000000000000000000000000000000..582daf40c1d1a1e6841f58e0a319a199a9b07a7a --- /dev/null +++ b/packages/meteor-autocomplete/server/autocomplete-server.js @@ -0,0 +1,31 @@ +// This also attaches an onStop callback to sub, so we don't need to worry about that. +// https://github.com/meteor/meteor/blob/devel/packages/mongo/collection.js +const Autocomplete = class { + publishCursor(cursor, sub) { + Mongo.Collection._publishCursor(cursor, sub, 'autocompleteRecords'); + } +}; + +Meteor.publish('autocomplete-recordset', function(selector, options, collName) { + const collection = global[collName]; + + // This is a semi-documented Meteor feature: + // https://github.com/meteor/meteor/blob/devel/packages/mongo-livedata/collection.js + if (!collection) { + throw new Error(`${ collName } is not defined on the global namespace of the server.`); + } + if (!collection._isInsecure()) { + Meteor._debug(`${ collName } is a secure collection, therefore no data was returned because the client could compromise security by subscribing to arbitrary server collections via the browser console. Please write your own publish function.`); + return []; // We need this for the subscription to be marked ready + } + if (options.limit) { + // guard against client-side DOS: hard limit to 50 + options.limit = Math.min(50, Math.abs(options.limit)); + } + + // Push this into our own collection on the client so they don't interfere with other publications of the named collection. + // This also stops the observer automatically when the subscription is stopped. + Autocomplete.publishCursor(collection.find(selector, options), this); + // Mark the subscription ready after the initial addition of documents. + this.ready(); +}); diff --git a/packages/meteor-autocomplete/templates.coffee b/packages/meteor-autocomplete/templates.coffee deleted file mode 100755 index cd56d8ba739846aa43fde4f8deca69d1eaf5dd63..0000000000000000000000000000000000000000 --- a/packages/meteor-autocomplete/templates.coffee +++ /dev/null @@ -1,50 +0,0 @@ -# Events on template instances, sent to the autocomplete class -acEvents = - "keydown": (e, t) -> t.ac.onKeyDown(e) - "keyup": (e, t) -> t.ac.onKeyUp(e) - "focus": (e, t) -> t.ac.onFocus(e) - "blur": (e, t) -> t.ac.onBlur(e) - -Template.inputAutocomplete.events(acEvents) -Template.textareaAutocomplete.events(acEvents) - -attributes = -> _.omit(@, 'settings') # Render all but the settings parameter - -autocompleteHelpers = { - attributes, - autocompleteContainer: new Template('AutocompleteContainer', -> - ac = new AutoComplete( Blaze.getData().settings ) - # Set the autocomplete object on the parent template instance - this.parentView.templateInstance().ac = ac - - # Set nodes on render in the autocomplete class - this.onViewReady -> - ac.element = this.parentView.firstNode() - ac.$element = $(ac.element) - - return Blaze.With(ac, -> Template._autocompleteContainer) - ) -} - -Template.inputAutocomplete.helpers(autocompleteHelpers) -Template.textareaAutocomplete.helpers(autocompleteHelpers) - -Template._autocompleteContainer.rendered = -> - @data.tmplInst = this - -Template._autocompleteContainer.destroyed = -> - # Meteor._debug "autocomplete destroyed" - @data.teardown() - -### - List rendering helpers -### - -Template._autocompleteContainer.events - # t.data is the AutoComplete instance; `this` is the data item - "click .-autocomplete-item": (e, t) -> t.data.onItemClick(this, e) - "mouseenter .-autocomplete-item": (e, t) -> t.data.onItemHover(this, e) - -Template._autocompleteContainer.helpers - empty: -> @filteredList().count() is 0 - noMatchTemplate: -> @matchedRule().noMatchTemplate || Template._noMatch diff --git a/packages/rocketchat-api/package.js b/packages/rocketchat-api/package.js index ef8aaf159327b7b036b6c590911440315fc6646e..16d34b948044b991546631d6295b75e200caac4b 100644 --- a/packages/rocketchat-api/package.js +++ b/packages/rocketchat-api/package.js @@ -17,6 +17,7 @@ Package.onUse(function(api) { api.addFiles('server/settings.js', 'server'); //Register v1 helpers + api.addFiles('server/v1/helpers/requestParams.js', 'server'); api.addFiles('server/v1/helpers/getPaginationItems.js', 'server'); api.addFiles('server/v1/helpers/getUserFromParams.js', 'server'); api.addFiles('server/v1/helpers/isUserFromParams.js', 'server'); diff --git a/packages/rocketchat-api/server/api.js b/packages/rocketchat-api/server/api.js index f1e71eeaa5a29c859d8d7688b566d59274891fee..a8ee3da879d6ebc4cd04d71111b003c27a5f1c42 100644 --- a/packages/rocketchat-api/server/api.js +++ b/packages/rocketchat-api/server/api.js @@ -104,7 +104,7 @@ class API extends Restivus { try { result = originalAction.apply(this); } catch (e) { - this.logger.debug(`${ method } ${ route } threw an error:`, e); + this.logger.debug(`${ method } ${ route } threw an error:`, e.stack); return RocketChat.API.v1.failure(e.message, e.error); } diff --git a/packages/rocketchat-api/server/v1/channels.js b/packages/rocketchat-api/server/v1/channels.js index da10b21283d5f280aeb220152e2dcc8d6c36a3e5..3d2e33387f9e47470aa140dd2ed0e74562c83f86 100644 --- a/packages/rocketchat-api/server/v1/channels.js +++ b/packages/rocketchat-api/server/v1/channels.js @@ -1,18 +1,18 @@ //Returns the channel IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property -function findChannelByIdOrName({ roomId, roomName, checkedArchived = true }) { - if ((!roomId || !roomId.trim()) && (!roomName || !roomName.trim())) { +function findChannelByIdOrName({ params, checkedArchived = true }) { + if ((!params.roomId || !params.roomId.trim()) && (!params.roomName || !params.roomName.trim())) { throw new Meteor.Error('error-roomid-param-not-provided', 'The parameter "roomId" or "roomName" is required'); } let room; - if (roomId) { - room = RocketChat.models.Rooms.findOneById(roomId, { fields: RocketChat.API.v1.defaultFieldsToExclude }); - } else if (roomName) { - room = RocketChat.models.Rooms.findOneByName(roomName, { fields: RocketChat.API.v1.defaultFieldsToExclude }); + if (params.roomId) { + room = RocketChat.models.Rooms.findOneById(params.roomId, { fields: RocketChat.API.v1.defaultFieldsToExclude }); + } else if (params.roomName) { + room = RocketChat.models.Rooms.findOneByName(params.roomName, { fields: RocketChat.API.v1.defaultFieldsToExclude }); } if (!room || room.t !== 'c') { - throw new Meteor.Error('error-room-not-found', `No channel found by the id of: ${ roomId }`); + throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any channel'); } if (checkedArchived && room.archived) { @@ -24,7 +24,7 @@ function findChannelByIdOrName({ roomId, roomName, checkedArchived = true }) { RocketChat.API.v1.addRoute('channels.addAll', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); Meteor.runAsUser(this.userId, () => { Meteor.call('addAllUserToRoom', findResult._id, this.bodyParams.activeUsersOnly); @@ -38,7 +38,7 @@ RocketChat.API.v1.addRoute('channels.addAll', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.addModerator', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); const user = this.getUserFromParams(); @@ -52,7 +52,7 @@ RocketChat.API.v1.addRoute('channels.addModerator', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.addOwner', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); const user = this.getUserFromParams(); @@ -66,7 +66,7 @@ RocketChat.API.v1.addRoute('channels.addOwner', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.archive', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); Meteor.runAsUser(this.userId, () => { Meteor.call('archiveRoom', findResult._id); @@ -78,7 +78,7 @@ RocketChat.API.v1.addRoute('channels.archive', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.cleanHistory', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); if (!this.bodyParams.latest) { return RocketChat.API.v1.failure('Body parameter "latest" is required.'); @@ -106,7 +106,7 @@ RocketChat.API.v1.addRoute('channels.cleanHistory', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.close', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); const sub = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(findResult._id, this.userId); @@ -162,7 +162,7 @@ RocketChat.API.v1.addRoute('channels.create', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.delete', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); //The find method returns either with the group or the failur @@ -182,7 +182,7 @@ RocketChat.API.v1.addRoute('channels.getIntegrations', { authRequired: true }, { return RocketChat.API.v1.unauthorized(); } - const findResult = findChannelByIdOrName({ roomId: this.queryParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); let includeAllPublicChannels = true; if (typeof this.queryParams.includeAllPublicChannels !== 'undefined') { @@ -222,7 +222,7 @@ RocketChat.API.v1.addRoute('channels.getIntegrations', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.history', { authRequired: true }, { get() { - const findResult = findChannelByIdOrName({ roomId: this.queryParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); let latestDate = new Date(); if (this.queryParams.latest) { @@ -262,7 +262,7 @@ RocketChat.API.v1.addRoute('channels.history', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.info', { authRequired: true }, { get() { - const findResult = findChannelByIdOrName({ roomId: this.queryParams.roomId, roomName: this.queryParams.roomName, checkedArchived: false }); + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); return RocketChat.API.v1.success({ channel: RocketChat.models.Rooms.findOneById(findResult._id, { fields: RocketChat.API.v1.defaultFieldsToExclude }) @@ -272,7 +272,7 @@ RocketChat.API.v1.addRoute('channels.info', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.invite', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); const user = this.getUserFromParams(); @@ -288,7 +288,7 @@ RocketChat.API.v1.addRoute('channels.invite', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.join', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); Meteor.runAsUser(this.userId, () => { Meteor.call('joinRoom', findResult._id, this.bodyParams.joinCode); @@ -302,7 +302,7 @@ RocketChat.API.v1.addRoute('channels.join', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.kick', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); const user = this.getUserFromParams(); @@ -318,7 +318,7 @@ RocketChat.API.v1.addRoute('channels.kick', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.leave', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); Meteor.runAsUser(this.userId, () => { Meteor.call('leaveRoom', findResult._id); @@ -414,7 +414,7 @@ RocketChat.API.v1.addRoute('channels.online', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.open', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); const sub = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(findResult._id, this.userId); @@ -436,7 +436,7 @@ RocketChat.API.v1.addRoute('channels.open', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.removeModerator', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); const user = this.getUserFromParams(); @@ -450,7 +450,7 @@ RocketChat.API.v1.addRoute('channels.removeModerator', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.removeOwner', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); const user = this.getUserFromParams(); @@ -468,7 +468,7 @@ RocketChat.API.v1.addRoute('channels.rename', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "name" is required'); } - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: { roomId: this.bodyParams.roomId} }); if (findResult.name === this.bodyParams.name) { return RocketChat.API.v1.failure('The channel name is the same as what it would be renamed to.'); @@ -490,7 +490,7 @@ RocketChat.API.v1.addRoute('channels.setDescription', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "description" is required'); } - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); if (findResult.description === this.bodyParams.description) { return RocketChat.API.v1.failure('The channel description is the same as what it would be changed to.'); @@ -512,7 +512,7 @@ RocketChat.API.v1.addRoute('channels.setJoinCode', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "joinCode" is required'); } - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); Meteor.runAsUser(this.userId, () => { Meteor.call('saveRoomSettings', findResult._id, 'joinCode', this.bodyParams.joinCode); @@ -530,7 +530,7 @@ RocketChat.API.v1.addRoute('channels.setPurpose', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "purpose" is required'); } - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); if (findResult.description === this.bodyParams.purpose) { return RocketChat.API.v1.failure('The channel purpose (description) is the same as what it would be changed to.'); @@ -552,7 +552,7 @@ RocketChat.API.v1.addRoute('channels.setReadOnly', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "readOnly" is required'); } - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); if (findResult.ro === this.bodyParams.readOnly) { return RocketChat.API.v1.failure('The channel read only setting is the same as what it would be changed to.'); @@ -574,7 +574,7 @@ RocketChat.API.v1.addRoute('channels.setTopic', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "topic" is required'); } - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); if (findResult.topic === this.bodyParams.topic) { return RocketChat.API.v1.failure('The channel topic is the same as what it would be changed to.'); @@ -596,7 +596,7 @@ RocketChat.API.v1.addRoute('channels.setType', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "type" is required'); } - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId }); + const findResult = findChannelByIdOrName({ params: this.requestParams() }); if (findResult.t === this.bodyParams.type) { return RocketChat.API.v1.failure('The channel type is the same as what it would be changed to.'); @@ -614,7 +614,7 @@ RocketChat.API.v1.addRoute('channels.setType', { authRequired: true }, { RocketChat.API.v1.addRoute('channels.unarchive', { authRequired: true }, { post() { - const findResult = findChannelByIdOrName({ roomId: this.bodyParams.roomId, checkedArchived: false }); + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); if (!findResult.archived) { return RocketChat.API.v1.failure(`The channel, ${ findResult.name }, is not archived`); diff --git a/packages/rocketchat-api/server/v1/chat.js b/packages/rocketchat-api/server/v1/chat.js index a003368c9c1f8accd8a9e7fcaf8f51e9161b450b..4404750300f73cfd00c9cc6051c3261d871c19cf 100644 --- a/packages/rocketchat-api/server/v1/chat.js +++ b/packages/rocketchat-api/server/v1/chat.js @@ -28,6 +28,28 @@ RocketChat.API.v1.addRoute('chat.delete', { authRequired: true }, { } }); +RocketChat.API.v1.addRoute('chat.getMessage', { authRequired: true }, { + get() { + if (!this.queryParams.msgId) { + return RocketChat.API.v1.failure('The "msgId" query parameter must be provided.'); + } + + + let msg; + Meteor.runAsUser(this.userId, () => { + msg = Meteor.call('getSingleMessage', this.queryParams.msgId); + }); + + if (!msg) { + return RocketChat.API.v1.failure(); + } + + return RocketChat.API.v1.success({ + message: msg + }); + } +}); + RocketChat.API.v1.addRoute('chat.postMessage', { authRequired: true }, { post() { const messageReturn = processWebhookMessage(this.bodyParams, this.user)[0]; diff --git a/packages/rocketchat-api/server/v1/groups.js b/packages/rocketchat-api/server/v1/groups.js index 10bef449b9c1a51e83f15a76d842dc6f46861ec2..d5c11136d388bd8a6171261c5be6aedf55e836b6 100644 --- a/packages/rocketchat-api/server/v1/groups.js +++ b/packages/rocketchat-api/server/v1/groups.js @@ -1,18 +1,18 @@ //Returns the private group subscription IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property -function findPrivateGroupByIdOrName({ roomId, roomName, userId, checkedArchived = true }) { - if ((!roomId || !roomId.trim()) && (!roomName || !roomName.trim())) { +function findPrivateGroupByIdOrName({ params, userId, checkedArchived = true }) { + if ((!params.roomId || !params.roomId.trim()) && (!params.roomName || !params.roomName.trim())) { throw new Meteor.Error('error-roomid-param-not-provided', 'The parameter "roomId" or "roomName" is required'); } let roomSub; - if (roomId) { - roomSub = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(roomId, userId); - } else if (roomName) { - roomSub = RocketChat.models.Subscriptions.findOneByRoomNameAndUserId(roomName, userId); + if (params.roomId) { + roomSub = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(params.roomId, userId); + } else if (params.roomName) { + roomSub = RocketChat.models.Subscriptions.findOneByRoomNameAndUserId(params.roomName, userId); } if (!roomSub || roomSub.t !== 'p') { - throw new Meteor.Error('error-room-not-found', `No private group by the id of: ${ roomId }`); + throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); } if (checkedArchived && roomSub.archived) { @@ -24,7 +24,7 @@ function findPrivateGroupByIdOrName({ roomId, roomName, userId, checkedArchived RocketChat.API.v1.addRoute('groups.addAll', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); Meteor.runAsUser(this.userId, () => { Meteor.call('addAllUserToRoom', findResult.rid, this.bodyParams.activeUsersOnly); @@ -38,7 +38,7 @@ RocketChat.API.v1.addRoute('groups.addAll', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.addModerator', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); const user = this.getUserFromParams(); @@ -52,7 +52,7 @@ RocketChat.API.v1.addRoute('groups.addModerator', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.addOwner', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); const user = this.getUserFromParams(); @@ -67,7 +67,7 @@ RocketChat.API.v1.addRoute('groups.addOwner', { authRequired: true }, { //Archives a private group only if it wasn't RocketChat.API.v1.addRoute('groups.archive', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); Meteor.runAsUser(this.userId, () => { Meteor.call('archiveRoom', findResult.rid); @@ -79,10 +79,10 @@ RocketChat.API.v1.addRoute('groups.archive', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.close', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId, checkedArchived: false }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); if (!findResult.open) { - return RocketChat.API.v1.failure(`The private group with an id "${ this.bodyParams.roomId }" is already closed to the sender`); + return RocketChat.API.v1.failure(`The private group, ${ findResult.name }, is already closed to the sender`); } Meteor.runAsUser(this.userId, () => { @@ -130,7 +130,7 @@ RocketChat.API.v1.addRoute('groups.create', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.delete', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId, checkedArchived: false }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); Meteor.runAsUser(this.userId, () => { Meteor.call('eraseRoom', findResult.rid); @@ -148,7 +148,7 @@ RocketChat.API.v1.addRoute('groups.getIntegrations', { authRequired: true }, { return RocketChat.API.v1.unauthorized(); } - const findResult = findPrivateGroupByIdOrName({ roomId: this.queryParams.roomId, userId: this.userId, checkedArchived: false }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); let includeAllPrivateGroups = true; if (typeof this.queryParams.includeAllPrivateGroups !== 'undefined') { @@ -182,7 +182,7 @@ RocketChat.API.v1.addRoute('groups.getIntegrations', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.history', { authRequired: true }, { get() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.queryParams.roomId, userId: this.userId, checkedArchived: false }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); let latestDate = new Date(); if (this.queryParams.latest) { @@ -222,7 +222,7 @@ RocketChat.API.v1.addRoute('groups.history', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.info', { authRequired: true }, { get() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.queryParams.roomId, roomName: this.queryParams.roomName, userId: this.userId, checkedArchived: false }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); return RocketChat.API.v1.success({ group: RocketChat.models.Rooms.findOneById(findResult.rid, { fields: RocketChat.API.v1.defaultFieldsToExclude }) @@ -232,7 +232,7 @@ RocketChat.API.v1.addRoute('groups.info', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.invite', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); const user = this.getUserFromParams(); @@ -248,7 +248,7 @@ RocketChat.API.v1.addRoute('groups.invite', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.kick', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); const user = this.getUserFromParams(); @@ -262,7 +262,7 @@ RocketChat.API.v1.addRoute('groups.kick', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.leave', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); Meteor.runAsUser(this.userId, () => { Meteor.call('leaveRoom', findResult.rid); @@ -331,10 +331,10 @@ RocketChat.API.v1.addRoute('groups.online', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.open', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId, checkedArchived: false }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); if (findResult.open) { - return RocketChat.API.v1.failure(`The private group, ${ this.bodyParams.name }, is already open for the sender`); + return RocketChat.API.v1.failure(`The private group, ${ findResult.name }, is already open for the sender`); } Meteor.runAsUser(this.userId, () => { @@ -347,7 +347,7 @@ RocketChat.API.v1.addRoute('groups.open', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.removeModerator', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); const user = this.getUserFromParams(); @@ -361,7 +361,7 @@ RocketChat.API.v1.addRoute('groups.removeModerator', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.removeOwner', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); const user = this.getUserFromParams(); @@ -379,7 +379,7 @@ RocketChat.API.v1.addRoute('groups.rename', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "name" is required'); } - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: { roomId: this.bodyParams.roomId}, userId: this.userId }); Meteor.runAsUser(this.userId, () => { Meteor.call('saveRoomSettings', findResult.rid, 'roomName', this.bodyParams.name); @@ -397,7 +397,7 @@ RocketChat.API.v1.addRoute('groups.setDescription', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "description" is required'); } - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); Meteor.runAsUser(this.userId, () => { Meteor.call('saveRoomSettings', findResult.rid, 'roomDescription', this.bodyParams.description); @@ -415,7 +415,7 @@ RocketChat.API.v1.addRoute('groups.setPurpose', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "purpose" is required'); } - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); Meteor.runAsUser(this.userId, () => { Meteor.call('saveRoomSettings', findResult.rid, 'roomDescription', this.bodyParams.purpose); @@ -433,7 +433,7 @@ RocketChat.API.v1.addRoute('groups.setReadOnly', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "readOnly" is required'); } - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); if (findResult.ro === this.bodyParams.readOnly) { return RocketChat.API.v1.failure('The private group read only setting is the same as what it would be changed to.'); @@ -455,7 +455,7 @@ RocketChat.API.v1.addRoute('groups.setTopic', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "topic" is required'); } - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); Meteor.runAsUser(this.userId, () => { Meteor.call('saveRoomSettings', findResult.rid, 'roomTopic', this.bodyParams.topic); @@ -473,7 +473,7 @@ RocketChat.API.v1.addRoute('groups.setType', { authRequired: true }, { return RocketChat.API.v1.failure('The bodyParam "type" is required'); } - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); if (findResult.t === this.bodyParams.type) { return RocketChat.API.v1.failure('The private group type is the same as what it would be changed to.'); @@ -491,7 +491,7 @@ RocketChat.API.v1.addRoute('groups.setType', { authRequired: true }, { RocketChat.API.v1.addRoute('groups.unarchive', { authRequired: true }, { post() { - const findResult = findPrivateGroupByIdOrName({ roomId: this.bodyParams.roomId, userId: this.userId, checkedArchived: false }); + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); Meteor.runAsUser(this.userId, () => { Meteor.call('unarchiveRoom', findResult.rid); diff --git a/packages/rocketchat-api/server/v1/helpers/getUserFromParams.js b/packages/rocketchat-api/server/v1/helpers/getUserFromParams.js index 01c075ea0abbebb2c89e138fbad39329da52ca5c..c52296f0fb7aedafe0ed423eb84bd9778485cd7b 100644 --- a/packages/rocketchat-api/server/v1/helpers/getUserFromParams.js +++ b/packages/rocketchat-api/server/v1/helpers/getUserFromParams.js @@ -2,32 +2,19 @@ RocketChat.API.v1.helperMethods.set('getUserFromParams', function _getUserFromParams() { const doesntExist = { _doesntExist: true }; let user; + const params = this.requestParams(); - switch (this.request.method) { - case 'POST': - case 'PUT': - if (this.bodyParams.userId && this.bodyParams.userId.trim()) { - user = RocketChat.models.Users.findOneById(this.bodyParams.userId) || doesntExist; - } else if (this.bodyParams.username && this.bodyParams.username.trim()) { - user = RocketChat.models.Users.findOneByUsername(this.bodyParams.username) || doesntExist; - } else if (this.bodyParams.user && this.bodyParams.user.trim()) { - user = RocketChat.models.Users.findOneByUsername(this.bodyParams.user) || doesntExist; - } - break; - default: - if (this.queryParams.userId && this.queryParams.userId.trim()) { - user = RocketChat.models.Users.findOneById(this.queryParams.userId) || doesntExist; - } else if (this.queryParams.username && this.queryParams.username.trim()) { - user = RocketChat.models.Users.findOneByUsername(this.queryParams.username) || doesntExist; - } else if (this.queryParams.user && this.queryParams.user.trim()) { - user = RocketChat.models.Users.findOneByUsername(this.queryParams.user) || doesntExist; - } - break; + if (params.userId && params.userId.trim()) { + user = RocketChat.models.Users.findOneById(params.userId) || doesntExist; + } else if (params.username && params.username.trim()) { + user = RocketChat.models.Users.findOneByUsername(params.username) || doesntExist; + } else if (params.user && params.user.trim()) { + user = RocketChat.models.Users.findOneByUsername(params.user) || doesntExist; + } else { + throw new Meteor.Error('error-user-param-not-provided', 'The required "userId" or "username" param was not provided'); } - if (!user) { - throw new Meteor.Error('error-user-param-not-provided', 'The required "userId" or "username" param was not provided'); - } else if (user._doesntExist) { + if (user._doesntExist) { throw new Meteor.Error('error-invalid-user', 'The required "userId" or "username" param provided does not match any users'); } diff --git a/packages/rocketchat-api/server/v1/helpers/isUserFromParams.js b/packages/rocketchat-api/server/v1/helpers/isUserFromParams.js index f0b24a78096b84753d74f3d8e71c5f7db6127aae..fab907bc96da76e4d4a3ef097145da34e4efcb7e 100644 --- a/packages/rocketchat-api/server/v1/helpers/isUserFromParams.js +++ b/packages/rocketchat-api/server/v1/helpers/isUserFromParams.js @@ -1,5 +1,8 @@ RocketChat.API.v1.helperMethods.set('isUserFromParams', function _isUserFromParams() { - return (this.queryParams.userId && this.userId === this.queryParams.userId) || - (this.queryParams.username && this.user.username === this.queryParams.username) || - (this.queryParams.user && this.user.username === this.queryParams.user); + const params = this.requestParams(); + + return (!params.userId && !params.username && !params.user) || + (params.userId && this.userId === params.userId) || + (params.username && this.user.username === params.username) || + (params.user && this.user.username === params.user); }); diff --git a/packages/rocketchat-api/server/v1/helpers/requestParams.js b/packages/rocketchat-api/server/v1/helpers/requestParams.js new file mode 100644 index 0000000000000000000000000000000000000000..bc5718313913ab2839d46c77d80d9c8b1e0f4eef --- /dev/null +++ b/packages/rocketchat-api/server/v1/helpers/requestParams.js @@ -0,0 +1,3 @@ +RocketChat.API.v1.helperMethods.set('requestParams', function _requestParams() { + return ['POST', 'PUT'].includes(this.request.method) ? this.bodyParams : this.queryParams; +}); diff --git a/packages/rocketchat-api/server/v1/users.js b/packages/rocketchat-api/server/v1/users.js index aa66fab610f0714dc65cd1eb7aaa007a5b23b360..2a9651c3098b1c95bc94cd8551db799c58a07b15 100644 --- a/packages/rocketchat-api/server/v1/users.js +++ b/packages/rocketchat-api/server/v1/users.js @@ -197,32 +197,34 @@ RocketChat.API.v1.addRoute('users.setAvatar', { authRequired: true }, { return RocketChat.API.v1.unauthorized(); } - if (this.bodyParams.avatarUrl) { - RocketChat.setUserAvatar(user, this.bodyParams.avatarUrl, '', 'url'); - } else { - const Busboy = Npm.require('busboy'); - const busboy = new Busboy({ headers: this.request.headers }); - - Meteor.wrapAsync((callback) => { - busboy.on('file', Meteor.bindEnvironment((fieldname, file, filename, encoding, mimetype) => { - if (fieldname !== 'image') { - return callback(new Meteor.Error('invalid-field')); - } - - const imageData = []; - file.on('data', Meteor.bindEnvironment((data) => { - imageData.push(data); - })); + Meteor.runAsUser(user._id, () => { + if (this.bodyParams.avatarUrl) { + RocketChat.setUserAvatar(user, this.bodyParams.avatarUrl, '', 'url'); + } else { + const Busboy = Npm.require('busboy'); + const busboy = new Busboy({ headers: this.request.headers }); + + Meteor.wrapAsync((callback) => { + busboy.on('file', Meteor.bindEnvironment((fieldname, file, filename, encoding, mimetype) => { + if (fieldname !== 'image') { + return callback(new Meteor.Error('invalid-field')); + } + + const imageData = []; + file.on('data', Meteor.bindEnvironment((data) => { + imageData.push(data); + })); + + file.on('end', Meteor.bindEnvironment(() => { + RocketChat.setUserAvatar(user, Buffer.concat(imageData), mimetype, 'rest'); + callback(); + })); - file.on('end', Meteor.bindEnvironment(() => { - RocketChat.setUserAvatar(user, Buffer.concat(imageData), mimetype, 'rest'); - callback(); })); - this.request.pipe(busboy); - })); - })(); - } + })(); + } + }); return RocketChat.API.v1.success(); } diff --git a/packages/rocketchat-authorization/server/startup.js b/packages/rocketchat-authorization/server/startup.js index 6aeb68290d7ff0bf083a4bf398314aee8e0c84c2..3fe7771125de4edc9d9be81a81f2c6d5111b1316 100644 --- a/packages/rocketchat-authorization/server/startup.js +++ b/packages/rocketchat-authorization/server/startup.js @@ -32,6 +32,7 @@ Meteor.startup(function() { { _id: 'edit-other-user-password', roles : ['admin'] }, { _id: 'edit-privileged-setting', roles : ['admin'] }, { _id: 'edit-room', roles : ['admin', 'owner', 'moderator'] }, + { _id: 'force-delete-message', roles : ['admin', 'owner'] }, { _id: 'join-without-join-code', roles : ['admin', 'bot'] }, { _id: 'manage-assets', roles : ['admin'] }, { _id: 'manage-emoji', roles : ['admin'] }, diff --git a/packages/rocketchat-channel-settings/client/views/channelSettings.js b/packages/rocketchat-channel-settings/client/views/channelSettings.js index 4479caa45e9966de7461ee754e711edc41060965..d24806ba20b2cbce01bbaa5d3f448785c69086ec 100644 --- a/packages/rocketchat-channel-settings/client/views/channelSettings.js +++ b/packages/rocketchat-channel-settings/client/views/channelSettings.js @@ -108,11 +108,17 @@ Template.channelSettings.events({ t.saveSetting(); } }, - 'click [data-edit]'(e, t) { + 'click [data-edit], click .button.edit'(e, t) { e.preventDefault(); - if ($(e.currentTarget).data('edit')) { - t.editing.set($(e.currentTarget).data('edit')); - return setTimeout((function() { + let input = $(e.currentTarget); + + if (input.hasClass('button')) { + input = $(e.currentTarget).siblings('.current-setting'); + } + + if (input.data('edit')) { + t.editing.set(input.data('edit')); + setTimeout((function() { return t.$('input.editing').focus().select(); }), 100); } diff --git a/packages/rocketchat-chatops/client/views/stylesheets/chatops.css b/packages/rocketchat-chatops/client/views/stylesheets/chatops.css index 5c439017ffa2efd5c2cb55be672ba7d83d379438..de78b6409549ab957e59576853b7178345d45d06 100644 --- a/packages/rocketchat-chatops/client/views/stylesheets/chatops.css +++ b/packages/rocketchat-chatops/client/views/stylesheets/chatops.css @@ -1,9 +1,9 @@ .map-container { - width: 670px; - max-width: 100%; - height: 500px; - padding: 5px, 5px, 5px 5px; + width: 670px; + max-width: 100%; + height: 500px; + padding: 5px; } .red { @@ -12,4 +12,4 @@ .green { color: green; -} \ No newline at end of file +} diff --git a/packages/rocketchat-colors/style.css b/packages/rocketchat-colors/style.css index 303346a873e6c912e0b519331d99cf47e4c36e3d..e1f3999f69b64595116896d63ffbc9891ccd7855 100644 --- a/packages/rocketchat-colors/style.css +++ b/packages/rocketchat-colors/style.css @@ -10,7 +10,7 @@ border-radius: 3px; margin-right: 3px; margin-left: 2px; - border: 1px solid rgba(0, 0, 0, .2); + border: 1px solid rgba(0, 0, 0, 0.2); position: relative; top: 2px; } diff --git a/packages/rocketchat-dolphin/login-button.css b/packages/rocketchat-dolphin/login-button.css index e75b5e8e743281d66ea31af16c0c430efe20c04b..836c193cdeb5119d46fd6b4ff9a1865677519377 100644 --- a/packages/rocketchat-dolphin/login-button.css +++ b/packages/rocketchat-dolphin/login-button.css @@ -4,10 +4,10 @@ height: 20px; background-image: url(data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNDMwIDQzMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxnIGlkPSJzaWduLWluIiB0cmFuc2Zvcm09Im1hdHJpeCgwLjUgMCAwIDAuNSAwIDApIj4NCjxwYXRoIGZpbGw9IiNmZmZmZmYiIGQ9Ik02NjAuNjcyIDUwMC4wMzJxMCAxNC41MDggLTEwLjYwMiAyNS4xMWwtMzAzLjU1MiAzMDMuNTUycS0xMC42MDIgMTAuNjAyIC0yNS4xMSAxMC42MDJ0LTI1LjExIC0xMC42MDIgLTEwLjYwMiAtMjUuMTF2LTE2MC43MDRoLTI0OS45ODRxLTE0LjUwOCAwIC0yNS4xMSAtMTAuNjAydC0xMC42MDIgLTI1LjExdi0yMTQuMjcycTAgLTE0LjUwOCAxMC42MDIgLTI1LjExdDI1LjExIC0xMC42MDJoMjQ5Ljk4NHYtMTYwLjcwNHEwIC0xNC41MDggMTAuNjAyIC0yNS4xMXQyNS4xMSAtMTAuNjAyIDI1LjExIDEwLjYwMmwzMDMuNTUyIDMwMy41NTJxMTAuNjAyIDEwLjYwMiAxMC42MDIgMjUuMTF6bTE5Ni40MTYgLTE5Ni40MTZ2MzkyLjgzMnEwIDY2LjQwMiAtNDcuMTUxIDExMy41NTN0LTExMy41NTMgNDcuMTUxaC0xNzguNTZxLTcuMjU0IDAgLTEyLjU1NSAtNS4zMDF0LTUuMzAxIC0xMi41NTVxMCAtMi4yMzIgLS41NTggLTExLjE2dC0uMjc5IC0xNC43ODcgMS42NzQgLTEzLjExMyA1LjU4IC0xMC44ODEgMTEuNDM5IC0zLjYyN2gxNzguNTZxMzYuODI4IDAgNjMuMDU0IC0yNi4yMjZ0MjYuMjI2IC02My4wNTR2LTM5Mi44MzJxMCAtMzYuODI4IC0yNi4yMjYgLTYzLjA1NHQtNjMuMDU0IC0yNi4yMjZoLTE3NC4wOTZ0LTYuNDE3IC0uNTU4IC02LjQxNyAtMS42NzQgLTQuNDY0IC0zLjA2OSAtMy45MDYgLTUuMDIyIC0xLjExNiAtNy41MzNxMCAtMi4yMzIgLS41NTggLTExLjE2dC0uMjc5IC0xNC43ODcgMS42NzQgLTEzLjExMyA1LjU4IC0xMC44ODEgMTEuNDM5IC0zLjYyN2gxNzguNTZxNjYuNDAyIDAgMTEzLjU1MyA0Ny4xNTF0NDcuMTUxIDExMy41NTN6Ii8+DQo8L2c+DQo8L3N2Zz4=); background-repeat: no-repeat; - vertical-align: middle; + vertical-align: middle; } .icon-dolphin ~ .icon-spin, .icon-dolphin ~ span { - vertical-align: middle; + vertical-align: middle; } diff --git a/packages/rocketchat-emoji-emojione/sprites.css b/packages/rocketchat-emoji-emojione/sprites.css index a10f82b425216ba53ed7bccc0f6d2d11d8ccb28a..f82fc3a4e47ca8d58f68dd0e95ef2185741c0885 100644 --- a/packages/rocketchat-emoji-emojione/sprites.css +++ b/packages/rocketchat-emoji-emojione/sprites.css @@ -1,5517 +1,7351 @@ .emojione { - image-rendering: -webkit-optimize-contrast; - image-rendering: optimizeQuality; - font-size: inherit; - height: 22px; - width: 22px; - position: relative; - display: inline-block; - margin: 0 .15em; - line-height: normal; - vertical-align: middle; - background-image: url("../../packages/emojione_emojione/assets/sprites/emojione.sprites.png"); - background-size: 4365.625% 4365.625%; - background-repeat: no-repeat; - text-indent: 100%; - white-space: nowrap; - overflow: hidden; } + image-rendering: -webkit-optimize-contrast; + image-rendering: optimizeQuality; + font-size: inherit; + height: 22px; + width: 22px; + position: relative; + display: inline-block; + margin: 0 0.15em; + line-height: normal; + vertical-align: middle; + background-image: url("../../packages/emojione_emojione/assets/sprites/emojione.sprites.png"); + background-size: 4365.625% 4365.625%; + background-repeat: no-repeat; + text-indent: 100%; + white-space: nowrap; + overflow: hidden; +} .emojione.big { - height: 44px !important; - width: 44px !important; } + height: 44px !important; + width: 44px !important; +} .emojione-0023-20e3 { - background-position: 2.38095% 0%; } + background-position: 2.38095% 0%; +} .emojione-0023 { - background-position: 50% 66.66667%; } + background-position: 50% 66.66667%; +} .emojione-002a-20e3 { - background-position: 0% 2.38095%; } + background-position: 0% 2.38095%; +} .emojione-002a { - background-position: 2.38095% 2.38095%; } + background-position: 2.38095% 2.38095%; +} .emojione-0030-20e3 { - background-position: 4.7619% 0%; } + background-position: 4.7619% 0%; +} .emojione-0030 { - background-position: 4.7619% 2.38095%; } + background-position: 4.7619% 2.38095%; +} .emojione-0031-20e3 { - background-position: 0% 4.7619%; } + background-position: 0% 4.7619%; +} .emojione-0031 { - background-position: 2.38095% 4.7619%; } + background-position: 2.38095% 4.7619%; +} .emojione-0032-20e3 { - background-position: 4.7619% 4.7619%; } + background-position: 4.7619% 4.7619%; +} .emojione-0032 { - background-position: 7.14286% 0%; } + background-position: 7.14286% 0%; +} .emojione-0033-20e3 { - background-position: 7.14286% 2.38095%; } + background-position: 7.14286% 2.38095%; +} .emojione-0033 { - background-position: 7.14286% 4.7619%; } + background-position: 7.14286% 4.7619%; +} .emojione-0034-20e3 { - background-position: 0% 7.14286%; } + background-position: 0% 7.14286%; +} .emojione-0034 { - background-position: 2.38095% 7.14286%; } + background-position: 2.38095% 7.14286%; +} .emojione-0035-20e3 { - background-position: 4.7619% 7.14286%; } + background-position: 4.7619% 7.14286%; +} .emojione-0035 { - background-position: 7.14286% 7.14286%; } + background-position: 7.14286% 7.14286%; +} .emojione-0036-20e3 { - background-position: 9.52381% 0%; } + background-position: 9.52381% 0%; +} .emojione-0036 { - background-position: 9.52381% 2.38095%; } + background-position: 9.52381% 2.38095%; +} .emojione-0037-20e3 { - background-position: 9.52381% 4.7619%; } + background-position: 9.52381% 4.7619%; +} .emojione-0037 { - background-position: 9.52381% 7.14286%; } + background-position: 9.52381% 7.14286%; +} .emojione-0038-20e3 { - background-position: 0% 9.52381%; } + background-position: 0% 9.52381%; +} .emojione-0038 { - background-position: 2.38095% 9.52381%; } + background-position: 2.38095% 9.52381%; +} .emojione-0039-20e3 { - background-position: 4.7619% 9.52381%; } + background-position: 4.7619% 9.52381%; +} .emojione-0039 { - background-position: 7.14286% 9.52381%; } + background-position: 7.14286% 9.52381%; +} .emojione-00a9 { - background-position: 9.52381% 9.52381%; } + background-position: 9.52381% 9.52381%; +} .emojione-00ae { - background-position: 11.90476% 0%; } + background-position: 11.90476% 0%; +} .emojione-1f004 { - background-position: 11.90476% 2.38095%; } + background-position: 11.90476% 2.38095%; +} .emojione-1f0cf { - background-position: 11.90476% 4.7619%; } + background-position: 11.90476% 4.7619%; +} .emojione-1f170 { - background-position: 11.90476% 7.14286%; } + background-position: 11.90476% 7.14286%; +} .emojione-1f171 { - background-position: 11.90476% 9.52381%; } + background-position: 11.90476% 9.52381%; +} .emojione-1f17e { - background-position: 0% 11.90476%; } + background-position: 0% 11.90476%; +} .emojione-1f17f { - background-position: 2.38095% 11.90476%; } + background-position: 2.38095% 11.90476%; +} .emojione-1f18e { - background-position: 4.7619% 11.90476%; } + background-position: 4.7619% 11.90476%; +} .emojione-1f191 { - background-position: 7.14286% 11.90476%; } + background-position: 7.14286% 11.90476%; +} .emojione-1f192 { - background-position: 9.52381% 11.90476%; } + background-position: 9.52381% 11.90476%; +} .emojione-1f193 { - background-position: 11.90476% 11.90476%; } + background-position: 11.90476% 11.90476%; +} .emojione-1f194 { - background-position: 14.28571% 0%; } + background-position: 14.28571% 0%; +} .emojione-1f195 { - background-position: 14.28571% 2.38095%; } + background-position: 14.28571% 2.38095%; +} .emojione-1f196 { - background-position: 14.28571% 4.7619%; } + background-position: 14.28571% 4.7619%; +} .emojione-1f197 { - background-position: 14.28571% 7.14286%; } + background-position: 14.28571% 7.14286%; +} .emojione-1f198 { - background-position: 14.28571% 9.52381%; } + background-position: 14.28571% 9.52381%; +} .emojione-1f199 { - background-position: 14.28571% 11.90476%; } + background-position: 14.28571% 11.90476%; +} .emojione-1f19a { - background-position: 0% 14.28571%; } + background-position: 0% 14.28571%; +} .emojione-1f1e6-1f1e8 { - background-position: 2.38095% 14.28571%; } + background-position: 2.38095% 14.28571%; +} .emojione-1f1e6-1f1e9 { - background-position: 4.7619% 14.28571%; } + background-position: 4.7619% 14.28571%; +} .emojione-1f1e6-1f1ea { - background-position: 7.14286% 14.28571%; } + background-position: 7.14286% 14.28571%; +} .emojione-1f1e6-1f1eb { - background-position: 9.52381% 14.28571%; } + background-position: 9.52381% 14.28571%; +} .emojione-1f1e6-1f1ec { - background-position: 11.90476% 14.28571%; } + background-position: 11.90476% 14.28571%; +} .emojione-1f1e6-1f1ee { - background-position: 14.28571% 14.28571%; } + background-position: 14.28571% 14.28571%; +} .emojione-1f1e6-1f1f1 { - background-position: 16.66667% 0%; } + background-position: 16.66667% 0%; +} .emojione-1f1e6-1f1f2 { - background-position: 16.66667% 2.38095%; } + background-position: 16.66667% 2.38095%; +} .emojione-1f1e6-1f1f4 { - background-position: 16.66667% 4.7619%; } + background-position: 16.66667% 4.7619%; +} .emojione-1f1e6-1f1f6 { - background-position: 16.66667% 7.14286%; } + background-position: 16.66667% 7.14286%; +} .emojione-1f1e6-1f1f7 { - background-position: 16.66667% 9.52381%; } + background-position: 16.66667% 9.52381%; +} .emojione-1f1e6-1f1f8 { - background-position: 16.66667% 11.90476%; } + background-position: 16.66667% 11.90476%; +} .emojione-1f1e6-1f1f9 { - background-position: 16.66667% 14.28571%; } + background-position: 16.66667% 14.28571%; +} .emojione-1f1e6-1f1fa { - background-position: 0% 16.66667%; } + background-position: 0% 16.66667%; +} .emojione-1f1e6-1f1fc { - background-position: 2.38095% 16.66667%; } + background-position: 2.38095% 16.66667%; +} .emojione-1f1e6-1f1fd { - background-position: 4.7619% 16.66667%; } + background-position: 4.7619% 16.66667%; +} .emojione-1f1e6-1f1ff { - background-position: 7.14286% 16.66667%; } + background-position: 7.14286% 16.66667%; +} .emojione-1f1e6 { - background-position: 9.52381% 16.66667%; } + background-position: 9.52381% 16.66667%; +} .emojione-1f1e7-1f1e6 { - background-position: 11.90476% 16.66667%; } + background-position: 11.90476% 16.66667%; +} .emojione-1f1e7-1f1e7 { - background-position: 14.28571% 16.66667%; } + background-position: 14.28571% 16.66667%; +} .emojione-1f1e7-1f1e9 { - background-position: 16.66667% 16.66667%; } + background-position: 16.66667% 16.66667%; +} .emojione-1f1e7-1f1ea { - background-position: 19.04762% 0%; } + background-position: 19.04762% 0%; +} .emojione-1f1e7-1f1eb { - background-position: 19.04762% 2.38095%; } + background-position: 19.04762% 2.38095%; +} .emojione-1f1e7-1f1ec { - background-position: 19.04762% 4.7619%; } + background-position: 19.04762% 4.7619%; +} .emojione-1f1e7-1f1ed { - background-position: 19.04762% 7.14286%; } + background-position: 19.04762% 7.14286%; +} .emojione-1f1e7-1f1ee { - background-position: 19.04762% 9.52381%; } + background-position: 19.04762% 9.52381%; +} .emojione-1f1e7-1f1ef { - background-position: 19.04762% 11.90476%; } + background-position: 19.04762% 11.90476%; +} .emojione-1f1e7-1f1f1 { - background-position: 19.04762% 14.28571%; } + background-position: 19.04762% 14.28571%; +} .emojione-1f1e7-1f1f2 { - background-position: 19.04762% 16.66667%; } + background-position: 19.04762% 16.66667%; +} .emojione-1f1e7-1f1f3 { - background-position: 0% 19.04762%; } + background-position: 0% 19.04762%; +} .emojione-1f1e7-1f1f4 { - background-position: 2.38095% 19.04762%; } + background-position: 2.38095% 19.04762%; +} .emojione-1f1e7-1f1f6 { - background-position: 4.7619% 19.04762%; } + background-position: 4.7619% 19.04762%; +} .emojione-1f1e7-1f1f7 { - background-position: 7.14286% 19.04762%; } + background-position: 7.14286% 19.04762%; +} .emojione-1f1e7-1f1f8 { - background-position: 9.52381% 19.04762%; } + background-position: 9.52381% 19.04762%; +} .emojione-1f1e7-1f1f9 { - background-position: 11.90476% 19.04762%; } + background-position: 11.90476% 19.04762%; +} .emojione-1f1e7-1f1fb { - background-position: 14.28571% 19.04762%; } + background-position: 14.28571% 19.04762%; +} .emojione-1f1e7-1f1fc { - background-position: 16.66667% 19.04762%; } + background-position: 16.66667% 19.04762%; +} .emojione-1f1e7-1f1fe { - background-position: 19.04762% 19.04762%; } + background-position: 19.04762% 19.04762%; +} .emojione-1f1e7-1f1ff { - background-position: 21.42857% 0%; } + background-position: 21.42857% 0%; +} .emojione-1f1e7 { - background-position: 21.42857% 2.38095%; } + background-position: 21.42857% 2.38095%; +} .emojione-1f1e8-1f1e6 { - background-position: 21.42857% 4.7619%; } + background-position: 21.42857% 4.7619%; +} .emojione-1f1e8-1f1e8 { - background-position: 21.42857% 7.14286%; } + background-position: 21.42857% 7.14286%; +} .emojione-1f1e8-1f1e9 { - background-position: 21.42857% 9.52381%; } + background-position: 21.42857% 9.52381%; +} .emojione-1f1e8-1f1eb { - background-position: 21.42857% 11.90476%; } + background-position: 21.42857% 11.90476%; +} .emojione-1f1e8-1f1ec { - background-position: 21.42857% 14.28571%; } + background-position: 21.42857% 14.28571%; +} .emojione-1f1e8-1f1ed { - background-position: 21.42857% 16.66667%; } + background-position: 21.42857% 16.66667%; +} .emojione-1f1e8-1f1ee { - background-position: 21.42857% 19.04762%; } + background-position: 21.42857% 19.04762%; +} .emojione-1f1e8-1f1f0 { - background-position: 0% 21.42857%; } + background-position: 0% 21.42857%; +} .emojione-1f1e8-1f1f1 { - background-position: 2.38095% 21.42857%; } + background-position: 2.38095% 21.42857%; +} .emojione-1f1e8-1f1f2 { - background-position: 4.7619% 21.42857%; } + background-position: 4.7619% 21.42857%; +} .emojione-1f1e8-1f1f3 { - background-position: 7.14286% 21.42857%; } + background-position: 7.14286% 21.42857%; +} .emojione-1f1e8-1f1f4 { - background-position: 9.52381% 21.42857%; } + background-position: 9.52381% 21.42857%; +} .emojione-1f1e8-1f1f5 { - background-position: 11.90476% 21.42857%; } + background-position: 11.90476% 21.42857%; +} .emojione-1f1e8-1f1f7 { - background-position: 14.28571% 21.42857%; } + background-position: 14.28571% 21.42857%; +} .emojione-1f1e8-1f1fa { - background-position: 16.66667% 21.42857%; } + background-position: 16.66667% 21.42857%; +} .emojione-1f1e8-1f1fb { - background-position: 19.04762% 21.42857%; } + background-position: 19.04762% 21.42857%; +} .emojione-1f1e8-1f1fc { - background-position: 21.42857% 21.42857%; } + background-position: 21.42857% 21.42857%; +} .emojione-1f1e8-1f1fd { - background-position: 23.80952% 0%; } + background-position: 23.80952% 0%; +} .emojione-1f1e8-1f1fe { - background-position: 23.80952% 2.38095%; } + background-position: 23.80952% 2.38095%; +} .emojione-1f1e8-1f1ff { - background-position: 23.80952% 4.7619%; } + background-position: 23.80952% 4.7619%; +} .emojione-1f1e8 { - background-position: 23.80952% 7.14286%; } + background-position: 23.80952% 7.14286%; +} .emojione-1f1e9-1f1ea { - background-position: 23.80952% 9.52381%; } + background-position: 23.80952% 9.52381%; +} .emojione-1f1e9-1f1ec { - background-position: 23.80952% 11.90476%; } + background-position: 23.80952% 11.90476%; +} .emojione-1f1e9-1f1ef { - background-position: 23.80952% 14.28571%; } + background-position: 23.80952% 14.28571%; +} .emojione-1f1e9-1f1f0 { - background-position: 23.80952% 16.66667%; } + background-position: 23.80952% 16.66667%; +} .emojione-1f1e9-1f1f2 { - background-position: 23.80952% 19.04762%; } + background-position: 23.80952% 19.04762%; +} .emojione-1f1e9-1f1f4 { - background-position: 23.80952% 21.42857%; } + background-position: 23.80952% 21.42857%; +} .emojione-1f1e9-1f1ff { - background-position: 0% 23.80952%; } + background-position: 0% 23.80952%; +} .emojione-1f1e9 { - background-position: 2.38095% 23.80952%; } + background-position: 2.38095% 23.80952%; +} .emojione-1f1ea-1f1e6 { - background-position: 4.7619% 23.80952%; } + background-position: 4.7619% 23.80952%; +} .emojione-1f1ea-1f1e8 { - background-position: 7.14286% 23.80952%; } + background-position: 7.14286% 23.80952%; +} .emojione-1f1ea-1f1ea { - background-position: 9.52381% 23.80952%; } + background-position: 9.52381% 23.80952%; +} .emojione-1f1ea-1f1ec { - background-position: 11.90476% 23.80952%; } + background-position: 11.90476% 23.80952%; +} .emojione-1f1ea-1f1ed { - background-position: 14.28571% 23.80952%; } + background-position: 14.28571% 23.80952%; +} .emojione-1f1ea-1f1f7 { - background-position: 16.66667% 23.80952%; } + background-position: 16.66667% 23.80952%; +} .emojione-1f1ea-1f1f8 { - background-position: 19.04762% 23.80952%; } + background-position: 19.04762% 23.80952%; +} .emojione-1f1ea-1f1f9 { - background-position: 21.42857% 23.80952%; } + background-position: 21.42857% 23.80952%; +} .emojione-1f1ea-1f1fa { - background-position: 23.80952% 23.80952%; } + background-position: 23.80952% 23.80952%; +} .emojione-1f1ea { - background-position: 26.19048% 0%; } + background-position: 26.19048% 0%; +} .emojione-1f1eb-1f1ee { - background-position: 26.19048% 2.38095%; } + background-position: 26.19048% 2.38095%; +} .emojione-1f1eb-1f1ef { - background-position: 26.19048% 4.7619%; } + background-position: 26.19048% 4.7619%; +} .emojione-1f1eb-1f1f0 { - background-position: 26.19048% 7.14286%; } + background-position: 26.19048% 7.14286%; +} .emojione-1f1eb-1f1f2 { - background-position: 26.19048% 9.52381%; } + background-position: 26.19048% 9.52381%; +} .emojione-1f1eb-1f1f4 { - background-position: 26.19048% 11.90476%; } + background-position: 26.19048% 11.90476%; +} .emojione-1f1eb-1f1f7 { - background-position: 26.19048% 14.28571%; } + background-position: 26.19048% 14.28571%; +} .emojione-1f1eb { - background-position: 26.19048% 16.66667%; } + background-position: 26.19048% 16.66667%; +} .emojione-1f1ec-1f1e6 { - background-position: 26.19048% 19.04762%; } + background-position: 26.19048% 19.04762%; +} .emojione-1f1ec-1f1e7 { - background-position: 26.19048% 21.42857%; } + background-position: 26.19048% 21.42857%; +} .emojione-1f1ec-1f1e9 { - background-position: 26.19048% 23.80952%; } + background-position: 26.19048% 23.80952%; +} .emojione-1f1ec-1f1ea { - background-position: 0% 26.19048%; } + background-position: 0% 26.19048%; +} .emojione-1f1ec-1f1eb { - background-position: 2.38095% 26.19048%; } + background-position: 2.38095% 26.19048%; +} .emojione-1f1ec-1f1ec { - background-position: 4.7619% 26.19048%; } + background-position: 4.7619% 26.19048%; +} .emojione-1f1ec-1f1ed { - background-position: 7.14286% 26.19048%; } + background-position: 7.14286% 26.19048%; +} .emojione-1f1ec-1f1ee { - background-position: 9.52381% 26.19048%; } + background-position: 9.52381% 26.19048%; +} .emojione-1f1ec-1f1f1 { - background-position: 11.90476% 26.19048%; } + background-position: 11.90476% 26.19048%; +} .emojione-1f1ec-1f1f2 { - background-position: 14.28571% 26.19048%; } + background-position: 14.28571% 26.19048%; +} .emojione-1f1ec-1f1f3 { - background-position: 16.66667% 26.19048%; } + background-position: 16.66667% 26.19048%; +} .emojione-1f1ec-1f1f5 { - background-position: 19.04762% 26.19048%; } + background-position: 19.04762% 26.19048%; +} .emojione-1f1ec-1f1f6 { - background-position: 21.42857% 26.19048%; } + background-position: 21.42857% 26.19048%; +} .emojione-1f1ec-1f1f7 { - background-position: 23.80952% 26.19048%; } + background-position: 23.80952% 26.19048%; +} .emojione-1f1ec-1f1f8 { - background-position: 26.19048% 26.19048%; } + background-position: 26.19048% 26.19048%; +} .emojione-1f1ec-1f1f9 { - background-position: 28.57143% 0%; } + background-position: 28.57143% 0%; +} .emojione-1f1ec-1f1fa { - background-position: 28.57143% 2.38095%; } + background-position: 28.57143% 2.38095%; +} .emojione-1f1ec-1f1fc { - background-position: 28.57143% 4.7619%; } + background-position: 28.57143% 4.7619%; +} .emojione-1f1ec-1f1fe { - background-position: 28.57143% 7.14286%; } + background-position: 28.57143% 7.14286%; +} .emojione-1f1ec { - background-position: 28.57143% 9.52381%; } + background-position: 28.57143% 9.52381%; +} .emojione-1f1ed-1f1f0 { - background-position: 28.57143% 11.90476%; } + background-position: 28.57143% 11.90476%; +} .emojione-1f1ed-1f1f2 { - background-position: 28.57143% 14.28571%; } + background-position: 28.57143% 14.28571%; +} .emojione-1f1ed-1f1f3 { - background-position: 28.57143% 16.66667%; } + background-position: 28.57143% 16.66667%; +} .emojione-1f1ed-1f1f7 { - background-position: 28.57143% 19.04762%; } + background-position: 28.57143% 19.04762%; +} .emojione-1f1ed-1f1f9 { - background-position: 28.57143% 21.42857%; } + background-position: 28.57143% 21.42857%; +} .emojione-1f1ed-1f1fa { - background-position: 28.57143% 23.80952%; } + background-position: 28.57143% 23.80952%; +} .emojione-1f1ed { - background-position: 28.57143% 26.19048%; } + background-position: 28.57143% 26.19048%; +} .emojione-1f1ee-1f1e8 { - background-position: 0% 28.57143%; } + background-position: 0% 28.57143%; +} .emojione-1f1ee-1f1e9 { - background-position: 2.38095% 28.57143%; } + background-position: 2.38095% 28.57143%; +} .emojione-1f1ee-1f1ea { - background-position: 4.7619% 28.57143%; } + background-position: 4.7619% 28.57143%; +} .emojione-1f1ee-1f1f1 { - background-position: 7.14286% 28.57143%; } + background-position: 7.14286% 28.57143%; +} .emojione-1f1ee-1f1f2 { - background-position: 9.52381% 28.57143%; } + background-position: 9.52381% 28.57143%; +} .emojione-1f1ee-1f1f3 { - background-position: 11.90476% 28.57143%; } + background-position: 11.90476% 28.57143%; +} .emojione-1f1ee-1f1f4 { - background-position: 14.28571% 28.57143%; } + background-position: 14.28571% 28.57143%; +} .emojione-1f1ee-1f1f6 { - background-position: 16.66667% 28.57143%; } + background-position: 16.66667% 28.57143%; +} .emojione-1f1ee-1f1f7 { - background-position: 19.04762% 28.57143%; } + background-position: 19.04762% 28.57143%; +} .emojione-1f1ee-1f1f8 { - background-position: 21.42857% 28.57143%; } + background-position: 21.42857% 28.57143%; +} .emojione-1f1ee-1f1f9 { - background-position: 23.80952% 28.57143%; } + background-position: 23.80952% 28.57143%; +} .emojione-1f1ee { - background-position: 26.19048% 28.57143%; } + background-position: 26.19048% 28.57143%; +} .emojione-1f1ef-1f1ea { - background-position: 28.57143% 28.57143%; } + background-position: 28.57143% 28.57143%; +} .emojione-1f1ef-1f1f2 { - background-position: 30.95238% 0%; } + background-position: 30.95238% 0%; +} .emojione-1f1ef-1f1f4 { - background-position: 30.95238% 2.38095%; } + background-position: 30.95238% 2.38095%; +} .emojione-1f1ef-1f1f5 { - background-position: 30.95238% 4.7619%; } + background-position: 30.95238% 4.7619%; +} .emojione-1f1ef { - background-position: 30.95238% 7.14286%; } + background-position: 30.95238% 7.14286%; +} .emojione-1f1f0-1f1ea { - background-position: 30.95238% 9.52381%; } + background-position: 30.95238% 9.52381%; +} .emojione-1f1f0-1f1ec { - background-position: 30.95238% 11.90476%; } + background-position: 30.95238% 11.90476%; +} .emojione-1f1f0-1f1ed { - background-position: 30.95238% 14.28571%; } + background-position: 30.95238% 14.28571%; +} .emojione-1f1f0-1f1ee { - background-position: 30.95238% 16.66667%; } + background-position: 30.95238% 16.66667%; +} .emojione-1f1f0-1f1f2 { - background-position: 30.95238% 19.04762%; } + background-position: 30.95238% 19.04762%; +} .emojione-1f1f0-1f1f3 { - background-position: 30.95238% 21.42857%; } + background-position: 30.95238% 21.42857%; +} .emojione-1f1f0-1f1f5 { - background-position: 30.95238% 23.80952%; } + background-position: 30.95238% 23.80952%; +} .emojione-1f1f0-1f1f7 { - background-position: 30.95238% 26.19048%; } + background-position: 30.95238% 26.19048%; +} .emojione-1f1f0-1f1fc { - background-position: 30.95238% 28.57143%; } + background-position: 30.95238% 28.57143%; +} .emojione-1f1f0-1f1fe { - background-position: 0% 30.95238%; } + background-position: 0% 30.95238%; +} .emojione-1f1f0-1f1ff { - background-position: 2.38095% 30.95238%; } + background-position: 2.38095% 30.95238%; +} .emojione-1f1f0 { - background-position: 4.7619% 30.95238%; } + background-position: 4.7619% 30.95238%; +} .emojione-1f1f1-1f1e6 { - background-position: 7.14286% 30.95238%; } + background-position: 7.14286% 30.95238%; +} .emojione-1f1f1-1f1e7 { - background-position: 9.52381% 30.95238%; } + background-position: 9.52381% 30.95238%; +} .emojione-1f1f1-1f1e8 { - background-position: 11.90476% 30.95238%; } + background-position: 11.90476% 30.95238%; +} .emojione-1f1f1-1f1ee { - background-position: 14.28571% 30.95238%; } + background-position: 14.28571% 30.95238%; +} .emojione-1f1f1-1f1f0 { - background-position: 16.66667% 30.95238%; } + background-position: 16.66667% 30.95238%; +} .emojione-1f1f1-1f1f7 { - background-position: 19.04762% 30.95238%; } + background-position: 19.04762% 30.95238%; +} .emojione-1f1f1-1f1f8 { - background-position: 21.42857% 30.95238%; } + background-position: 21.42857% 30.95238%; +} .emojione-1f1f1-1f1f9 { - background-position: 23.80952% 30.95238%; } + background-position: 23.80952% 30.95238%; +} .emojione-1f1f1-1f1fa { - background-position: 26.19048% 30.95238%; } + background-position: 26.19048% 30.95238%; +} .emojione-1f1f1-1f1fb { - background-position: 28.57143% 30.95238%; } + background-position: 28.57143% 30.95238%; +} .emojione-1f1f1-1f1fe { - background-position: 30.95238% 30.95238%; } + background-position: 30.95238% 30.95238%; +} .emojione-1f1f1 { - background-position: 33.33333% 0%; } + background-position: 33.33333% 0%; +} .emojione-1f1f2-1f1e6 { - background-position: 33.33333% 2.38095%; } + background-position: 33.33333% 2.38095%; +} .emojione-1f1f2-1f1e8 { - background-position: 33.33333% 4.7619%; } + background-position: 33.33333% 4.7619%; +} .emojione-1f1f2-1f1e9 { - background-position: 33.33333% 7.14286%; } + background-position: 33.33333% 7.14286%; +} .emojione-1f1f2-1f1ea { - background-position: 33.33333% 9.52381%; } + background-position: 33.33333% 9.52381%; +} .emojione-1f1f2-1f1eb { - background-position: 33.33333% 11.90476%; } + background-position: 33.33333% 11.90476%; +} .emojione-1f1f2-1f1ec { - background-position: 33.33333% 14.28571%; } + background-position: 33.33333% 14.28571%; +} .emojione-1f1f2-1f1ed { - background-position: 33.33333% 16.66667%; } + background-position: 33.33333% 16.66667%; +} .emojione-1f1f2-1f1f0 { - background-position: 33.33333% 19.04762%; } + background-position: 33.33333% 19.04762%; +} .emojione-1f1f2-1f1f1 { - background-position: 33.33333% 21.42857%; } + background-position: 33.33333% 21.42857%; +} .emojione-1f1f2-1f1f2 { - background-position: 33.33333% 23.80952%; } + background-position: 33.33333% 23.80952%; +} .emojione-1f1f2-1f1f3 { - background-position: 33.33333% 26.19048%; } + background-position: 33.33333% 26.19048%; +} .emojione-1f1f2-1f1f4 { - background-position: 33.33333% 28.57143%; } + background-position: 33.33333% 28.57143%; +} .emojione-1f1f2-1f1f5 { - background-position: 33.33333% 30.95238%; } + background-position: 33.33333% 30.95238%; +} .emojione-1f1f2-1f1f6 { - background-position: 0% 33.33333%; } + background-position: 0% 33.33333%; +} .emojione-1f1f2-1f1f7 { - background-position: 2.38095% 33.33333%; } + background-position: 2.38095% 33.33333%; +} .emojione-1f1f2-1f1f8 { - background-position: 4.7619% 33.33333%; } + background-position: 4.7619% 33.33333%; +} .emojione-1f1f2-1f1f9 { - background-position: 7.14286% 33.33333%; } + background-position: 7.14286% 33.33333%; +} .emojione-1f1f2-1f1fa { - background-position: 9.52381% 33.33333%; } + background-position: 9.52381% 33.33333%; +} .emojione-1f1f2-1f1fb { - background-position: 11.90476% 33.33333%; } + background-position: 11.90476% 33.33333%; +} .emojione-1f1f2-1f1fc { - background-position: 14.28571% 33.33333%; } + background-position: 14.28571% 33.33333%; +} .emojione-1f1f2-1f1fd { - background-position: 16.66667% 33.33333%; } + background-position: 16.66667% 33.33333%; +} .emojione-1f1f2-1f1fe { - background-position: 19.04762% 33.33333%; } + background-position: 19.04762% 33.33333%; +} .emojione-1f1f2-1f1ff { - background-position: 21.42857% 33.33333%; } + background-position: 21.42857% 33.33333%; +} .emojione-1f1f2 { - background-position: 23.80952% 33.33333%; } + background-position: 23.80952% 33.33333%; +} .emojione-1f1f3-1f1e6 { - background-position: 26.19048% 33.33333%; } + background-position: 26.19048% 33.33333%; +} .emojione-1f1f3-1f1e8 { - background-position: 28.57143% 33.33333%; } + background-position: 28.57143% 33.33333%; +} .emojione-1f1f3-1f1ea { - background-position: 30.95238% 33.33333%; } + background-position: 30.95238% 33.33333%; +} .emojione-1f1f3-1f1eb { - background-position: 33.33333% 33.33333%; } + background-position: 33.33333% 33.33333%; +} .emojione-1f1f3-1f1ec { - background-position: 35.71429% 0%; } + background-position: 35.71429% 0%; +} .emojione-1f1f3-1f1ee { - background-position: 35.71429% 2.38095%; } + background-position: 35.71429% 2.38095%; +} .emojione-1f1f3-1f1f1 { - background-position: 35.71429% 4.7619%; } + background-position: 35.71429% 4.7619%; +} .emojione-1f1f3-1f1f4 { - background-position: 35.71429% 7.14286%; } + background-position: 35.71429% 7.14286%; +} .emojione-1f1f3-1f1f5 { - background-position: 35.71429% 9.52381%; } + background-position: 35.71429% 9.52381%; +} .emojione-1f1f3-1f1f7 { - background-position: 35.71429% 11.90476%; } + background-position: 35.71429% 11.90476%; +} .emojione-1f1f3-1f1fa { - background-position: 35.71429% 14.28571%; } + background-position: 35.71429% 14.28571%; +} .emojione-1f1f3-1f1ff { - background-position: 35.71429% 16.66667%; } + background-position: 35.71429% 16.66667%; +} .emojione-1f1f3 { - background-position: 35.71429% 19.04762%; } + background-position: 35.71429% 19.04762%; +} .emojione-1f1f4-1f1f2 { - background-position: 35.71429% 21.42857%; } + background-position: 35.71429% 21.42857%; +} .emojione-1f1f4 { - background-position: 35.71429% 23.80952%; } + background-position: 35.71429% 23.80952%; +} .emojione-1f1f5-1f1e6 { - background-position: 35.71429% 26.19048%; } + background-position: 35.71429% 26.19048%; +} .emojione-1f1f5-1f1ea { - background-position: 35.71429% 28.57143%; } + background-position: 35.71429% 28.57143%; +} .emojione-1f1f5-1f1eb { - background-position: 35.71429% 30.95238%; } + background-position: 35.71429% 30.95238%; +} .emojione-1f1f5-1f1ec { - background-position: 35.71429% 33.33333%; } + background-position: 35.71429% 33.33333%; +} .emojione-1f1f5-1f1ed { - background-position: 0% 35.71429%; } + background-position: 0% 35.71429%; +} .emojione-1f1f5-1f1f0 { - background-position: 2.38095% 35.71429%; } + background-position: 2.38095% 35.71429%; +} .emojione-1f1f5-1f1f1 { - background-position: 4.7619% 35.71429%; } + background-position: 4.7619% 35.71429%; +} .emojione-1f1f5-1f1f2 { - background-position: 7.14286% 35.71429%; } + background-position: 7.14286% 35.71429%; +} .emojione-1f1f5-1f1f3 { - background-position: 9.52381% 35.71429%; } + background-position: 9.52381% 35.71429%; +} .emojione-1f1f5-1f1f7 { - background-position: 11.90476% 35.71429%; } + background-position: 11.90476% 35.71429%; +} .emojione-1f1f5-1f1f8 { - background-position: 14.28571% 35.71429%; } + background-position: 14.28571% 35.71429%; +} .emojione-1f1f5-1f1f9 { - background-position: 16.66667% 35.71429%; } + background-position: 16.66667% 35.71429%; +} .emojione-1f1f5-1f1fc { - background-position: 19.04762% 35.71429%; } + background-position: 19.04762% 35.71429%; +} .emojione-1f1f5-1f1fe { - background-position: 21.42857% 35.71429%; } + background-position: 21.42857% 35.71429%; +} .emojione-1f1f5 { - background-position: 23.80952% 35.71429%; } + background-position: 23.80952% 35.71429%; +} .emojione-1f1f6-1f1e6 { - background-position: 26.19048% 35.71429%; } + background-position: 26.19048% 35.71429%; +} .emojione-1f1f6 { - background-position: 28.57143% 35.71429%; } + background-position: 28.57143% 35.71429%; +} .emojione-1f1f7-1f1ea { - background-position: 30.95238% 35.71429%; } + background-position: 30.95238% 35.71429%; +} .emojione-1f1f7-1f1f4 { - background-position: 33.33333% 35.71429%; } + background-position: 33.33333% 35.71429%; +} .emojione-1f1f7-1f1f8 { - background-position: 35.71429% 35.71429%; } + background-position: 35.71429% 35.71429%; +} .emojione-1f1f7-1f1fa { - background-position: 38.09524% 0%; } + background-position: 38.09524% 0%; +} .emojione-1f1f7-1f1fc { - background-position: 38.09524% 2.38095%; } + background-position: 38.09524% 2.38095%; +} .emojione-1f1f7 { - background-position: 38.09524% 4.7619%; } + background-position: 38.09524% 4.7619%; +} .emojione-1f1f8-1f1e6 { - background-position: 38.09524% 7.14286%; } + background-position: 38.09524% 7.14286%; +} .emojione-1f1f8-1f1e7 { - background-position: 38.09524% 9.52381%; } + background-position: 38.09524% 9.52381%; +} .emojione-1f1f8-1f1e8 { - background-position: 38.09524% 11.90476%; } + background-position: 38.09524% 11.90476%; +} .emojione-1f1f8-1f1e9 { - background-position: 38.09524% 14.28571%; } + background-position: 38.09524% 14.28571%; +} .emojione-1f1f8-1f1ea { - background-position: 38.09524% 16.66667%; } + background-position: 38.09524% 16.66667%; +} .emojione-1f1f8-1f1ec { - background-position: 38.09524% 19.04762%; } + background-position: 38.09524% 19.04762%; +} .emojione-1f1f8-1f1ed { - background-position: 38.09524% 21.42857%; } + background-position: 38.09524% 21.42857%; +} .emojione-1f1f8-1f1ee { - background-position: 38.09524% 23.80952%; } + background-position: 38.09524% 23.80952%; +} .emojione-1f1f8-1f1ef { - background-position: 38.09524% 26.19048%; } + background-position: 38.09524% 26.19048%; +} .emojione-1f1f8-1f1f0 { - background-position: 38.09524% 28.57143%; } + background-position: 38.09524% 28.57143%; +} .emojione-1f1f8-1f1f1 { - background-position: 38.09524% 30.95238%; } + background-position: 38.09524% 30.95238%; +} .emojione-1f1f8-1f1f2 { - background-position: 38.09524% 33.33333%; } + background-position: 38.09524% 33.33333%; +} .emojione-1f1f8-1f1f3 { - background-position: 38.09524% 35.71429%; } + background-position: 38.09524% 35.71429%; +} .emojione-1f1f8-1f1f4 { - background-position: 0% 38.09524%; } + background-position: 0% 38.09524%; +} .emojione-1f1f8-1f1f7 { - background-position: 2.38095% 38.09524%; } + background-position: 2.38095% 38.09524%; +} .emojione-1f1f8-1f1f8 { - background-position: 4.7619% 38.09524%; } + background-position: 4.7619% 38.09524%; +} .emojione-1f1f8-1f1f9 { - background-position: 7.14286% 38.09524%; } + background-position: 7.14286% 38.09524%; +} .emojione-1f1f8-1f1fb { - background-position: 9.52381% 38.09524%; } + background-position: 9.52381% 38.09524%; +} .emojione-1f1f8-1f1fd { - background-position: 11.90476% 38.09524%; } + background-position: 11.90476% 38.09524%; +} .emojione-1f1f8-1f1fe { - background-position: 14.28571% 38.09524%; } + background-position: 14.28571% 38.09524%; +} .emojione-1f1f8-1f1ff { - background-position: 16.66667% 38.09524%; } + background-position: 16.66667% 38.09524%; +} .emojione-1f1f8 { - background-position: 19.04762% 38.09524%; } + background-position: 19.04762% 38.09524%; +} .emojione-1f1f9-1f1e6 { - background-position: 21.42857% 38.09524%; } + background-position: 21.42857% 38.09524%; +} .emojione-1f1f9-1f1e8 { - background-position: 23.80952% 38.09524%; } + background-position: 23.80952% 38.09524%; +} .emojione-1f1f9-1f1e9 { - background-position: 26.19048% 38.09524%; } + background-position: 26.19048% 38.09524%; +} .emojione-1f1f9-1f1eb { - background-position: 28.57143% 38.09524%; } + background-position: 28.57143% 38.09524%; +} .emojione-1f1f9-1f1ec { - background-position: 30.95238% 38.09524%; } + background-position: 30.95238% 38.09524%; +} .emojione-1f1f9-1f1ed { - background-position: 33.33333% 38.09524%; } + background-position: 33.33333% 38.09524%; +} .emojione-1f1f9-1f1ef { - background-position: 35.71429% 38.09524%; } + background-position: 35.71429% 38.09524%; +} .emojione-1f1f9-1f1f0 { - background-position: 38.09524% 38.09524%; } + background-position: 38.09524% 38.09524%; +} .emojione-1f1f9-1f1f1 { - background-position: 40.47619% 0%; } + background-position: 40.47619% 0%; +} .emojione-1f1f9-1f1f2 { - background-position: 40.47619% 2.38095%; } + background-position: 40.47619% 2.38095%; +} .emojione-1f1f9-1f1f3 { - background-position: 40.47619% 4.7619%; } + background-position: 40.47619% 4.7619%; +} .emojione-1f1f9-1f1f4 { - background-position: 40.47619% 7.14286%; } + background-position: 40.47619% 7.14286%; +} .emojione-1f1f9-1f1f7 { - background-position: 40.47619% 9.52381%; } + background-position: 40.47619% 9.52381%; +} .emojione-1f1f9-1f1f9 { - background-position: 40.47619% 11.90476%; } + background-position: 40.47619% 11.90476%; +} .emojione-1f1f9-1f1fb { - background-position: 40.47619% 14.28571%; } + background-position: 40.47619% 14.28571%; +} .emojione-1f1f9-1f1fc { - background-position: 40.47619% 16.66667%; } + background-position: 40.47619% 16.66667%; +} .emojione-1f1f9-1f1ff { - background-position: 40.47619% 19.04762%; } + background-position: 40.47619% 19.04762%; +} .emojione-1f1f9 { - background-position: 40.47619% 21.42857%; } + background-position: 40.47619% 21.42857%; +} .emojione-1f1fa-1f1e6 { - background-position: 40.47619% 23.80952%; } + background-position: 40.47619% 23.80952%; +} .emojione-1f1fa-1f1ec { - background-position: 40.47619% 26.19048%; } + background-position: 40.47619% 26.19048%; +} .emojione-1f1fa-1f1f2 { - background-position: 40.47619% 28.57143%; } + background-position: 40.47619% 28.57143%; +} .emojione-1f1fa-1f1f8 { - background-position: 40.47619% 30.95238%; } + background-position: 40.47619% 30.95238%; +} .emojione-1f1fa-1f1fe { - background-position: 40.47619% 33.33333%; } + background-position: 40.47619% 33.33333%; +} .emojione-1f1fa-1f1ff { - background-position: 40.47619% 35.71429%; } + background-position: 40.47619% 35.71429%; +} .emojione-1f1fa { - background-position: 40.47619% 38.09524%; } + background-position: 40.47619% 38.09524%; +} .emojione-1f1fb-1f1e6 { - background-position: 0% 40.47619%; } + background-position: 0% 40.47619%; +} .emojione-1f1fb-1f1e8 { - background-position: 2.38095% 40.47619%; } + background-position: 2.38095% 40.47619%; +} .emojione-1f1fb-1f1ea { - background-position: 4.7619% 40.47619%; } + background-position: 4.7619% 40.47619%; +} .emojione-1f1fb-1f1ec { - background-position: 7.14286% 40.47619%; } + background-position: 7.14286% 40.47619%; +} .emojione-1f1fb-1f1ee { - background-position: 9.52381% 40.47619%; } + background-position: 9.52381% 40.47619%; +} .emojione-1f1fb-1f1f3 { - background-position: 11.90476% 40.47619%; } + background-position: 11.90476% 40.47619%; +} .emojione-1f1fb-1f1fa { - background-position: 14.28571% 40.47619%; } + background-position: 14.28571% 40.47619%; +} .emojione-1f1fb { - background-position: 16.66667% 40.47619%; } + background-position: 16.66667% 40.47619%; +} .emojione-1f1fc-1f1eb { - background-position: 19.04762% 40.47619%; } + background-position: 19.04762% 40.47619%; +} .emojione-1f1fc-1f1f8 { - background-position: 21.42857% 40.47619%; } + background-position: 21.42857% 40.47619%; +} .emojione-1f1fc { - background-position: 23.80952% 40.47619%; } + background-position: 23.80952% 40.47619%; +} .emojione-1f1fd-1f1f0 { - background-position: 26.19048% 40.47619%; } + background-position: 26.19048% 40.47619%; +} .emojione-1f1fd { - background-position: 28.57143% 40.47619%; } + background-position: 28.57143% 40.47619%; +} .emojione-1f1fe-1f1ea { - background-position: 30.95238% 40.47619%; } + background-position: 30.95238% 40.47619%; +} .emojione-1f1fe-1f1f9 { - background-position: 33.33333% 40.47619%; } + background-position: 33.33333% 40.47619%; +} .emojione-1f1fe { - background-position: 35.71429% 40.47619%; } + background-position: 35.71429% 40.47619%; +} .emojione-1f1ff-1f1e6 { - background-position: 38.09524% 40.47619%; } + background-position: 38.09524% 40.47619%; +} .emojione-1f1ff-1f1f2 { - background-position: 40.47619% 40.47619%; } + background-position: 40.47619% 40.47619%; +} .emojione-1f1ff-1f1fc { - background-position: 42.85714% 0%; } + background-position: 42.85714% 0%; +} .emojione-1f1ff { - background-position: 42.85714% 2.38095%; } + background-position: 42.85714% 2.38095%; +} .emojione-1f201 { - background-position: 42.85714% 4.7619%; } + background-position: 42.85714% 4.7619%; +} .emojione-1f202 { - background-position: 42.85714% 7.14286%; } + background-position: 42.85714% 7.14286%; +} .emojione-1f21a { - background-position: 42.85714% 9.52381%; } + background-position: 42.85714% 9.52381%; +} .emojione-1f22f { - background-position: 42.85714% 11.90476%; } + background-position: 42.85714% 11.90476%; +} .emojione-1f232 { - background-position: 42.85714% 14.28571%; } + background-position: 42.85714% 14.28571%; +} .emojione-1f233 { - background-position: 42.85714% 16.66667%; } + background-position: 42.85714% 16.66667%; +} .emojione-1f234 { - background-position: 42.85714% 19.04762%; } + background-position: 42.85714% 19.04762%; +} .emojione-1f235 { - background-position: 42.85714% 21.42857%; } + background-position: 42.85714% 21.42857%; +} .emojione-1f236 { - background-position: 42.85714% 23.80952%; } + background-position: 42.85714% 23.80952%; +} .emojione-1f237 { - background-position: 42.85714% 26.19048%; } + background-position: 42.85714% 26.19048%; +} .emojione-1f238 { - background-position: 42.85714% 28.57143%; } + background-position: 42.85714% 28.57143%; +} .emojione-1f239 { - background-position: 42.85714% 30.95238%; } + background-position: 42.85714% 30.95238%; +} .emojione-1f23a { - background-position: 42.85714% 33.33333%; } + background-position: 42.85714% 33.33333%; +} .emojione-1f250 { - background-position: 42.85714% 35.71429%; } + background-position: 42.85714% 35.71429%; +} .emojione-1f251 { - background-position: 42.85714% 38.09524%; } + background-position: 42.85714% 38.09524%; +} .emojione-1f300 { - background-position: 42.85714% 40.47619%; } + background-position: 42.85714% 40.47619%; +} .emojione-1f301 { - background-position: 0% 42.85714%; } + background-position: 0% 42.85714%; +} .emojione-1f302 { - background-position: 2.38095% 42.85714%; } + background-position: 2.38095% 42.85714%; +} .emojione-1f303 { - background-position: 4.7619% 42.85714%; } + background-position: 4.7619% 42.85714%; +} .emojione-1f304 { - background-position: 7.14286% 42.85714%; } + background-position: 7.14286% 42.85714%; +} .emojione-1f305 { - background-position: 9.52381% 42.85714%; } + background-position: 9.52381% 42.85714%; +} .emojione-1f306 { - background-position: 11.90476% 42.85714%; } + background-position: 11.90476% 42.85714%; +} .emojione-1f307 { - background-position: 14.28571% 42.85714%; } + background-position: 14.28571% 42.85714%; +} .emojione-1f308 { - background-position: 16.66667% 42.85714%; } + background-position: 16.66667% 42.85714%; +} .emojione-1f309 { - background-position: 19.04762% 42.85714%; } + background-position: 19.04762% 42.85714%; +} .emojione-1f30a { - background-position: 21.42857% 42.85714%; } + background-position: 21.42857% 42.85714%; +} .emojione-1f30b { - background-position: 23.80952% 42.85714%; } + background-position: 23.80952% 42.85714%; +} .emojione-1f30c { - background-position: 26.19048% 42.85714%; } + background-position: 26.19048% 42.85714%; +} .emojione-1f30d { - background-position: 28.57143% 42.85714%; } + background-position: 28.57143% 42.85714%; +} .emojione-1f30e { - background-position: 30.95238% 42.85714%; } + background-position: 30.95238% 42.85714%; +} .emojione-1f30f { - background-position: 33.33333% 42.85714%; } + background-position: 33.33333% 42.85714%; +} .emojione-1f310 { - background-position: 35.71429% 42.85714%; } + background-position: 35.71429% 42.85714%; +} .emojione-1f311 { - background-position: 38.09524% 42.85714%; } + background-position: 38.09524% 42.85714%; +} .emojione-1f312 { - background-position: 40.47619% 42.85714%; } + background-position: 40.47619% 42.85714%; +} .emojione-1f313 { - background-position: 42.85714% 42.85714%; } + background-position: 42.85714% 42.85714%; +} .emojione-1f314 { - background-position: 45.2381% 0%; } + background-position: 45.2381% 0%; +} .emojione-1f315 { - background-position: 45.2381% 2.38095%; } + background-position: 45.2381% 2.38095%; +} .emojione-1f316 { - background-position: 45.2381% 4.7619%; } + background-position: 45.2381% 4.7619%; +} .emojione-1f317 { - background-position: 45.2381% 7.14286%; } + background-position: 45.2381% 7.14286%; +} .emojione-1f318 { - background-position: 45.2381% 9.52381%; } + background-position: 45.2381% 9.52381%; +} .emojione-1f319 { - background-position: 45.2381% 11.90476%; } + background-position: 45.2381% 11.90476%; +} .emojione-1f31a { - background-position: 45.2381% 14.28571%; } + background-position: 45.2381% 14.28571%; +} .emojione-1f31b { - background-position: 45.2381% 16.66667%; } + background-position: 45.2381% 16.66667%; +} .emojione-1f31c { - background-position: 45.2381% 19.04762%; } + background-position: 45.2381% 19.04762%; +} .emojione-1f31d { - background-position: 45.2381% 21.42857%; } + background-position: 45.2381% 21.42857%; +} .emojione-1f31e { - background-position: 45.2381% 23.80952%; } + background-position: 45.2381% 23.80952%; +} .emojione-1f31f { - background-position: 45.2381% 26.19048%; } + background-position: 45.2381% 26.19048%; +} .emojione-1f320 { - background-position: 45.2381% 28.57143%; } + background-position: 45.2381% 28.57143%; +} .emojione-1f321 { - background-position: 45.2381% 30.95238%; } + background-position: 45.2381% 30.95238%; +} .emojione-1f324 { - background-position: 45.2381% 33.33333%; } + background-position: 45.2381% 33.33333%; +} .emojione-1f325 { - background-position: 45.2381% 35.71429%; } + background-position: 45.2381% 35.71429%; +} .emojione-1f326 { - background-position: 45.2381% 38.09524%; } + background-position: 45.2381% 38.09524%; +} .emojione-1f327 { - background-position: 45.2381% 40.47619%; } + background-position: 45.2381% 40.47619%; +} .emojione-1f328 { - background-position: 45.2381% 42.85714%; } + background-position: 45.2381% 42.85714%; +} .emojione-1f329 { - background-position: 0% 45.2381%; } + background-position: 0% 45.2381%; +} .emojione-1f32a { - background-position: 2.38095% 45.2381%; } + background-position: 2.38095% 45.2381%; +} .emojione-1f32b { - background-position: 4.7619% 45.2381%; } + background-position: 4.7619% 45.2381%; +} .emojione-1f32c { - background-position: 7.14286% 45.2381%; } + background-position: 7.14286% 45.2381%; +} .emojione-1f32d { - background-position: 9.52381% 45.2381%; } + background-position: 9.52381% 45.2381%; +} .emojione-1f32e { - background-position: 11.90476% 45.2381%; } + background-position: 11.90476% 45.2381%; +} .emojione-1f32f { - background-position: 14.28571% 45.2381%; } + background-position: 14.28571% 45.2381%; +} .emojione-1f330 { - background-position: 16.66667% 45.2381%; } + background-position: 16.66667% 45.2381%; +} .emojione-1f331 { - background-position: 19.04762% 45.2381%; } + background-position: 19.04762% 45.2381%; +} .emojione-1f332 { - background-position: 21.42857% 45.2381%; } + background-position: 21.42857% 45.2381%; +} .emojione-1f333 { - background-position: 23.80952% 45.2381%; } + background-position: 23.80952% 45.2381%; +} .emojione-1f334 { - background-position: 26.19048% 45.2381%; } + background-position: 26.19048% 45.2381%; +} .emojione-1f335 { - background-position: 28.57143% 45.2381%; } + background-position: 28.57143% 45.2381%; +} .emojione-1f336 { - background-position: 30.95238% 45.2381%; } + background-position: 30.95238% 45.2381%; +} .emojione-1f337 { - background-position: 33.33333% 45.2381%; } + background-position: 33.33333% 45.2381%; +} .emojione-1f338 { - background-position: 35.71429% 45.2381%; } + background-position: 35.71429% 45.2381%; +} .emojione-1f339 { - background-position: 38.09524% 45.2381%; } + background-position: 38.09524% 45.2381%; +} .emojione-1f33a { - background-position: 40.47619% 45.2381%; } + background-position: 40.47619% 45.2381%; +} .emojione-1f33b { - background-position: 42.85714% 45.2381%; } + background-position: 42.85714% 45.2381%; +} .emojione-1f33c { - background-position: 45.2381% 45.2381%; } + background-position: 45.2381% 45.2381%; +} .emojione-1f33d { - background-position: 47.61905% 0%; } + background-position: 47.61905% 0%; +} .emojione-1f33e { - background-position: 47.61905% 2.38095%; } + background-position: 47.61905% 2.38095%; +} .emojione-1f33f { - background-position: 47.61905% 4.7619%; } + background-position: 47.61905% 4.7619%; +} .emojione-1f340 { - background-position: 47.61905% 7.14286%; } + background-position: 47.61905% 7.14286%; +} .emojione-1f341 { - background-position: 47.61905% 9.52381%; } + background-position: 47.61905% 9.52381%; +} .emojione-1f342 { - background-position: 47.61905% 11.90476%; } + background-position: 47.61905% 11.90476%; +} .emojione-1f343 { - background-position: 47.61905% 14.28571%; } + background-position: 47.61905% 14.28571%; +} .emojione-1f344 { - background-position: 47.61905% 16.66667%; } + background-position: 47.61905% 16.66667%; +} .emojione-1f345 { - background-position: 47.61905% 19.04762%; } + background-position: 47.61905% 19.04762%; +} .emojione-1f346 { - background-position: 47.61905% 21.42857%; } + background-position: 47.61905% 21.42857%; +} .emojione-1f347 { - background-position: 47.61905% 23.80952%; } + background-position: 47.61905% 23.80952%; +} .emojione-1f348 { - background-position: 47.61905% 26.19048%; } + background-position: 47.61905% 26.19048%; +} .emojione-1f349 { - background-position: 47.61905% 28.57143%; } + background-position: 47.61905% 28.57143%; +} .emojione-1f34a { - background-position: 47.61905% 30.95238%; } + background-position: 47.61905% 30.95238%; +} .emojione-1f34b { - background-position: 47.61905% 33.33333%; } + background-position: 47.61905% 33.33333%; +} .emojione-1f34c { - background-position: 47.61905% 35.71429%; } + background-position: 47.61905% 35.71429%; +} .emojione-1f34d { - background-position: 47.61905% 38.09524%; } + background-position: 47.61905% 38.09524%; +} .emojione-1f34e { - background-position: 47.61905% 40.47619%; } + background-position: 47.61905% 40.47619%; +} .emojione-1f34f { - background-position: 47.61905% 42.85714%; } + background-position: 47.61905% 42.85714%; +} .emojione-1f350 { - background-position: 47.61905% 45.2381%; } + background-position: 47.61905% 45.2381%; +} .emojione-1f351 { - background-position: 0% 47.61905%; } + background-position: 0% 47.61905%; +} .emojione-1f352 { - background-position: 2.38095% 47.61905%; } + background-position: 2.38095% 47.61905%; +} .emojione-1f353 { - background-position: 4.7619% 47.61905%; } + background-position: 4.7619% 47.61905%; +} .emojione-1f354 { - background-position: 7.14286% 47.61905%; } + background-position: 7.14286% 47.61905%; +} .emojione-1f355 { - background-position: 9.52381% 47.61905%; } + background-position: 9.52381% 47.61905%; +} .emojione-1f356 { - background-position: 11.90476% 47.61905%; } + background-position: 11.90476% 47.61905%; +} .emojione-1f357 { - background-position: 14.28571% 47.61905%; } + background-position: 14.28571% 47.61905%; +} .emojione-1f358 { - background-position: 16.66667% 47.61905%; } + background-position: 16.66667% 47.61905%; +} .emojione-1f359 { - background-position: 19.04762% 47.61905%; } + background-position: 19.04762% 47.61905%; +} .emojione-1f35a { - background-position: 21.42857% 47.61905%; } + background-position: 21.42857% 47.61905%; +} .emojione-1f35b { - background-position: 23.80952% 47.61905%; } + background-position: 23.80952% 47.61905%; +} .emojione-1f35c { - background-position: 26.19048% 47.61905%; } + background-position: 26.19048% 47.61905%; +} .emojione-1f35d { - background-position: 28.57143% 47.61905%; } + background-position: 28.57143% 47.61905%; +} .emojione-1f35e { - background-position: 30.95238% 47.61905%; } + background-position: 30.95238% 47.61905%; +} .emojione-1f35f { - background-position: 33.33333% 47.61905%; } + background-position: 33.33333% 47.61905%; +} .emojione-1f360 { - background-position: 35.71429% 47.61905%; } + background-position: 35.71429% 47.61905%; +} .emojione-1f361 { - background-position: 38.09524% 47.61905%; } + background-position: 38.09524% 47.61905%; +} .emojione-1f362 { - background-position: 40.47619% 47.61905%; } + background-position: 40.47619% 47.61905%; +} .emojione-1f363 { - background-position: 42.85714% 47.61905%; } + background-position: 42.85714% 47.61905%; +} .emojione-1f364 { - background-position: 45.2381% 47.61905%; } + background-position: 45.2381% 47.61905%; +} .emojione-1f365 { - background-position: 47.61905% 47.61905%; } + background-position: 47.61905% 47.61905%; +} .emojione-1f366 { - background-position: 50% 0%; } + background-position: 50% 0%; +} .emojione-1f367 { - background-position: 50% 2.38095%; } + background-position: 50% 2.38095%; +} .emojione-1f368 { - background-position: 50% 4.7619%; } + background-position: 50% 4.7619%; +} .emojione-1f369 { - background-position: 50% 7.14286%; } + background-position: 50% 7.14286%; +} .emojione-1f36a { - background-position: 50% 9.52381%; } + background-position: 50% 9.52381%; +} .emojione-1f36b { - background-position: 50% 11.90476%; } + background-position: 50% 11.90476%; +} .emojione-1f36c { - background-position: 50% 14.28571%; } + background-position: 50% 14.28571%; +} .emojione-1f36d { - background-position: 50% 16.66667%; } + background-position: 50% 16.66667%; +} .emojione-1f36e { - background-position: 50% 19.04762%; } + background-position: 50% 19.04762%; +} .emojione-1f36f { - background-position: 50% 21.42857%; } + background-position: 50% 21.42857%; +} .emojione-1f370 { - background-position: 50% 23.80952%; } + background-position: 50% 23.80952%; +} .emojione-1f371 { - background-position: 50% 26.19048%; } + background-position: 50% 26.19048%; +} .emojione-1f372 { - background-position: 50% 28.57143%; } + background-position: 50% 28.57143%; +} .emojione-1f373 { - background-position: 50% 30.95238%; } + background-position: 50% 30.95238%; +} .emojione-1f374 { - background-position: 50% 33.33333%; } + background-position: 50% 33.33333%; +} .emojione-1f375 { - background-position: 50% 35.71429%; } + background-position: 50% 35.71429%; +} .emojione-1f376 { - background-position: 50% 38.09524%; } + background-position: 50% 38.09524%; +} .emojione-1f377 { - background-position: 50% 40.47619%; } + background-position: 50% 40.47619%; +} .emojione-1f378 { - background-position: 50% 42.85714%; } + background-position: 50% 42.85714%; +} .emojione-1f379 { - background-position: 50% 45.2381%; } + background-position: 50% 45.2381%; +} .emojione-1f37a { - background-position: 50% 47.61905%; } + background-position: 50% 47.61905%; +} .emojione-1f37b { - background-position: 0% 50%; } + background-position: 0% 50%; +} .emojione-1f37c { - background-position: 2.38095% 50%; } + background-position: 2.38095% 50%; +} .emojione-1f37d { - background-position: 4.7619% 50%; } + background-position: 4.7619% 50%; +} .emojione-1f37e { - background-position: 7.14286% 50%; } + background-position: 7.14286% 50%; +} .emojione-1f37f { - background-position: 9.52381% 50%; } + background-position: 9.52381% 50%; +} .emojione-1f380 { - background-position: 11.90476% 50%; } + background-position: 11.90476% 50%; +} .emojione-1f381 { - background-position: 14.28571% 50%; } + background-position: 14.28571% 50%; +} .emojione-1f382 { - background-position: 16.66667% 50%; } + background-position: 16.66667% 50%; +} .emojione-1f383 { - background-position: 19.04762% 50%; } + background-position: 19.04762% 50%; +} .emojione-1f384 { - background-position: 21.42857% 50%; } + background-position: 21.42857% 50%; +} .emojione-1f385-1f3fb { - background-position: 23.80952% 50%; } + background-position: 23.80952% 50%; +} .emojione-1f385-1f3fc { - background-position: 26.19048% 50%; } + background-position: 26.19048% 50%; +} .emojione-1f385-1f3fd { - background-position: 28.57143% 50%; } + background-position: 28.57143% 50%; +} .emojione-1f385-1f3fe { - background-position: 30.95238% 50%; } + background-position: 30.95238% 50%; +} .emojione-1f385-1f3ff { - background-position: 33.33333% 50%; } + background-position: 33.33333% 50%; +} .emojione-1f385 { - background-position: 35.71429% 50%; } + background-position: 35.71429% 50%; +} .emojione-1f386 { - background-position: 38.09524% 50%; } + background-position: 38.09524% 50%; +} .emojione-1f387 { - background-position: 40.47619% 50%; } + background-position: 40.47619% 50%; +} .emojione-1f388 { - background-position: 42.85714% 50%; } + background-position: 42.85714% 50%; +} .emojione-1f389 { - background-position: 45.2381% 50%; } + background-position: 45.2381% 50%; +} .emojione-1f38a { - background-position: 47.61905% 50%; } + background-position: 47.61905% 50%; +} .emojione-1f38b { - background-position: 50% 50%; } + background-position: 50% 50%; +} .emojione-1f38c { - background-position: 52.38095% 0%; } + background-position: 52.38095% 0%; +} .emojione-1f38d { - background-position: 52.38095% 2.38095%; } + background-position: 52.38095% 2.38095%; +} .emojione-1f38e { - background-position: 52.38095% 4.7619%; } + background-position: 52.38095% 4.7619%; +} .emojione-1f38f { - background-position: 52.38095% 7.14286%; } + background-position: 52.38095% 7.14286%; +} .emojione-1f390 { - background-position: 52.38095% 9.52381%; } + background-position: 52.38095% 9.52381%; +} .emojione-1f391 { - background-position: 52.38095% 11.90476%; } + background-position: 52.38095% 11.90476%; +} .emojione-1f392 { - background-position: 52.38095% 14.28571%; } + background-position: 52.38095% 14.28571%; +} .emojione-1f393 { - background-position: 52.38095% 16.66667%; } + background-position: 52.38095% 16.66667%; +} .emojione-1f396 { - background-position: 52.38095% 19.04762%; } + background-position: 52.38095% 19.04762%; +} .emojione-1f397 { - background-position: 52.38095% 21.42857%; } + background-position: 52.38095% 21.42857%; +} .emojione-1f399 { - background-position: 52.38095% 23.80952%; } + background-position: 52.38095% 23.80952%; +} .emojione-1f39a { - background-position: 52.38095% 26.19048%; } + background-position: 52.38095% 26.19048%; +} .emojione-1f39b { - background-position: 52.38095% 28.57143%; } + background-position: 52.38095% 28.57143%; +} .emojione-1f39e { - background-position: 52.38095% 30.95238%; } + background-position: 52.38095% 30.95238%; +} .emojione-1f39f { - background-position: 52.38095% 33.33333%; } + background-position: 52.38095% 33.33333%; +} .emojione-1f3a0 { - background-position: 52.38095% 35.71429%; } + background-position: 52.38095% 35.71429%; +} .emojione-1f3a1 { - background-position: 52.38095% 38.09524%; } + background-position: 52.38095% 38.09524%; +} .emojione-1f3a2 { - background-position: 52.38095% 40.47619%; } + background-position: 52.38095% 40.47619%; +} .emojione-1f3a3 { - background-position: 52.38095% 42.85714%; } + background-position: 52.38095% 42.85714%; +} .emojione-1f3a4 { - background-position: 52.38095% 45.2381%; } + background-position: 52.38095% 45.2381%; +} .emojione-1f3a5 { - background-position: 52.38095% 47.61905%; } + background-position: 52.38095% 47.61905%; +} .emojione-1f3a6 { - background-position: 52.38095% 50%; } + background-position: 52.38095% 50%; +} .emojione-1f3a7 { - background-position: 0% 52.38095%; } + background-position: 0% 52.38095%; +} .emojione-1f3a8 { - background-position: 2.38095% 52.38095%; } + background-position: 2.38095% 52.38095%; +} .emojione-1f3a9 { - background-position: 4.7619% 52.38095%; } + background-position: 4.7619% 52.38095%; +} .emojione-1f3aa { - background-position: 7.14286% 52.38095%; } + background-position: 7.14286% 52.38095%; +} .emojione-1f3ab { - background-position: 9.52381% 52.38095%; } + background-position: 9.52381% 52.38095%; +} .emojione-1f3ac { - background-position: 11.90476% 52.38095%; } + background-position: 11.90476% 52.38095%; +} .emojione-1f3ad { - background-position: 14.28571% 52.38095%; } + background-position: 14.28571% 52.38095%; +} .emojione-1f3ae { - background-position: 16.66667% 52.38095%; } + background-position: 16.66667% 52.38095%; +} .emojione-1f3af { - background-position: 19.04762% 52.38095%; } + background-position: 19.04762% 52.38095%; +} .emojione-1f3b0 { - background-position: 21.42857% 52.38095%; } + background-position: 21.42857% 52.38095%; +} .emojione-1f3b1 { - background-position: 23.80952% 52.38095%; } + background-position: 23.80952% 52.38095%; +} .emojione-1f3b2 { - background-position: 26.19048% 52.38095%; } + background-position: 26.19048% 52.38095%; +} .emojione-1f3b3 { - background-position: 28.57143% 52.38095%; } + background-position: 28.57143% 52.38095%; +} .emojione-1f3b4 { - background-position: 30.95238% 52.38095%; } + background-position: 30.95238% 52.38095%; +} .emojione-1f3b5 { - background-position: 33.33333% 52.38095%; } + background-position: 33.33333% 52.38095%; +} .emojione-1f3b6 { - background-position: 35.71429% 52.38095%; } + background-position: 35.71429% 52.38095%; +} .emojione-1f3b7 { - background-position: 38.09524% 52.38095%; } + background-position: 38.09524% 52.38095%; +} .emojione-1f3b8 { - background-position: 40.47619% 52.38095%; } + background-position: 40.47619% 52.38095%; +} .emojione-1f3b9 { - background-position: 42.85714% 52.38095%; } + background-position: 42.85714% 52.38095%; +} .emojione-1f3ba { - background-position: 45.2381% 52.38095%; } + background-position: 45.2381% 52.38095%; +} .emojione-1f3bb { - background-position: 47.61905% 52.38095%; } + background-position: 47.61905% 52.38095%; +} .emojione-1f3bc { - background-position: 50% 52.38095%; } + background-position: 50% 52.38095%; +} .emojione-1f3bd { - background-position: 52.38095% 52.38095%; } + background-position: 52.38095% 52.38095%; +} .emojione-1f3be { - background-position: 54.7619% 0%; } + background-position: 54.7619% 0%; +} .emojione-1f3bf { - background-position: 54.7619% 2.38095%; } + background-position: 54.7619% 2.38095%; +} .emojione-1f3c0 { - background-position: 54.7619% 4.7619%; } + background-position: 54.7619% 4.7619%; +} .emojione-1f3c1 { - background-position: 54.7619% 7.14286%; } + background-position: 54.7619% 7.14286%; +} .emojione-1f3c2 { - background-position: 54.7619% 9.52381%; } + background-position: 54.7619% 9.52381%; +} .emojione-1f3c3-1f3fb { - background-position: 54.7619% 11.90476%; } + background-position: 54.7619% 11.90476%; +} .emojione-1f3c3-1f3fc { - background-position: 54.7619% 14.28571%; } + background-position: 54.7619% 14.28571%; +} .emojione-1f3c3-1f3fd { - background-position: 54.7619% 16.66667%; } + background-position: 54.7619% 16.66667%; +} .emojione-1f3c3-1f3fe { - background-position: 54.7619% 19.04762%; } + background-position: 54.7619% 19.04762%; +} .emojione-1f3c3-1f3ff { - background-position: 54.7619% 21.42857%; } + background-position: 54.7619% 21.42857%; +} .emojione-1f3c3 { - background-position: 54.7619% 23.80952%; } + background-position: 54.7619% 23.80952%; +} .emojione-1f3c4-1f3fb { - background-position: 54.7619% 26.19048%; } + background-position: 54.7619% 26.19048%; +} .emojione-1f3c4-1f3fc { - background-position: 54.7619% 28.57143%; } + background-position: 54.7619% 28.57143%; +} .emojione-1f3c4-1f3fd { - background-position: 54.7619% 30.95238%; } + background-position: 54.7619% 30.95238%; +} .emojione-1f3c4-1f3fe { - background-position: 54.7619% 33.33333%; } + background-position: 54.7619% 33.33333%; +} .emojione-1f3c4-1f3ff { - background-position: 54.7619% 35.71429%; } + background-position: 54.7619% 35.71429%; +} .emojione-1f3c4 { - background-position: 54.7619% 38.09524%; } + background-position: 54.7619% 38.09524%; +} .emojione-1f3c5 { - background-position: 54.7619% 40.47619%; } + background-position: 54.7619% 40.47619%; +} .emojione-1f3c6 { - background-position: 54.7619% 42.85714%; } + background-position: 54.7619% 42.85714%; +} .emojione-1f3c7-1f3fb { - background-position: 54.7619% 45.2381%; } + background-position: 54.7619% 45.2381%; +} .emojione-1f3c7-1f3fc { - background-position: 54.7619% 47.61905%; } + background-position: 54.7619% 47.61905%; +} .emojione-1f3c7-1f3fd { - background-position: 54.7619% 50%; } + background-position: 54.7619% 50%; +} .emojione-1f3c7-1f3fe { - background-position: 54.7619% 52.38095%; } + background-position: 54.7619% 52.38095%; +} .emojione-1f3c7-1f3ff { - background-position: 0% 54.7619%; } + background-position: 0% 54.7619%; +} .emojione-1f3c7 { - background-position: 2.38095% 54.7619%; } + background-position: 2.38095% 54.7619%; +} .emojione-1f3c8 { - background-position: 4.7619% 54.7619%; } + background-position: 4.7619% 54.7619%; +} .emojione-1f3c9 { - background-position: 7.14286% 54.7619%; } + background-position: 7.14286% 54.7619%; +} .emojione-1f3ca-1f3fb { - background-position: 9.52381% 54.7619%; } + background-position: 9.52381% 54.7619%; +} .emojione-1f3ca-1f3fc { - background-position: 11.90476% 54.7619%; } + background-position: 11.90476% 54.7619%; +} .emojione-1f3ca-1f3fd { - background-position: 14.28571% 54.7619%; } + background-position: 14.28571% 54.7619%; +} .emojione-1f3ca-1f3fe { - background-position: 16.66667% 54.7619%; } + background-position: 16.66667% 54.7619%; +} .emojione-1f3ca-1f3ff { - background-position: 19.04762% 54.7619%; } + background-position: 19.04762% 54.7619%; +} .emojione-1f3ca { - background-position: 21.42857% 54.7619%; } + background-position: 21.42857% 54.7619%; +} .emojione-1f3cb-1f3fb { - background-position: 23.80952% 54.7619%; } + background-position: 23.80952% 54.7619%; +} .emojione-1f3cb-1f3fc { - background-position: 26.19048% 54.7619%; } + background-position: 26.19048% 54.7619%; +} .emojione-1f3cb-1f3fd { - background-position: 28.57143% 54.7619%; } + background-position: 28.57143% 54.7619%; +} .emojione-1f3cb-1f3fe { - background-position: 30.95238% 54.7619%; } + background-position: 30.95238% 54.7619%; +} .emojione-1f3cb-1f3ff { - background-position: 33.33333% 54.7619%; } + background-position: 33.33333% 54.7619%; +} .emojione-1f3cb { - background-position: 35.71429% 54.7619%; } + background-position: 35.71429% 54.7619%; +} .emojione-1f3cc { - background-position: 38.09524% 54.7619%; } + background-position: 38.09524% 54.7619%; +} .emojione-1f3cd { - background-position: 40.47619% 54.7619%; } + background-position: 40.47619% 54.7619%; +} .emojione-1f3ce { - background-position: 42.85714% 54.7619%; } + background-position: 42.85714% 54.7619%; +} .emojione-1f3cf { - background-position: 45.2381% 54.7619%; } + background-position: 45.2381% 54.7619%; +} .emojione-1f3d0 { - background-position: 47.61905% 54.7619%; } + background-position: 47.61905% 54.7619%; +} .emojione-1f3d1 { - background-position: 50% 54.7619%; } + background-position: 50% 54.7619%; +} .emojione-1f3d2 { - background-position: 52.38095% 54.7619%; } + background-position: 52.38095% 54.7619%; +} .emojione-1f3d3 { - background-position: 54.7619% 54.7619%; } + background-position: 54.7619% 54.7619%; +} .emojione-1f3d4 { - background-position: 57.14286% 0%; } + background-position: 57.14286% 0%; +} .emojione-1f3d5 { - background-position: 57.14286% 2.38095%; } + background-position: 57.14286% 2.38095%; +} .emojione-1f3d6 { - background-position: 57.14286% 4.7619%; } + background-position: 57.14286% 4.7619%; +} .emojione-1f3d7 { - background-position: 57.14286% 7.14286%; } + background-position: 57.14286% 7.14286%; +} .emojione-1f3d8 { - background-position: 57.14286% 9.52381%; } + background-position: 57.14286% 9.52381%; +} .emojione-1f3d9 { - background-position: 57.14286% 11.90476%; } + background-position: 57.14286% 11.90476%; +} .emojione-1f3da { - background-position: 57.14286% 14.28571%; } + background-position: 57.14286% 14.28571%; +} .emojione-1f3db { - background-position: 57.14286% 16.66667%; } + background-position: 57.14286% 16.66667%; +} .emojione-1f3dc { - background-position: 57.14286% 19.04762%; } + background-position: 57.14286% 19.04762%; +} .emojione-1f3dd { - background-position: 57.14286% 21.42857%; } + background-position: 57.14286% 21.42857%; +} .emojione-1f3de { - background-position: 57.14286% 23.80952%; } + background-position: 57.14286% 23.80952%; +} .emojione-1f3df { - background-position: 57.14286% 26.19048%; } + background-position: 57.14286% 26.19048%; +} .emojione-1f3e0 { - background-position: 57.14286% 28.57143%; } + background-position: 57.14286% 28.57143%; +} .emojione-1f3e1 { - background-position: 57.14286% 30.95238%; } + background-position: 57.14286% 30.95238%; +} .emojione-1f3e2 { - background-position: 57.14286% 33.33333%; } + background-position: 57.14286% 33.33333%; +} .emojione-1f3e3 { - background-position: 57.14286% 35.71429%; } + background-position: 57.14286% 35.71429%; +} .emojione-1f3e4 { - background-position: 57.14286% 38.09524%; } + background-position: 57.14286% 38.09524%; +} .emojione-1f3e5 { - background-position: 57.14286% 40.47619%; } + background-position: 57.14286% 40.47619%; +} .emojione-1f3e6 { - background-position: 57.14286% 42.85714%; } + background-position: 57.14286% 42.85714%; +} .emojione-1f3e7 { - background-position: 57.14286% 45.2381%; } + background-position: 57.14286% 45.2381%; +} .emojione-1f3e8 { - background-position: 57.14286% 47.61905%; } + background-position: 57.14286% 47.61905%; +} .emojione-1f3e9 { - background-position: 57.14286% 50%; } + background-position: 57.14286% 50%; +} .emojione-1f3ea { - background-position: 57.14286% 52.38095%; } + background-position: 57.14286% 52.38095%; +} .emojione-1f3eb { - background-position: 57.14286% 54.7619%; } + background-position: 57.14286% 54.7619%; +} .emojione-1f3ec { - background-position: 0% 57.14286%; } + background-position: 0% 57.14286%; +} .emojione-1f3ed { - background-position: 2.38095% 57.14286%; } + background-position: 2.38095% 57.14286%; +} .emojione-1f3ee { - background-position: 4.7619% 57.14286%; } + background-position: 4.7619% 57.14286%; +} .emojione-1f3ef { - background-position: 7.14286% 57.14286%; } + background-position: 7.14286% 57.14286%; +} .emojione-1f3f0 { - background-position: 9.52381% 57.14286%; } + background-position: 9.52381% 57.14286%; +} .emojione-1f3f3-1f308 { - background-position: 11.90476% 57.14286%; } + background-position: 11.90476% 57.14286%; +} .emojione-1f3f3 { - background-position: 14.28571% 57.14286%; } + background-position: 14.28571% 57.14286%; +} .emojione-1f3f4 { - background-position: 16.66667% 57.14286%; } + background-position: 16.66667% 57.14286%; +} .emojione-1f3f5 { - background-position: 19.04762% 57.14286%; } + background-position: 19.04762% 57.14286%; +} .emojione-1f3f7 { - background-position: 21.42857% 57.14286%; } + background-position: 21.42857% 57.14286%; +} .emojione-1f3f8 { - background-position: 23.80952% 57.14286%; } + background-position: 23.80952% 57.14286%; +} .emojione-1f3f9 { - background-position: 26.19048% 57.14286%; } + background-position: 26.19048% 57.14286%; +} .emojione-1f3fa { - background-position: 28.57143% 57.14286%; } + background-position: 28.57143% 57.14286%; +} .emojione-1f3fb { - background-position: 30.95238% 57.14286%; } + background-position: 30.95238% 57.14286%; +} .emojione-1f3fc { - background-position: 33.33333% 57.14286%; } + background-position: 33.33333% 57.14286%; +} .emojione-1f3fd { - background-position: 35.71429% 57.14286%; } + background-position: 35.71429% 57.14286%; +} .emojione-1f3fe { - background-position: 38.09524% 57.14286%; } + background-position: 38.09524% 57.14286%; +} .emojione-1f3ff { - background-position: 40.47619% 57.14286%; } + background-position: 40.47619% 57.14286%; +} .emojione-1f400 { - background-position: 42.85714% 57.14286%; } + background-position: 42.85714% 57.14286%; +} .emojione-1f401 { - background-position: 45.2381% 57.14286%; } + background-position: 45.2381% 57.14286%; +} .emojione-1f402 { - background-position: 47.61905% 57.14286%; } + background-position: 47.61905% 57.14286%; +} .emojione-1f403 { - background-position: 50% 57.14286%; } + background-position: 50% 57.14286%; +} .emojione-1f404 { - background-position: 52.38095% 57.14286%; } + background-position: 52.38095% 57.14286%; +} .emojione-1f405 { - background-position: 54.7619% 57.14286%; } + background-position: 54.7619% 57.14286%; +} .emojione-1f406 { - background-position: 57.14286% 57.14286%; } + background-position: 57.14286% 57.14286%; +} .emojione-1f407 { - background-position: 59.52381% 0%; } + background-position: 59.52381% 0%; +} .emojione-1f408 { - background-position: 59.52381% 2.38095%; } + background-position: 59.52381% 2.38095%; +} .emojione-1f409 { - background-position: 59.52381% 4.7619%; } + background-position: 59.52381% 4.7619%; +} .emojione-1f40a { - background-position: 59.52381% 7.14286%; } + background-position: 59.52381% 7.14286%; +} .emojione-1f40b { - background-position: 59.52381% 9.52381%; } + background-position: 59.52381% 9.52381%; +} .emojione-1f40c { - background-position: 59.52381% 11.90476%; } + background-position: 59.52381% 11.90476%; +} .emojione-1f40d { - background-position: 59.52381% 14.28571%; } + background-position: 59.52381% 14.28571%; +} .emojione-1f40e { - background-position: 59.52381% 16.66667%; } + background-position: 59.52381% 16.66667%; +} .emojione-1f40f { - background-position: 59.52381% 19.04762%; } + background-position: 59.52381% 19.04762%; +} .emojione-1f410 { - background-position: 59.52381% 21.42857%; } + background-position: 59.52381% 21.42857%; +} .emojione-1f411 { - background-position: 59.52381% 23.80952%; } + background-position: 59.52381% 23.80952%; +} .emojione-1f412 { - background-position: 59.52381% 26.19048%; } + background-position: 59.52381% 26.19048%; +} .emojione-1f413 { - background-position: 59.52381% 28.57143%; } + background-position: 59.52381% 28.57143%; +} .emojione-1f414 { - background-position: 59.52381% 30.95238%; } + background-position: 59.52381% 30.95238%; +} .emojione-1f415 { - background-position: 59.52381% 33.33333%; } + background-position: 59.52381% 33.33333%; +} .emojione-1f416 { - background-position: 59.52381% 35.71429%; } + background-position: 59.52381% 35.71429%; +} .emojione-1f417 { - background-position: 59.52381% 38.09524%; } + background-position: 59.52381% 38.09524%; +} .emojione-1f418 { - background-position: 59.52381% 40.47619%; } + background-position: 59.52381% 40.47619%; +} .emojione-1f419 { - background-position: 59.52381% 42.85714%; } + background-position: 59.52381% 42.85714%; +} .emojione-1f41a { - background-position: 59.52381% 45.2381%; } + background-position: 59.52381% 45.2381%; +} .emojione-1f41b { - background-position: 59.52381% 47.61905%; } + background-position: 59.52381% 47.61905%; +} .emojione-1f41c { - background-position: 59.52381% 50%; } + background-position: 59.52381% 50%; +} .emojione-1f41d { - background-position: 59.52381% 52.38095%; } + background-position: 59.52381% 52.38095%; +} .emojione-1f41e { - background-position: 59.52381% 54.7619%; } + background-position: 59.52381% 54.7619%; +} .emojione-1f41f { - background-position: 59.52381% 57.14286%; } + background-position: 59.52381% 57.14286%; +} .emojione-1f420 { - background-position: 0% 59.52381%; } + background-position: 0% 59.52381%; +} .emojione-1f421 { - background-position: 2.38095% 59.52381%; } + background-position: 2.38095% 59.52381%; +} .emojione-1f422 { - background-position: 4.7619% 59.52381%; } + background-position: 4.7619% 59.52381%; +} .emojione-1f423 { - background-position: 7.14286% 59.52381%; } + background-position: 7.14286% 59.52381%; +} .emojione-1f424 { - background-position: 9.52381% 59.52381%; } + background-position: 9.52381% 59.52381%; +} .emojione-1f425 { - background-position: 11.90476% 59.52381%; } + background-position: 11.90476% 59.52381%; +} .emojione-1f426 { - background-position: 14.28571% 59.52381%; } + background-position: 14.28571% 59.52381%; +} .emojione-1f427 { - background-position: 16.66667% 59.52381%; } + background-position: 16.66667% 59.52381%; +} .emojione-1f428 { - background-position: 19.04762% 59.52381%; } + background-position: 19.04762% 59.52381%; +} .emojione-1f429 { - background-position: 21.42857% 59.52381%; } + background-position: 21.42857% 59.52381%; +} .emojione-1f42a { - background-position: 23.80952% 59.52381%; } + background-position: 23.80952% 59.52381%; +} .emojione-1f42b { - background-position: 26.19048% 59.52381%; } + background-position: 26.19048% 59.52381%; +} .emojione-1f42c { - background-position: 28.57143% 59.52381%; } + background-position: 28.57143% 59.52381%; +} .emojione-1f42d { - background-position: 30.95238% 59.52381%; } + background-position: 30.95238% 59.52381%; +} .emojione-1f42e { - background-position: 33.33333% 59.52381%; } + background-position: 33.33333% 59.52381%; +} .emojione-1f42f { - background-position: 35.71429% 59.52381%; } + background-position: 35.71429% 59.52381%; +} .emojione-1f430 { - background-position: 38.09524% 59.52381%; } + background-position: 38.09524% 59.52381%; +} .emojione-1f431 { - background-position: 40.47619% 59.52381%; } + background-position: 40.47619% 59.52381%; +} .emojione-1f432 { - background-position: 42.85714% 59.52381%; } + background-position: 42.85714% 59.52381%; +} .emojione-1f433 { - background-position: 45.2381% 59.52381%; } + background-position: 45.2381% 59.52381%; +} .emojione-1f434 { - background-position: 47.61905% 59.52381%; } + background-position: 47.61905% 59.52381%; +} .emojione-1f435 { - background-position: 50% 59.52381%; } + background-position: 50% 59.52381%; +} .emojione-1f436 { - background-position: 52.38095% 59.52381%; } + background-position: 52.38095% 59.52381%; +} .emojione-1f437 { - background-position: 54.7619% 59.52381%; } + background-position: 54.7619% 59.52381%; +} .emojione-1f438 { - background-position: 57.14286% 59.52381%; } + background-position: 57.14286% 59.52381%; +} .emojione-1f439 { - background-position: 59.52381% 59.52381%; } + background-position: 59.52381% 59.52381%; +} .emojione-1f43a { - background-position: 61.90476% 0%; } + background-position: 61.90476% 0%; +} .emojione-1f43b { - background-position: 61.90476% 2.38095%; } + background-position: 61.90476% 2.38095%; +} .emojione-1f43c { - background-position: 61.90476% 4.7619%; } + background-position: 61.90476% 4.7619%; +} .emojione-1f43d { - background-position: 61.90476% 7.14286%; } + background-position: 61.90476% 7.14286%; +} .emojione-1f43e { - background-position: 61.90476% 9.52381%; } + background-position: 61.90476% 9.52381%; +} .emojione-1f43f { - background-position: 61.90476% 11.90476%; } + background-position: 61.90476% 11.90476%; +} .emojione-1f440 { - background-position: 61.90476% 14.28571%; } + background-position: 61.90476% 14.28571%; +} .emojione-1f441-1f5e8 { - background-position: 61.90476% 16.66667%; } + background-position: 61.90476% 16.66667%; +} .emojione-1f441 { - background-position: 61.90476% 19.04762%; } + background-position: 61.90476% 19.04762%; +} .emojione-1f442-1f3fb { - background-position: 61.90476% 21.42857%; } + background-position: 61.90476% 21.42857%; +} .emojione-1f442-1f3fc { - background-position: 61.90476% 23.80952%; } + background-position: 61.90476% 23.80952%; +} .emojione-1f442-1f3fd { - background-position: 61.90476% 26.19048%; } + background-position: 61.90476% 26.19048%; +} .emojione-1f442-1f3fe { - background-position: 61.90476% 28.57143%; } + background-position: 61.90476% 28.57143%; +} .emojione-1f442-1f3ff { - background-position: 61.90476% 30.95238%; } + background-position: 61.90476% 30.95238%; +} .emojione-1f442 { - background-position: 61.90476% 33.33333%; } + background-position: 61.90476% 33.33333%; +} .emojione-1f443-1f3fb { - background-position: 61.90476% 35.71429%; } + background-position: 61.90476% 35.71429%; +} .emojione-1f443-1f3fc { - background-position: 61.90476% 38.09524%; } + background-position: 61.90476% 38.09524%; +} .emojione-1f443-1f3fd { - background-position: 61.90476% 40.47619%; } + background-position: 61.90476% 40.47619%; +} .emojione-1f443-1f3fe { - background-position: 61.90476% 42.85714%; } + background-position: 61.90476% 42.85714%; +} .emojione-1f443-1f3ff { - background-position: 61.90476% 45.2381%; } + background-position: 61.90476% 45.2381%; +} .emojione-1f443 { - background-position: 61.90476% 47.61905%; } + background-position: 61.90476% 47.61905%; +} .emojione-1f444 { - background-position: 61.90476% 50%; } + background-position: 61.90476% 50%; +} .emojione-1f445 { - background-position: 61.90476% 52.38095%; } + background-position: 61.90476% 52.38095%; +} .emojione-1f446-1f3fb { - background-position: 61.90476% 54.7619%; } + background-position: 61.90476% 54.7619%; +} .emojione-1f446-1f3fc { - background-position: 61.90476% 57.14286%; } + background-position: 61.90476% 57.14286%; +} .emojione-1f446-1f3fd { - background-position: 61.90476% 59.52381%; } + background-position: 61.90476% 59.52381%; +} .emojione-1f446-1f3fe { - background-position: 0% 61.90476%; } + background-position: 0% 61.90476%; +} .emojione-1f446-1f3ff { - background-position: 2.38095% 61.90476%; } + background-position: 2.38095% 61.90476%; +} .emojione-1f446 { - background-position: 4.7619% 61.90476%; } + background-position: 4.7619% 61.90476%; +} .emojione-1f447-1f3fb { - background-position: 7.14286% 61.90476%; } + background-position: 7.14286% 61.90476%; +} .emojione-1f447-1f3fc { - background-position: 9.52381% 61.90476%; } + background-position: 9.52381% 61.90476%; +} .emojione-1f447-1f3fd { - background-position: 11.90476% 61.90476%; } + background-position: 11.90476% 61.90476%; +} .emojione-1f447-1f3fe { - background-position: 14.28571% 61.90476%; } + background-position: 14.28571% 61.90476%; +} .emojione-1f447-1f3ff { - background-position: 16.66667% 61.90476%; } + background-position: 16.66667% 61.90476%; +} .emojione-1f447 { - background-position: 19.04762% 61.90476%; } + background-position: 19.04762% 61.90476%; +} .emojione-1f448-1f3fb { - background-position: 21.42857% 61.90476%; } + background-position: 21.42857% 61.90476%; +} .emojione-1f448-1f3fc { - background-position: 23.80952% 61.90476%; } + background-position: 23.80952% 61.90476%; +} .emojione-1f448-1f3fd { - background-position: 26.19048% 61.90476%; } + background-position: 26.19048% 61.90476%; +} .emojione-1f448-1f3fe { - background-position: 28.57143% 61.90476%; } + background-position: 28.57143% 61.90476%; +} .emojione-1f448-1f3ff { - background-position: 30.95238% 61.90476%; } + background-position: 30.95238% 61.90476%; +} .emojione-1f448 { - background-position: 33.33333% 61.90476%; } + background-position: 33.33333% 61.90476%; +} .emojione-1f449-1f3fb { - background-position: 35.71429% 61.90476%; } + background-position: 35.71429% 61.90476%; +} .emojione-1f449-1f3fc { - background-position: 38.09524% 61.90476%; } + background-position: 38.09524% 61.90476%; +} .emojione-1f449-1f3fd { - background-position: 40.47619% 61.90476%; } + background-position: 40.47619% 61.90476%; +} .emojione-1f449-1f3fe { - background-position: 42.85714% 61.90476%; } + background-position: 42.85714% 61.90476%; +} .emojione-1f449-1f3ff { - background-position: 45.2381% 61.90476%; } + background-position: 45.2381% 61.90476%; +} .emojione-1f449 { - background-position: 47.61905% 61.90476%; } + background-position: 47.61905% 61.90476%; +} .emojione-1f44a-1f3fb { - background-position: 50% 61.90476%; } + background-position: 50% 61.90476%; +} .emojione-1f44a-1f3fc { - background-position: 52.38095% 61.90476%; } + background-position: 52.38095% 61.90476%; +} .emojione-1f44a-1f3fd { - background-position: 54.7619% 61.90476%; } + background-position: 54.7619% 61.90476%; +} .emojione-1f44a-1f3fe { - background-position: 57.14286% 61.90476%; } + background-position: 57.14286% 61.90476%; +} .emojione-1f44a-1f3ff { - background-position: 59.52381% 61.90476%; } + background-position: 59.52381% 61.90476%; +} .emojione-1f44a { - background-position: 61.90476% 61.90476%; } + background-position: 61.90476% 61.90476%; +} .emojione-1f44b-1f3fb { - background-position: 64.28571% 0%; } + background-position: 64.28571% 0%; +} .emojione-1f44b-1f3fc { - background-position: 64.28571% 2.38095%; } + background-position: 64.28571% 2.38095%; +} .emojione-1f44b-1f3fd { - background-position: 64.28571% 4.7619%; } + background-position: 64.28571% 4.7619%; +} .emojione-1f44b-1f3fe { - background-position: 64.28571% 7.14286%; } + background-position: 64.28571% 7.14286%; +} .emojione-1f44b-1f3ff { - background-position: 64.28571% 9.52381%; } + background-position: 64.28571% 9.52381%; +} .emojione-1f44b { - background-position: 64.28571% 11.90476%; } + background-position: 64.28571% 11.90476%; +} .emojione-1f44c-1f3fb { - background-position: 64.28571% 14.28571%; } + background-position: 64.28571% 14.28571%; +} .emojione-1f44c-1f3fc { - background-position: 64.28571% 16.66667%; } + background-position: 64.28571% 16.66667%; +} .emojione-1f44c-1f3fd { - background-position: 64.28571% 19.04762%; } + background-position: 64.28571% 19.04762%; +} .emojione-1f44c-1f3fe { - background-position: 64.28571% 21.42857%; } + background-position: 64.28571% 21.42857%; +} .emojione-1f44c-1f3ff { - background-position: 64.28571% 23.80952%; } + background-position: 64.28571% 23.80952%; +} .emojione-1f44c { - background-position: 64.28571% 26.19048%; } + background-position: 64.28571% 26.19048%; +} .emojione-1f44d-1f3fb { - background-position: 64.28571% 28.57143%; } + background-position: 64.28571% 28.57143%; +} .emojione-1f44d-1f3fc { - background-position: 64.28571% 30.95238%; } + background-position: 64.28571% 30.95238%; +} .emojione-1f44d-1f3fd { - background-position: 64.28571% 33.33333%; } + background-position: 64.28571% 33.33333%; +} .emojione-1f44d-1f3fe { - background-position: 64.28571% 35.71429%; } + background-position: 64.28571% 35.71429%; +} .emojione-1f44d-1f3ff { - background-position: 64.28571% 38.09524%; } + background-position: 64.28571% 38.09524%; +} .emojione-1f44d { - background-position: 64.28571% 40.47619%; } + background-position: 64.28571% 40.47619%; +} .emojione-1f44e-1f3fb { - background-position: 64.28571% 42.85714%; } + background-position: 64.28571% 42.85714%; +} .emojione-1f44e-1f3fc { - background-position: 64.28571% 45.2381%; } + background-position: 64.28571% 45.2381%; +} .emojione-1f44e-1f3fd { - background-position: 64.28571% 47.61905%; } + background-position: 64.28571% 47.61905%; +} .emojione-1f44e-1f3fe { - background-position: 64.28571% 50%; } + background-position: 64.28571% 50%; +} .emojione-1f44e-1f3ff { - background-position: 64.28571% 52.38095%; } + background-position: 64.28571% 52.38095%; +} .emojione-1f44e { - background-position: 64.28571% 54.7619%; } + background-position: 64.28571% 54.7619%; +} .emojione-1f44f-1f3fb { - background-position: 64.28571% 57.14286%; } + background-position: 64.28571% 57.14286%; +} .emojione-1f44f-1f3fc { - background-position: 64.28571% 59.52381%; } + background-position: 64.28571% 59.52381%; +} .emojione-1f44f-1f3fd { - background-position: 64.28571% 61.90476%; } + background-position: 64.28571% 61.90476%; +} .emojione-1f44f-1f3fe { - background-position: 0% 64.28571%; } + background-position: 0% 64.28571%; +} .emojione-1f44f-1f3ff { - background-position: 2.38095% 64.28571%; } + background-position: 2.38095% 64.28571%; +} .emojione-1f44f { - background-position: 4.7619% 64.28571%; } + background-position: 4.7619% 64.28571%; +} .emojione-1f450-1f3fb { - background-position: 7.14286% 64.28571%; } + background-position: 7.14286% 64.28571%; +} .emojione-1f450-1f3fc { - background-position: 9.52381% 64.28571%; } + background-position: 9.52381% 64.28571%; +} .emojione-1f450-1f3fd { - background-position: 11.90476% 64.28571%; } + background-position: 11.90476% 64.28571%; +} .emojione-1f450-1f3fe { - background-position: 14.28571% 64.28571%; } + background-position: 14.28571% 64.28571%; +} .emojione-1f450-1f3ff { - background-position: 16.66667% 64.28571%; } + background-position: 16.66667% 64.28571%; +} .emojione-1f450 { - background-position: 19.04762% 64.28571%; } + background-position: 19.04762% 64.28571%; +} .emojione-1f451 { - background-position: 21.42857% 64.28571%; } + background-position: 21.42857% 64.28571%; +} .emojione-1f452 { - background-position: 23.80952% 64.28571%; } + background-position: 23.80952% 64.28571%; +} .emojione-1f453 { - background-position: 26.19048% 64.28571%; } + background-position: 26.19048% 64.28571%; +} .emojione-1f454 { - background-position: 28.57143% 64.28571%; } + background-position: 28.57143% 64.28571%; +} .emojione-1f455 { - background-position: 30.95238% 64.28571%; } + background-position: 30.95238% 64.28571%; +} .emojione-1f456 { - background-position: 33.33333% 64.28571%; } + background-position: 33.33333% 64.28571%; +} .emojione-1f457 { - background-position: 35.71429% 64.28571%; } + background-position: 35.71429% 64.28571%; +} .emojione-1f458 { - background-position: 38.09524% 64.28571%; } + background-position: 38.09524% 64.28571%; +} .emojione-1f459 { - background-position: 40.47619% 64.28571%; } + background-position: 40.47619% 64.28571%; +} .emojione-1f45a { - background-position: 42.85714% 64.28571%; } + background-position: 42.85714% 64.28571%; +} .emojione-1f45b { - background-position: 45.2381% 64.28571%; } + background-position: 45.2381% 64.28571%; +} .emojione-1f45c { - background-position: 47.61905% 64.28571%; } + background-position: 47.61905% 64.28571%; +} .emojione-1f45d { - background-position: 50% 64.28571%; } + background-position: 50% 64.28571%; +} .emojione-1f45e { - background-position: 52.38095% 64.28571%; } + background-position: 52.38095% 64.28571%; +} .emojione-1f45f { - background-position: 54.7619% 64.28571%; } + background-position: 54.7619% 64.28571%; +} .emojione-1f460 { - background-position: 57.14286% 64.28571%; } + background-position: 57.14286% 64.28571%; +} .emojione-1f461 { - background-position: 59.52381% 64.28571%; } + background-position: 59.52381% 64.28571%; +} .emojione-1f462 { - background-position: 61.90476% 64.28571%; } + background-position: 61.90476% 64.28571%; +} .emojione-1f463 { - background-position: 64.28571% 64.28571%; } + background-position: 64.28571% 64.28571%; +} .emojione-1f464 { - background-position: 66.66667% 0%; } + background-position: 66.66667% 0%; +} .emojione-1f465 { - background-position: 66.66667% 2.38095%; } + background-position: 66.66667% 2.38095%; +} .emojione-1f466-1f3fb { - background-position: 66.66667% 4.7619%; } + background-position: 66.66667% 4.7619%; +} .emojione-1f466-1f3fc { - background-position: 66.66667% 7.14286%; } + background-position: 66.66667% 7.14286%; +} .emojione-1f466-1f3fd { - background-position: 66.66667% 9.52381%; } + background-position: 66.66667% 9.52381%; +} .emojione-1f466-1f3fe { - background-position: 66.66667% 11.90476%; } + background-position: 66.66667% 11.90476%; +} .emojione-1f466-1f3ff { - background-position: 66.66667% 14.28571%; } + background-position: 66.66667% 14.28571%; +} .emojione-1f466 { - background-position: 66.66667% 16.66667%; } + background-position: 66.66667% 16.66667%; +} .emojione-1f467-1f3fb { - background-position: 66.66667% 19.04762%; } + background-position: 66.66667% 19.04762%; +} .emojione-1f467-1f3fc { - background-position: 66.66667% 21.42857%; } + background-position: 66.66667% 21.42857%; +} .emojione-1f467-1f3fd { - background-position: 66.66667% 23.80952%; } + background-position: 66.66667% 23.80952%; +} .emojione-1f467-1f3fe { - background-position: 66.66667% 26.19048%; } + background-position: 66.66667% 26.19048%; +} .emojione-1f467-1f3ff { - background-position: 66.66667% 28.57143%; } + background-position: 66.66667% 28.57143%; +} .emojione-1f467 { - background-position: 66.66667% 30.95238%; } + background-position: 66.66667% 30.95238%; +} .emojione-1f468-1f3fb { - background-position: 66.66667% 33.33333%; } + background-position: 66.66667% 33.33333%; +} .emojione-1f468-1f3fc { - background-position: 66.66667% 35.71429%; } + background-position: 66.66667% 35.71429%; +} .emojione-1f468-1f3fd { - background-position: 66.66667% 38.09524%; } + background-position: 66.66667% 38.09524%; +} .emojione-1f468-1f3fe { - background-position: 66.66667% 40.47619%; } + background-position: 66.66667% 40.47619%; +} .emojione-1f468-1f3ff { - background-position: 66.66667% 42.85714%; } + background-position: 66.66667% 42.85714%; +} .emojione-1f468-1f468-1f466-1f466 { - background-position: 66.66667% 45.2381%; } + background-position: 66.66667% 45.2381%; +} .emojione-1f468-1f468-1f466 { - background-position: 66.66667% 47.61905%; } + background-position: 66.66667% 47.61905%; +} .emojione-1f468-1f468-1f467-1f466 { - background-position: 66.66667% 50%; } + background-position: 66.66667% 50%; +} .emojione-1f468-1f468-1f467-1f467 { - background-position: 66.66667% 52.38095%; } + background-position: 66.66667% 52.38095%; +} .emojione-1f468-1f468-1f467 { - background-position: 66.66667% 54.7619%; } + background-position: 66.66667% 54.7619%; +} .emojione-1f468-1f469-1f466-1f466 { - background-position: 66.66667% 57.14286%; } + background-position: 66.66667% 57.14286%; +} .emojione-1f468-1f469-1f467-1f466 { - background-position: 66.66667% 59.52381%; } + background-position: 66.66667% 59.52381%; +} .emojione-1f468-1f469-1f467-1f467 { - background-position: 66.66667% 61.90476%; } + background-position: 66.66667% 61.90476%; +} .emojione-1f468-1f469-1f467 { - background-position: 66.66667% 64.28571%; } + background-position: 66.66667% 64.28571%; +} .emojione-1f468-2764-1f468 { - background-position: 0% 66.66667%; } + background-position: 0% 66.66667%; +} .emojione-1f468-2764-1f48b-1f468 { - background-position: 2.38095% 66.66667%; } + background-position: 2.38095% 66.66667%; +} .emojione-1f468 { - background-position: 4.7619% 66.66667%; } + background-position: 4.7619% 66.66667%; +} .emojione-1f469-1f3fb { - background-position: 7.14286% 66.66667%; } + background-position: 7.14286% 66.66667%; +} .emojione-1f469-1f3fc { - background-position: 9.52381% 66.66667%; } + background-position: 9.52381% 66.66667%; +} .emojione-1f469-1f3fd { - background-position: 11.90476% 66.66667%; } + background-position: 11.90476% 66.66667%; +} .emojione-1f469-1f3fe { - background-position: 14.28571% 66.66667%; } + background-position: 14.28571% 66.66667%; +} .emojione-1f469-1f3ff { - background-position: 16.66667% 66.66667%; } + background-position: 16.66667% 66.66667%; +} .emojione-1f469-1f469-1f466-1f466 { - background-position: 19.04762% 66.66667%; } + background-position: 19.04762% 66.66667%; +} .emojione-1f469-1f469-1f466 { - background-position: 21.42857% 66.66667%; } + background-position: 21.42857% 66.66667%; +} .emojione-1f469-1f469-1f467-1f466 { - background-position: 23.80952% 66.66667%; } + background-position: 23.80952% 66.66667%; +} .emojione-1f469-1f469-1f467-1f467 { - background-position: 26.19048% 66.66667%; } + background-position: 26.19048% 66.66667%; +} .emojione-1f469-1f469-1f467 { - background-position: 28.57143% 66.66667%; } + background-position: 28.57143% 66.66667%; +} .emojione-1f469-2764-1f469 { - background-position: 30.95238% 66.66667%; } + background-position: 30.95238% 66.66667%; +} .emojione-1f469-2764-1f48b-1f469 { - background-position: 33.33333% 66.66667%; } + background-position: 33.33333% 66.66667%; +} .emojione-1f469 { - background-position: 35.71429% 66.66667%; } + background-position: 35.71429% 66.66667%; +} .emojione-1f46a { - background-position: 38.09524% 66.66667%; } + background-position: 38.09524% 66.66667%; +} .emojione-1f46b { - background-position: 40.47619% 66.66667%; } + background-position: 40.47619% 66.66667%; +} .emojione-1f46c { - background-position: 42.85714% 66.66667%; } + background-position: 42.85714% 66.66667%; +} .emojione-1f46d { - background-position: 45.2381% 66.66667%; } + background-position: 45.2381% 66.66667%; +} .emojione-1f46e-1f3fb { - background-position: 47.61905% 66.66667%; } + background-position: 47.61905% 66.66667%; +} .emojione-1f46e-1f3fc { - background-position: 0% 0%; } + background-position: 0% 0%; +} .emojione-1f46e-1f3fd { - background-position: 52.38095% 66.66667%; } + background-position: 52.38095% 66.66667%; +} .emojione-1f46e-1f3fe { - background-position: 54.7619% 66.66667%; } + background-position: 54.7619% 66.66667%; +} .emojione-1f46e-1f3ff { - background-position: 57.14286% 66.66667%; } + background-position: 57.14286% 66.66667%; +} .emojione-1f46e { - background-position: 59.52381% 66.66667%; } + background-position: 59.52381% 66.66667%; +} .emojione-1f46f { - background-position: 61.90476% 66.66667%; } + background-position: 61.90476% 66.66667%; +} .emojione-1f470-1f3fb { - background-position: 64.28571% 66.66667%; } + background-position: 64.28571% 66.66667%; +} .emojione-1f470-1f3fc { - background-position: 66.66667% 66.66667%; } + background-position: 66.66667% 66.66667%; +} .emojione-1f470-1f3fd { - background-position: 69.04762% 0%; } + background-position: 69.04762% 0%; +} .emojione-1f470-1f3fe { - background-position: 69.04762% 2.38095%; } + background-position: 69.04762% 2.38095%; +} .emojione-1f470-1f3ff { - background-position: 69.04762% 4.7619%; } + background-position: 69.04762% 4.7619%; +} .emojione-1f470 { - background-position: 69.04762% 7.14286%; } + background-position: 69.04762% 7.14286%; +} .emojione-1f471-1f3fb { - background-position: 69.04762% 9.52381%; } + background-position: 69.04762% 9.52381%; +} .emojione-1f471-1f3fc { - background-position: 69.04762% 11.90476%; } + background-position: 69.04762% 11.90476%; +} .emojione-1f471-1f3fd { - background-position: 69.04762% 14.28571%; } + background-position: 69.04762% 14.28571%; +} .emojione-1f471-1f3fe { - background-position: 69.04762% 16.66667%; } + background-position: 69.04762% 16.66667%; +} .emojione-1f471-1f3ff { - background-position: 69.04762% 19.04762%; } + background-position: 69.04762% 19.04762%; +} .emojione-1f471 { - background-position: 69.04762% 21.42857%; } + background-position: 69.04762% 21.42857%; +} .emojione-1f472-1f3fb { - background-position: 69.04762% 23.80952%; } + background-position: 69.04762% 23.80952%; +} .emojione-1f472-1f3fc { - background-position: 69.04762% 26.19048%; } + background-position: 69.04762% 26.19048%; +} .emojione-1f472-1f3fd { - background-position: 69.04762% 28.57143%; } + background-position: 69.04762% 28.57143%; +} .emojione-1f472-1f3fe { - background-position: 69.04762% 30.95238%; } + background-position: 69.04762% 30.95238%; +} .emojione-1f472-1f3ff { - background-position: 69.04762% 33.33333%; } + background-position: 69.04762% 33.33333%; +} .emojione-1f472 { - background-position: 69.04762% 35.71429%; } + background-position: 69.04762% 35.71429%; +} .emojione-1f473-1f3fb { - background-position: 69.04762% 38.09524%; } + background-position: 69.04762% 38.09524%; +} .emojione-1f473-1f3fc { - background-position: 69.04762% 40.47619%; } + background-position: 69.04762% 40.47619%; +} .emojione-1f473-1f3fd { - background-position: 69.04762% 42.85714%; } + background-position: 69.04762% 42.85714%; +} .emojione-1f473-1f3fe { - background-position: 69.04762% 45.2381%; } + background-position: 69.04762% 45.2381%; +} .emojione-1f473-1f3ff { - background-position: 69.04762% 47.61905%; } + background-position: 69.04762% 47.61905%; +} .emojione-1f473 { - background-position: 69.04762% 50%; } + background-position: 69.04762% 50%; +} .emojione-1f474-1f3fb { - background-position: 69.04762% 52.38095%; } + background-position: 69.04762% 52.38095%; +} .emojione-1f474-1f3fc { - background-position: 69.04762% 54.7619%; } + background-position: 69.04762% 54.7619%; +} .emojione-1f474-1f3fd { - background-position: 69.04762% 57.14286%; } + background-position: 69.04762% 57.14286%; +} .emojione-1f474-1f3fe { - background-position: 69.04762% 59.52381%; } + background-position: 69.04762% 59.52381%; +} .emojione-1f474-1f3ff { - background-position: 69.04762% 61.90476%; } + background-position: 69.04762% 61.90476%; +} .emojione-1f474 { - background-position: 69.04762% 64.28571%; } + background-position: 69.04762% 64.28571%; +} .emojione-1f475-1f3fb { - background-position: 69.04762% 66.66667%; } + background-position: 69.04762% 66.66667%; +} .emojione-1f475-1f3fc { - background-position: 0% 69.04762%; } + background-position: 0% 69.04762%; +} .emojione-1f475-1f3fd { - background-position: 2.38095% 69.04762%; } + background-position: 2.38095% 69.04762%; +} .emojione-1f475-1f3fe { - background-position: 4.7619% 69.04762%; } + background-position: 4.7619% 69.04762%; +} .emojione-1f475-1f3ff { - background-position: 7.14286% 69.04762%; } + background-position: 7.14286% 69.04762%; +} .emojione-1f475 { - background-position: 9.52381% 69.04762%; } + background-position: 9.52381% 69.04762%; +} .emojione-1f476-1f3fb { - background-position: 11.90476% 69.04762%; } + background-position: 11.90476% 69.04762%; +} .emojione-1f476-1f3fc { - background-position: 14.28571% 69.04762%; } + background-position: 14.28571% 69.04762%; +} .emojione-1f476-1f3fd { - background-position: 16.66667% 69.04762%; } + background-position: 16.66667% 69.04762%; +} .emojione-1f476-1f3fe { - background-position: 19.04762% 69.04762%; } + background-position: 19.04762% 69.04762%; +} .emojione-1f476-1f3ff { - background-position: 21.42857% 69.04762%; } + background-position: 21.42857% 69.04762%; +} .emojione-1f476 { - background-position: 23.80952% 69.04762%; } + background-position: 23.80952% 69.04762%; +} .emojione-1f477-1f3fb { - background-position: 26.19048% 69.04762%; } + background-position: 26.19048% 69.04762%; +} .emojione-1f477-1f3fc { - background-position: 28.57143% 69.04762%; } + background-position: 28.57143% 69.04762%; +} .emojione-1f477-1f3fd { - background-position: 30.95238% 69.04762%; } + background-position: 30.95238% 69.04762%; +} .emojione-1f477-1f3fe { - background-position: 33.33333% 69.04762%; } + background-position: 33.33333% 69.04762%; +} .emojione-1f477-1f3ff { - background-position: 35.71429% 69.04762%; } + background-position: 35.71429% 69.04762%; +} .emojione-1f477 { - background-position: 38.09524% 69.04762%; } + background-position: 38.09524% 69.04762%; +} .emojione-1f478-1f3fb { - background-position: 40.47619% 69.04762%; } + background-position: 40.47619% 69.04762%; +} .emojione-1f478-1f3fc { - background-position: 42.85714% 69.04762%; } + background-position: 42.85714% 69.04762%; +} .emojione-1f478-1f3fd { - background-position: 45.2381% 69.04762%; } + background-position: 45.2381% 69.04762%; +} .emojione-1f478-1f3fe { - background-position: 47.61905% 69.04762%; } + background-position: 47.61905% 69.04762%; +} .emojione-1f478-1f3ff { - background-position: 50% 69.04762%; } + background-position: 50% 69.04762%; +} .emojione-1f478 { - background-position: 52.38095% 69.04762%; } + background-position: 52.38095% 69.04762%; +} .emojione-1f479 { - background-position: 54.7619% 69.04762%; } + background-position: 54.7619% 69.04762%; +} .emojione-1f47a { - background-position: 57.14286% 69.04762%; } + background-position: 57.14286% 69.04762%; +} .emojione-1f47b { - background-position: 59.52381% 69.04762%; } + background-position: 59.52381% 69.04762%; +} .emojione-1f47c-1f3fb { - background-position: 61.90476% 69.04762%; } + background-position: 61.90476% 69.04762%; +} .emojione-1f47c-1f3fc { - background-position: 64.28571% 69.04762%; } + background-position: 64.28571% 69.04762%; +} .emojione-1f47c-1f3fd { - background-position: 66.66667% 69.04762%; } + background-position: 66.66667% 69.04762%; +} .emojione-1f47c-1f3fe { - background-position: 69.04762% 69.04762%; } + background-position: 69.04762% 69.04762%; +} .emojione-1f47c-1f3ff { - background-position: 71.42857% 0%; } + background-position: 71.42857% 0%; +} .emojione-1f47c { - background-position: 71.42857% 2.38095%; } + background-position: 71.42857% 2.38095%; +} .emojione-1f47d { - background-position: 71.42857% 4.7619%; } + background-position: 71.42857% 4.7619%; +} .emojione-1f47e { - background-position: 71.42857% 7.14286%; } + background-position: 71.42857% 7.14286%; +} .emojione-1f47f { - background-position: 71.42857% 9.52381%; } + background-position: 71.42857% 9.52381%; +} .emojione-1f480 { - background-position: 71.42857% 11.90476%; } + background-position: 71.42857% 11.90476%; +} .emojione-1f481-1f3fb { - background-position: 71.42857% 14.28571%; } + background-position: 71.42857% 14.28571%; +} .emojione-1f481-1f3fc { - background-position: 71.42857% 16.66667%; } + background-position: 71.42857% 16.66667%; +} .emojione-1f481-1f3fd { - background-position: 71.42857% 19.04762%; } + background-position: 71.42857% 19.04762%; +} .emojione-1f481-1f3fe { - background-position: 71.42857% 21.42857%; } + background-position: 71.42857% 21.42857%; +} .emojione-1f481-1f3ff { - background-position: 71.42857% 23.80952%; } + background-position: 71.42857% 23.80952%; +} .emojione-1f481 { - background-position: 71.42857% 26.19048%; } + background-position: 71.42857% 26.19048%; +} .emojione-1f482-1f3fb { - background-position: 71.42857% 28.57143%; } + background-position: 71.42857% 28.57143%; +} .emojione-1f482-1f3fc { - background-position: 71.42857% 30.95238%; } + background-position: 71.42857% 30.95238%; +} .emojione-1f482-1f3fd { - background-position: 71.42857% 33.33333%; } + background-position: 71.42857% 33.33333%; +} .emojione-1f482-1f3fe { - background-position: 71.42857% 35.71429%; } + background-position: 71.42857% 35.71429%; +} .emojione-1f482-1f3ff { - background-position: 71.42857% 38.09524%; } + background-position: 71.42857% 38.09524%; +} .emojione-1f482 { - background-position: 71.42857% 40.47619%; } + background-position: 71.42857% 40.47619%; +} .emojione-1f483-1f3fb { - background-position: 71.42857% 42.85714%; } + background-position: 71.42857% 42.85714%; +} .emojione-1f483-1f3fc { - background-position: 71.42857% 45.2381%; } + background-position: 71.42857% 45.2381%; +} .emojione-1f483-1f3fd { - background-position: 71.42857% 47.61905%; } + background-position: 71.42857% 47.61905%; +} .emojione-1f483-1f3fe { - background-position: 71.42857% 50%; } + background-position: 71.42857% 50%; +} .emojione-1f483-1f3ff { - background-position: 71.42857% 52.38095%; } + background-position: 71.42857% 52.38095%; +} .emojione-1f483 { - background-position: 71.42857% 54.7619%; } + background-position: 71.42857% 54.7619%; +} .emojione-1f484 { - background-position: 71.42857% 57.14286%; } + background-position: 71.42857% 57.14286%; +} .emojione-1f485-1f3fb { - background-position: 71.42857% 59.52381%; } + background-position: 71.42857% 59.52381%; +} .emojione-1f485-1f3fc { - background-position: 71.42857% 61.90476%; } + background-position: 71.42857% 61.90476%; +} .emojione-1f485-1f3fd { - background-position: 71.42857% 64.28571%; } + background-position: 71.42857% 64.28571%; +} .emojione-1f485-1f3fe { - background-position: 71.42857% 66.66667%; } + background-position: 71.42857% 66.66667%; +} .emojione-1f485-1f3ff { - background-position: 71.42857% 69.04762%; } + background-position: 71.42857% 69.04762%; +} .emojione-1f485 { - background-position: 0% 71.42857%; } + background-position: 0% 71.42857%; +} .emojione-1f486-1f3fb { - background-position: 2.38095% 71.42857%; } + background-position: 2.38095% 71.42857%; +} .emojione-1f486-1f3fc { - background-position: 4.7619% 71.42857%; } + background-position: 4.7619% 71.42857%; +} .emojione-1f486-1f3fd { - background-position: 7.14286% 71.42857%; } + background-position: 7.14286% 71.42857%; +} .emojione-1f486-1f3fe { - background-position: 9.52381% 71.42857%; } + background-position: 9.52381% 71.42857%; +} .emojione-1f486-1f3ff { - background-position: 11.90476% 71.42857%; } + background-position: 11.90476% 71.42857%; +} .emojione-1f486 { - background-position: 14.28571% 71.42857%; } + background-position: 14.28571% 71.42857%; +} .emojione-1f487-1f3fb { - background-position: 16.66667% 71.42857%; } + background-position: 16.66667% 71.42857%; +} .emojione-1f487-1f3fc { - background-position: 19.04762% 71.42857%; } + background-position: 19.04762% 71.42857%; +} .emojione-1f487-1f3fd { - background-position: 21.42857% 71.42857%; } + background-position: 21.42857% 71.42857%; +} .emojione-1f487-1f3fe { - background-position: 23.80952% 71.42857%; } + background-position: 23.80952% 71.42857%; +} .emojione-1f487-1f3ff { - background-position: 26.19048% 71.42857%; } + background-position: 26.19048% 71.42857%; +} .emojione-1f487 { - background-position: 28.57143% 71.42857%; } + background-position: 28.57143% 71.42857%; +} .emojione-1f488 { - background-position: 30.95238% 71.42857%; } + background-position: 30.95238% 71.42857%; +} .emojione-1f489 { - background-position: 33.33333% 71.42857%; } + background-position: 33.33333% 71.42857%; +} .emojione-1f48a { - background-position: 35.71429% 71.42857%; } + background-position: 35.71429% 71.42857%; +} .emojione-1f48b { - background-position: 38.09524% 71.42857%; } + background-position: 38.09524% 71.42857%; +} .emojione-1f48c { - background-position: 40.47619% 71.42857%; } + background-position: 40.47619% 71.42857%; +} .emojione-1f48d { - background-position: 42.85714% 71.42857%; } + background-position: 42.85714% 71.42857%; +} .emojione-1f48e { - background-position: 45.2381% 71.42857%; } + background-position: 45.2381% 71.42857%; +} .emojione-1f48f { - background-position: 47.61905% 71.42857%; } + background-position: 47.61905% 71.42857%; +} .emojione-1f490 { - background-position: 50% 71.42857%; } + background-position: 50% 71.42857%; +} .emojione-1f491 { - background-position: 52.38095% 71.42857%; } + background-position: 52.38095% 71.42857%; +} .emojione-1f492 { - background-position: 54.7619% 71.42857%; } + background-position: 54.7619% 71.42857%; +} .emojione-1f493 { - background-position: 57.14286% 71.42857%; } + background-position: 57.14286% 71.42857%; +} .emojione-1f494 { - background-position: 59.52381% 71.42857%; } + background-position: 59.52381% 71.42857%; +} .emojione-1f495 { - background-position: 61.90476% 71.42857%; } + background-position: 61.90476% 71.42857%; +} .emojione-1f496 { - background-position: 64.28571% 71.42857%; } + background-position: 64.28571% 71.42857%; +} .emojione-1f497 { - background-position: 66.66667% 71.42857%; } + background-position: 66.66667% 71.42857%; +} .emojione-1f498 { - background-position: 69.04762% 71.42857%; } + background-position: 69.04762% 71.42857%; +} .emojione-1f499 { - background-position: 71.42857% 71.42857%; } + background-position: 71.42857% 71.42857%; +} .emojione-1f49a { - background-position: 73.80952% 0%; } + background-position: 73.80952% 0%; +} .emojione-1f49b { - background-position: 73.80952% 2.38095%; } + background-position: 73.80952% 2.38095%; +} .emojione-1f49c { - background-position: 73.80952% 4.7619%; } + background-position: 73.80952% 4.7619%; +} .emojione-1f49d { - background-position: 73.80952% 7.14286%; } + background-position: 73.80952% 7.14286%; +} .emojione-1f49e { - background-position: 73.80952% 9.52381%; } + background-position: 73.80952% 9.52381%; +} .emojione-1f49f { - background-position: 73.80952% 11.90476%; } + background-position: 73.80952% 11.90476%; +} .emojione-1f4a0 { - background-position: 73.80952% 14.28571%; } + background-position: 73.80952% 14.28571%; +} .emojione-1f4a1 { - background-position: 73.80952% 16.66667%; } + background-position: 73.80952% 16.66667%; +} .emojione-1f4a2 { - background-position: 73.80952% 19.04762%; } + background-position: 73.80952% 19.04762%; +} .emojione-1f4a3 { - background-position: 73.80952% 21.42857%; } + background-position: 73.80952% 21.42857%; +} .emojione-1f4a4 { - background-position: 73.80952% 23.80952%; } + background-position: 73.80952% 23.80952%; +} .emojione-1f4a5 { - background-position: 73.80952% 26.19048%; } + background-position: 73.80952% 26.19048%; +} .emojione-1f4a6 { - background-position: 73.80952% 28.57143%; } + background-position: 73.80952% 28.57143%; +} .emojione-1f4a7 { - background-position: 73.80952% 30.95238%; } + background-position: 73.80952% 30.95238%; +} .emojione-1f4a8 { - background-position: 73.80952% 33.33333%; } + background-position: 73.80952% 33.33333%; +} .emojione-1f4a9 { - background-position: 73.80952% 35.71429%; } + background-position: 73.80952% 35.71429%; +} .emojione-1f4aa-1f3fb { - background-position: 73.80952% 38.09524%; } + background-position: 73.80952% 38.09524%; +} .emojione-1f4aa-1f3fc { - background-position: 73.80952% 40.47619%; } + background-position: 73.80952% 40.47619%; +} .emojione-1f4aa-1f3fd { - background-position: 73.80952% 42.85714%; } + background-position: 73.80952% 42.85714%; +} .emojione-1f4aa-1f3fe { - background-position: 73.80952% 45.2381%; } + background-position: 73.80952% 45.2381%; +} .emojione-1f4aa-1f3ff { - background-position: 73.80952% 47.61905%; } + background-position: 73.80952% 47.61905%; +} .emojione-1f4aa { - background-position: 73.80952% 50%; } + background-position: 73.80952% 50%; +} .emojione-1f4ab { - background-position: 73.80952% 52.38095%; } + background-position: 73.80952% 52.38095%; +} .emojione-1f4ac { - background-position: 73.80952% 54.7619%; } + background-position: 73.80952% 54.7619%; +} .emojione-1f4ad { - background-position: 73.80952% 57.14286%; } + background-position: 73.80952% 57.14286%; +} .emojione-1f4ae { - background-position: 73.80952% 59.52381%; } + background-position: 73.80952% 59.52381%; +} .emojione-1f4af { - background-position: 73.80952% 61.90476%; } + background-position: 73.80952% 61.90476%; +} .emojione-1f4b0 { - background-position: 73.80952% 64.28571%; } + background-position: 73.80952% 64.28571%; +} .emojione-1f4b1 { - background-position: 73.80952% 66.66667%; } + background-position: 73.80952% 66.66667%; +} .emojione-1f4b2 { - background-position: 73.80952% 69.04762%; } + background-position: 73.80952% 69.04762%; +} .emojione-1f4b3 { - background-position: 73.80952% 71.42857%; } + background-position: 73.80952% 71.42857%; +} .emojione-1f4b4 { - background-position: 0% 73.80952%; } + background-position: 0% 73.80952%; +} .emojione-1f4b5 { - background-position: 2.38095% 73.80952%; } + background-position: 2.38095% 73.80952%; +} .emojione-1f4b6 { - background-position: 4.7619% 73.80952%; } + background-position: 4.7619% 73.80952%; +} .emojione-1f4b7 { - background-position: 7.14286% 73.80952%; } + background-position: 7.14286% 73.80952%; +} .emojione-1f4b8 { - background-position: 9.52381% 73.80952%; } + background-position: 9.52381% 73.80952%; +} .emojione-1f4b9 { - background-position: 11.90476% 73.80952%; } + background-position: 11.90476% 73.80952%; +} .emojione-1f4ba { - background-position: 14.28571% 73.80952%; } + background-position: 14.28571% 73.80952%; +} .emojione-1f4bb { - background-position: 16.66667% 73.80952%; } + background-position: 16.66667% 73.80952%; +} .emojione-1f4bc { - background-position: 19.04762% 73.80952%; } + background-position: 19.04762% 73.80952%; +} .emojione-1f4bd { - background-position: 21.42857% 73.80952%; } + background-position: 21.42857% 73.80952%; +} .emojione-1f4be { - background-position: 23.80952% 73.80952%; } + background-position: 23.80952% 73.80952%; +} .emojione-1f4bf { - background-position: 26.19048% 73.80952%; } + background-position: 26.19048% 73.80952%; +} .emojione-1f4c0 { - background-position: 28.57143% 73.80952%; } + background-position: 28.57143% 73.80952%; +} .emojione-1f4c1 { - background-position: 30.95238% 73.80952%; } + background-position: 30.95238% 73.80952%; +} .emojione-1f4c2 { - background-position: 33.33333% 73.80952%; } + background-position: 33.33333% 73.80952%; +} .emojione-1f4c3 { - background-position: 35.71429% 73.80952%; } + background-position: 35.71429% 73.80952%; +} .emojione-1f4c4 { - background-position: 38.09524% 73.80952%; } + background-position: 38.09524% 73.80952%; +} .emojione-1f4c5 { - background-position: 40.47619% 73.80952%; } + background-position: 40.47619% 73.80952%; +} .emojione-1f4c6 { - background-position: 42.85714% 73.80952%; } + background-position: 42.85714% 73.80952%; +} .emojione-1f4c7 { - background-position: 45.2381% 73.80952%; } + background-position: 45.2381% 73.80952%; +} .emojione-1f4c8 { - background-position: 47.61905% 73.80952%; } + background-position: 47.61905% 73.80952%; +} .emojione-1f4c9 { - background-position: 50% 73.80952%; } + background-position: 50% 73.80952%; +} .emojione-1f4ca { - background-position: 52.38095% 73.80952%; } + background-position: 52.38095% 73.80952%; +} .emojione-1f4cb { - background-position: 54.7619% 73.80952%; } + background-position: 54.7619% 73.80952%; +} .emojione-1f4cc { - background-position: 57.14286% 73.80952%; } + background-position: 57.14286% 73.80952%; +} .emojione-1f4cd { - background-position: 59.52381% 73.80952%; } + background-position: 59.52381% 73.80952%; +} .emojione-1f4ce { - background-position: 61.90476% 73.80952%; } + background-position: 61.90476% 73.80952%; +} .emojione-1f4cf { - background-position: 64.28571% 73.80952%; } + background-position: 64.28571% 73.80952%; +} .emojione-1f4d0 { - background-position: 66.66667% 73.80952%; } + background-position: 66.66667% 73.80952%; +} .emojione-1f4d1 { - background-position: 69.04762% 73.80952%; } + background-position: 69.04762% 73.80952%; +} .emojione-1f4d2 { - background-position: 71.42857% 73.80952%; } + background-position: 71.42857% 73.80952%; +} .emojione-1f4d3 { - background-position: 73.80952% 73.80952%; } + background-position: 73.80952% 73.80952%; +} .emojione-1f4d4 { - background-position: 76.19048% 0%; } + background-position: 76.19048% 0%; +} .emojione-1f4d5 { - background-position: 76.19048% 2.38095%; } + background-position: 76.19048% 2.38095%; +} .emojione-1f4d6 { - background-position: 76.19048% 4.7619%; } + background-position: 76.19048% 4.7619%; +} .emojione-1f4d7 { - background-position: 76.19048% 7.14286%; } + background-position: 76.19048% 7.14286%; +} .emojione-1f4d8 { - background-position: 76.19048% 9.52381%; } + background-position: 76.19048% 9.52381%; +} .emojione-1f4d9 { - background-position: 76.19048% 11.90476%; } + background-position: 76.19048% 11.90476%; +} .emojione-1f4da { - background-position: 76.19048% 14.28571%; } + background-position: 76.19048% 14.28571%; +} .emojione-1f4db { - background-position: 76.19048% 16.66667%; } + background-position: 76.19048% 16.66667%; +} .emojione-1f4dc { - background-position: 76.19048% 19.04762%; } + background-position: 76.19048% 19.04762%; +} .emojione-1f4dd { - background-position: 76.19048% 21.42857%; } + background-position: 76.19048% 21.42857%; +} .emojione-1f4de { - background-position: 76.19048% 23.80952%; } + background-position: 76.19048% 23.80952%; +} .emojione-1f4df { - background-position: 76.19048% 26.19048%; } + background-position: 76.19048% 26.19048%; +} .emojione-1f4e0 { - background-position: 76.19048% 28.57143%; } + background-position: 76.19048% 28.57143%; +} .emojione-1f4e1 { - background-position: 76.19048% 30.95238%; } + background-position: 76.19048% 30.95238%; +} .emojione-1f4e2 { - background-position: 76.19048% 33.33333%; } + background-position: 76.19048% 33.33333%; +} .emojione-1f4e3 { - background-position: 76.19048% 35.71429%; } + background-position: 76.19048% 35.71429%; +} .emojione-1f4e4 { - background-position: 76.19048% 38.09524%; } + background-position: 76.19048% 38.09524%; +} .emojione-1f4e5 { - background-position: 76.19048% 40.47619%; } + background-position: 76.19048% 40.47619%; +} .emojione-1f4e6 { - background-position: 76.19048% 42.85714%; } + background-position: 76.19048% 42.85714%; +} .emojione-1f4e7 { - background-position: 76.19048% 45.2381%; } + background-position: 76.19048% 45.2381%; +} .emojione-1f4e8 { - background-position: 76.19048% 47.61905%; } + background-position: 76.19048% 47.61905%; +} .emojione-1f4e9 { - background-position: 76.19048% 50%; } + background-position: 76.19048% 50%; +} .emojione-1f4ea { - background-position: 76.19048% 52.38095%; } + background-position: 76.19048% 52.38095%; +} .emojione-1f4eb { - background-position: 76.19048% 54.7619%; } + background-position: 76.19048% 54.7619%; +} .emojione-1f4ec { - background-position: 76.19048% 57.14286%; } + background-position: 76.19048% 57.14286%; +} .emojione-1f4ed { - background-position: 76.19048% 59.52381%; } + background-position: 76.19048% 59.52381%; +} .emojione-1f4ee { - background-position: 76.19048% 61.90476%; } + background-position: 76.19048% 61.90476%; +} .emojione-1f4ef { - background-position: 76.19048% 64.28571%; } + background-position: 76.19048% 64.28571%; +} .emojione-1f4f0 { - background-position: 76.19048% 66.66667%; } + background-position: 76.19048% 66.66667%; +} .emojione-1f4f1 { - background-position: 76.19048% 69.04762%; } + background-position: 76.19048% 69.04762%; +} .emojione-1f4f2 { - background-position: 76.19048% 71.42857%; } + background-position: 76.19048% 71.42857%; +} .emojione-1f4f3 { - background-position: 76.19048% 73.80952%; } + background-position: 76.19048% 73.80952%; +} .emojione-1f4f4 { - background-position: 0% 76.19048%; } + background-position: 0% 76.19048%; +} .emojione-1f4f5 { - background-position: 2.38095% 76.19048%; } + background-position: 2.38095% 76.19048%; +} .emojione-1f4f6 { - background-position: 4.7619% 76.19048%; } + background-position: 4.7619% 76.19048%; +} .emojione-1f4f7 { - background-position: 7.14286% 76.19048%; } + background-position: 7.14286% 76.19048%; +} .emojione-1f4f8 { - background-position: 9.52381% 76.19048%; } + background-position: 9.52381% 76.19048%; +} .emojione-1f4f9 { - background-position: 11.90476% 76.19048%; } + background-position: 11.90476% 76.19048%; +} .emojione-1f4fa { - background-position: 14.28571% 76.19048%; } + background-position: 14.28571% 76.19048%; +} .emojione-1f4fb { - background-position: 16.66667% 76.19048%; } + background-position: 16.66667% 76.19048%; +} .emojione-1f4fc { - background-position: 19.04762% 76.19048%; } + background-position: 19.04762% 76.19048%; +} .emojione-1f4fd { - background-position: 21.42857% 76.19048%; } + background-position: 21.42857% 76.19048%; +} .emojione-1f4ff { - background-position: 23.80952% 76.19048%; } + background-position: 23.80952% 76.19048%; +} .emojione-1f500 { - background-position: 26.19048% 76.19048%; } + background-position: 26.19048% 76.19048%; +} .emojione-1f501 { - background-position: 28.57143% 76.19048%; } + background-position: 28.57143% 76.19048%; +} .emojione-1f502 { - background-position: 30.95238% 76.19048%; } + background-position: 30.95238% 76.19048%; +} .emojione-1f503 { - background-position: 33.33333% 76.19048%; } + background-position: 33.33333% 76.19048%; +} .emojione-1f504 { - background-position: 35.71429% 76.19048%; } + background-position: 35.71429% 76.19048%; +} .emojione-1f505 { - background-position: 38.09524% 76.19048%; } + background-position: 38.09524% 76.19048%; +} .emojione-1f506 { - background-position: 40.47619% 76.19048%; } + background-position: 40.47619% 76.19048%; +} .emojione-1f507 { - background-position: 42.85714% 76.19048%; } + background-position: 42.85714% 76.19048%; +} .emojione-1f508 { - background-position: 45.2381% 76.19048%; } + background-position: 45.2381% 76.19048%; +} .emojione-1f509 { - background-position: 47.61905% 76.19048%; } + background-position: 47.61905% 76.19048%; +} .emojione-1f50a { - background-position: 50% 76.19048%; } + background-position: 50% 76.19048%; +} .emojione-1f50b { - background-position: 52.38095% 76.19048%; } + background-position: 52.38095% 76.19048%; +} .emojione-1f50c { - background-position: 54.7619% 76.19048%; } + background-position: 54.7619% 76.19048%; +} .emojione-1f50d { - background-position: 57.14286% 76.19048%; } + background-position: 57.14286% 76.19048%; +} .emojione-1f50e { - background-position: 59.52381% 76.19048%; } + background-position: 59.52381% 76.19048%; +} .emojione-1f50f { - background-position: 61.90476% 76.19048%; } + background-position: 61.90476% 76.19048%; +} .emojione-1f510 { - background-position: 64.28571% 76.19048%; } + background-position: 64.28571% 76.19048%; +} .emojione-1f511 { - background-position: 66.66667% 76.19048%; } + background-position: 66.66667% 76.19048%; +} .emojione-1f512 { - background-position: 69.04762% 76.19048%; } + background-position: 69.04762% 76.19048%; +} .emojione-1f513 { - background-position: 71.42857% 76.19048%; } + background-position: 71.42857% 76.19048%; +} .emojione-1f514 { - background-position: 73.80952% 76.19048%; } + background-position: 73.80952% 76.19048%; +} .emojione-1f515 { - background-position: 76.19048% 76.19048%; } + background-position: 76.19048% 76.19048%; +} .emojione-1f516 { - background-position: 78.57143% 0%; } + background-position: 78.57143% 0%; +} .emojione-1f517 { - background-position: 78.57143% 2.38095%; } + background-position: 78.57143% 2.38095%; +} .emojione-1f518 { - background-position: 78.57143% 4.7619%; } + background-position: 78.57143% 4.7619%; +} .emojione-1f519 { - background-position: 78.57143% 7.14286%; } + background-position: 78.57143% 7.14286%; +} .emojione-1f51a { - background-position: 78.57143% 9.52381%; } + background-position: 78.57143% 9.52381%; +} .emojione-1f51b { - background-position: 78.57143% 11.90476%; } + background-position: 78.57143% 11.90476%; +} .emojione-1f51c { - background-position: 78.57143% 14.28571%; } + background-position: 78.57143% 14.28571%; +} .emojione-1f51d { - background-position: 78.57143% 16.66667%; } + background-position: 78.57143% 16.66667%; +} .emojione-1f51e { - background-position: 78.57143% 19.04762%; } + background-position: 78.57143% 19.04762%; +} .emojione-1f51f { - background-position: 78.57143% 21.42857%; } + background-position: 78.57143% 21.42857%; +} .emojione-1f520 { - background-position: 78.57143% 23.80952%; } + background-position: 78.57143% 23.80952%; +} .emojione-1f521 { - background-position: 78.57143% 26.19048%; } + background-position: 78.57143% 26.19048%; +} .emojione-1f522 { - background-position: 78.57143% 28.57143%; } + background-position: 78.57143% 28.57143%; +} .emojione-1f523 { - background-position: 78.57143% 30.95238%; } + background-position: 78.57143% 30.95238%; +} .emojione-1f524 { - background-position: 78.57143% 33.33333%; } + background-position: 78.57143% 33.33333%; +} .emojione-1f525 { - background-position: 78.57143% 35.71429%; } + background-position: 78.57143% 35.71429%; +} .emojione-1f526 { - background-position: 78.57143% 38.09524%; } + background-position: 78.57143% 38.09524%; +} .emojione-1f527 { - background-position: 78.57143% 40.47619%; } + background-position: 78.57143% 40.47619%; +} .emojione-1f528 { - background-position: 78.57143% 42.85714%; } + background-position: 78.57143% 42.85714%; +} .emojione-1f529 { - background-position: 78.57143% 45.2381%; } + background-position: 78.57143% 45.2381%; +} .emojione-1f52a { - background-position: 78.57143% 47.61905%; } + background-position: 78.57143% 47.61905%; +} .emojione-1f52b { - background-position: 78.57143% 50%; } + background-position: 78.57143% 50%; +} .emojione-1f52c { - background-position: 78.57143% 52.38095%; } + background-position: 78.57143% 52.38095%; +} .emojione-1f52d { - background-position: 78.57143% 54.7619%; } + background-position: 78.57143% 54.7619%; +} .emojione-1f52e { - background-position: 78.57143% 57.14286%; } + background-position: 78.57143% 57.14286%; +} .emojione-1f52f { - background-position: 78.57143% 59.52381%; } + background-position: 78.57143% 59.52381%; +} .emojione-1f530 { - background-position: 78.57143% 61.90476%; } + background-position: 78.57143% 61.90476%; +} .emojione-1f531 { - background-position: 78.57143% 64.28571%; } + background-position: 78.57143% 64.28571%; +} .emojione-1f532 { - background-position: 78.57143% 66.66667%; } + background-position: 78.57143% 66.66667%; +} .emojione-1f533 { - background-position: 78.57143% 69.04762%; } + background-position: 78.57143% 69.04762%; +} .emojione-1f534 { - background-position: 78.57143% 71.42857%; } + background-position: 78.57143% 71.42857%; +} .emojione-1f535 { - background-position: 78.57143% 73.80952%; } + background-position: 78.57143% 73.80952%; +} .emojione-1f536 { - background-position: 78.57143% 76.19048%; } + background-position: 78.57143% 76.19048%; +} .emojione-1f537 { - background-position: 0% 78.57143%; } + background-position: 0% 78.57143%; +} .emojione-1f538 { - background-position: 2.38095% 78.57143%; } + background-position: 2.38095% 78.57143%; +} .emojione-1f539 { - background-position: 4.7619% 78.57143%; } + background-position: 4.7619% 78.57143%; +} .emojione-1f53a { - background-position: 7.14286% 78.57143%; } + background-position: 7.14286% 78.57143%; +} .emojione-1f53b { - background-position: 9.52381% 78.57143%; } + background-position: 9.52381% 78.57143%; +} .emojione-1f53c { - background-position: 11.90476% 78.57143%; } + background-position: 11.90476% 78.57143%; +} .emojione-1f53d { - background-position: 14.28571% 78.57143%; } + background-position: 14.28571% 78.57143%; +} .emojione-1f549 { - background-position: 16.66667% 78.57143%; } + background-position: 16.66667% 78.57143%; +} .emojione-1f54a { - background-position: 19.04762% 78.57143%; } + background-position: 19.04762% 78.57143%; +} .emojione-1f54b { - background-position: 21.42857% 78.57143%; } + background-position: 21.42857% 78.57143%; +} .emojione-1f54c { - background-position: 23.80952% 78.57143%; } + background-position: 23.80952% 78.57143%; +} .emojione-1f54d { - background-position: 26.19048% 78.57143%; } + background-position: 26.19048% 78.57143%; +} .emojione-1f54e { - background-position: 28.57143% 78.57143%; } + background-position: 28.57143% 78.57143%; +} .emojione-1f550 { - background-position: 30.95238% 78.57143%; } + background-position: 30.95238% 78.57143%; +} .emojione-1f551 { - background-position: 33.33333% 78.57143%; } + background-position: 33.33333% 78.57143%; +} .emojione-1f552 { - background-position: 35.71429% 78.57143%; } + background-position: 35.71429% 78.57143%; +} .emojione-1f553 { - background-position: 38.09524% 78.57143%; } + background-position: 38.09524% 78.57143%; +} .emojione-1f554 { - background-position: 40.47619% 78.57143%; } + background-position: 40.47619% 78.57143%; +} .emojione-1f555 { - background-position: 42.85714% 78.57143%; } + background-position: 42.85714% 78.57143%; +} .emojione-1f556 { - background-position: 45.2381% 78.57143%; } + background-position: 45.2381% 78.57143%; +} .emojione-1f557 { - background-position: 47.61905% 78.57143%; } + background-position: 47.61905% 78.57143%; +} .emojione-1f558 { - background-position: 50% 78.57143%; } + background-position: 50% 78.57143%; +} .emojione-1f559 { - background-position: 52.38095% 78.57143%; } + background-position: 52.38095% 78.57143%; +} .emojione-1f55a { - background-position: 54.7619% 78.57143%; } + background-position: 54.7619% 78.57143%; +} .emojione-1f55b { - background-position: 57.14286% 78.57143%; } + background-position: 57.14286% 78.57143%; +} .emojione-1f55c { - background-position: 59.52381% 78.57143%; } + background-position: 59.52381% 78.57143%; +} .emojione-1f55d { - background-position: 61.90476% 78.57143%; } + background-position: 61.90476% 78.57143%; +} .emojione-1f55e { - background-position: 64.28571% 78.57143%; } + background-position: 64.28571% 78.57143%; +} .emojione-1f55f { - background-position: 66.66667% 78.57143%; } + background-position: 66.66667% 78.57143%; +} .emojione-1f560 { - background-position: 69.04762% 78.57143%; } + background-position: 69.04762% 78.57143%; +} .emojione-1f561 { - background-position: 71.42857% 78.57143%; } + background-position: 71.42857% 78.57143%; +} .emojione-1f562 { - background-position: 73.80952% 78.57143%; } + background-position: 73.80952% 78.57143%; +} .emojione-1f563 { - background-position: 76.19048% 78.57143%; } + background-position: 76.19048% 78.57143%; +} .emojione-1f564 { - background-position: 78.57143% 78.57143%; } + background-position: 78.57143% 78.57143%; +} .emojione-1f565 { - background-position: 80.95238% 0%; } + background-position: 80.95238% 0%; +} .emojione-1f566 { - background-position: 80.95238% 2.38095%; } + background-position: 80.95238% 2.38095%; +} .emojione-1f567 { - background-position: 80.95238% 4.7619%; } + background-position: 80.95238% 4.7619%; +} .emojione-1f56f { - background-position: 80.95238% 7.14286%; } + background-position: 80.95238% 7.14286%; +} .emojione-1f570 { - background-position: 80.95238% 9.52381%; } + background-position: 80.95238% 9.52381%; +} .emojione-1f573 { - background-position: 80.95238% 11.90476%; } + background-position: 80.95238% 11.90476%; +} .emojione-1f574 { - background-position: 80.95238% 14.28571%; } + background-position: 80.95238% 14.28571%; +} .emojione-1f575-1f3fb { - background-position: 80.95238% 16.66667%; } + background-position: 80.95238% 16.66667%; +} .emojione-1f575-1f3fc { - background-position: 80.95238% 19.04762%; } + background-position: 80.95238% 19.04762%; +} .emojione-1f575-1f3fd { - background-position: 80.95238% 21.42857%; } + background-position: 80.95238% 21.42857%; +} .emojione-1f575-1f3fe { - background-position: 80.95238% 23.80952%; } + background-position: 80.95238% 23.80952%; +} .emojione-1f575-1f3ff { - background-position: 80.95238% 26.19048%; } + background-position: 80.95238% 26.19048%; +} .emojione-1f575 { - background-position: 80.95238% 28.57143%; } + background-position: 80.95238% 28.57143%; +} .emojione-1f576 { - background-position: 80.95238% 30.95238%; } + background-position: 80.95238% 30.95238%; +} .emojione-1f577 { - background-position: 80.95238% 33.33333%; } + background-position: 80.95238% 33.33333%; +} .emojione-1f578 { - background-position: 80.95238% 35.71429%; } + background-position: 80.95238% 35.71429%; +} .emojione-1f579 { - background-position: 80.95238% 38.09524%; } + background-position: 80.95238% 38.09524%; +} .emojione-1f57a-1f3fb { - background-position: 80.95238% 40.47619%; } + background-position: 80.95238% 40.47619%; +} .emojione-1f57a-1f3fc { - background-position: 80.95238% 42.85714%; } + background-position: 80.95238% 42.85714%; +} .emojione-1f57a-1f3fd { - background-position: 80.95238% 45.2381%; } + background-position: 80.95238% 45.2381%; +} .emojione-1f57a-1f3fe { - background-position: 80.95238% 47.61905%; } + background-position: 80.95238% 47.61905%; +} .emojione-1f57a-1f3ff { - background-position: 80.95238% 50%; } + background-position: 80.95238% 50%; +} .emojione-1f57a { - background-position: 80.95238% 52.38095%; } + background-position: 80.95238% 52.38095%; +} .emojione-1f587 { - background-position: 80.95238% 54.7619%; } + background-position: 80.95238% 54.7619%; +} .emojione-1f58a { - background-position: 80.95238% 57.14286%; } + background-position: 80.95238% 57.14286%; +} .emojione-1f58b { - background-position: 80.95238% 59.52381%; } + background-position: 80.95238% 59.52381%; +} .emojione-1f58c { - background-position: 80.95238% 61.90476%; } + background-position: 80.95238% 61.90476%; +} .emojione-1f58d { - background-position: 80.95238% 64.28571%; } + background-position: 80.95238% 64.28571%; +} .emojione-1f590-1f3fb { - background-position: 80.95238% 66.66667%; } + background-position: 80.95238% 66.66667%; +} .emojione-1f590-1f3fc { - background-position: 80.95238% 69.04762%; } + background-position: 80.95238% 69.04762%; +} .emojione-1f590-1f3fd { - background-position: 80.95238% 71.42857%; } + background-position: 80.95238% 71.42857%; +} .emojione-1f590-1f3fe { - background-position: 80.95238% 73.80952%; } + background-position: 80.95238% 73.80952%; +} .emojione-1f590-1f3ff { - background-position: 80.95238% 76.19048%; } + background-position: 80.95238% 76.19048%; +} .emojione-1f590 { - background-position: 80.95238% 78.57143%; } + background-position: 80.95238% 78.57143%; +} .emojione-1f595-1f3fb { - background-position: 0% 80.95238%; } + background-position: 0% 80.95238%; +} .emojione-1f595-1f3fc { - background-position: 2.38095% 80.95238%; } + background-position: 2.38095% 80.95238%; +} .emojione-1f595-1f3fd { - background-position: 4.7619% 80.95238%; } + background-position: 4.7619% 80.95238%; +} .emojione-1f595-1f3fe { - background-position: 7.14286% 80.95238%; } + background-position: 7.14286% 80.95238%; +} .emojione-1f595-1f3ff { - background-position: 9.52381% 80.95238%; } + background-position: 9.52381% 80.95238%; +} .emojione-1f595 { - background-position: 11.90476% 80.95238%; } + background-position: 11.90476% 80.95238%; +} .emojione-1f596-1f3fb { - background-position: 14.28571% 80.95238%; } + background-position: 14.28571% 80.95238%; +} .emojione-1f596-1f3fc { - background-position: 16.66667% 80.95238%; } + background-position: 16.66667% 80.95238%; +} .emojione-1f596-1f3fd { - background-position: 19.04762% 80.95238%; } + background-position: 19.04762% 80.95238%; +} .emojione-1f596-1f3fe { - background-position: 21.42857% 80.95238%; } + background-position: 21.42857% 80.95238%; +} .emojione-1f596-1f3ff { - background-position: 23.80952% 80.95238%; } + background-position: 23.80952% 80.95238%; +} .emojione-1f596 { - background-position: 26.19048% 80.95238%; } + background-position: 26.19048% 80.95238%; +} .emojione-1f5a4 { - background-position: 28.57143% 80.95238%; } + background-position: 28.57143% 80.95238%; +} .emojione-1f5a5 { - background-position: 30.95238% 80.95238%; } + background-position: 30.95238% 80.95238%; +} .emojione-1f5a8 { - background-position: 33.33333% 80.95238%; } + background-position: 33.33333% 80.95238%; +} .emojione-1f5b1 { - background-position: 35.71429% 80.95238%; } + background-position: 35.71429% 80.95238%; +} .emojione-1f5b2 { - background-position: 38.09524% 80.95238%; } + background-position: 38.09524% 80.95238%; +} .emojione-1f5bc { - background-position: 40.47619% 80.95238%; } + background-position: 40.47619% 80.95238%; +} .emojione-1f5c2 { - background-position: 42.85714% 80.95238%; } + background-position: 42.85714% 80.95238%; +} .emojione-1f5c3 { - background-position: 45.2381% 80.95238%; } + background-position: 45.2381% 80.95238%; +} .emojione-1f5c4 { - background-position: 47.61905% 80.95238%; } + background-position: 47.61905% 80.95238%; +} .emojione-1f5d1 { - background-position: 50% 80.95238%; } + background-position: 50% 80.95238%; +} .emojione-1f5d2 { - background-position: 52.38095% 80.95238%; } + background-position: 52.38095% 80.95238%; +} .emojione-1f5d3 { - background-position: 54.7619% 80.95238%; } + background-position: 54.7619% 80.95238%; +} .emojione-1f5dc { - background-position: 57.14286% 80.95238%; } + background-position: 57.14286% 80.95238%; +} .emojione-1f5dd { - background-position: 59.52381% 80.95238%; } + background-position: 59.52381% 80.95238%; +} .emojione-1f5de { - background-position: 61.90476% 80.95238%; } + background-position: 61.90476% 80.95238%; +} .emojione-1f5e1 { - background-position: 64.28571% 80.95238%; } + background-position: 64.28571% 80.95238%; +} .emojione-1f5e3 { - background-position: 66.66667% 80.95238%; } + background-position: 66.66667% 80.95238%; +} .emojione-1f5e8 { - background-position: 69.04762% 80.95238%; } + background-position: 69.04762% 80.95238%; +} .emojione-1f5ef { - background-position: 71.42857% 80.95238%; } + background-position: 71.42857% 80.95238%; +} .emojione-1f5f3 { - background-position: 73.80952% 80.95238%; } + background-position: 73.80952% 80.95238%; +} .emojione-1f5fa { - background-position: 76.19048% 80.95238%; } + background-position: 76.19048% 80.95238%; +} .emojione-1f5fb { - background-position: 78.57143% 80.95238%; } + background-position: 78.57143% 80.95238%; +} .emojione-1f5fc { - background-position: 80.95238% 80.95238%; } + background-position: 80.95238% 80.95238%; +} .emojione-1f5fd { - background-position: 83.33333% 0%; } + background-position: 83.33333% 0%; +} .emojione-1f5fe { - background-position: 83.33333% 2.38095%; } + background-position: 83.33333% 2.38095%; +} .emojione-1f5ff { - background-position: 83.33333% 4.7619%; } + background-position: 83.33333% 4.7619%; +} .emojione-1f600 { - background-position: 83.33333% 7.14286%; } + background-position: 83.33333% 7.14286%; +} .emojione-1f601 { - background-position: 83.33333% 9.52381%; } + background-position: 83.33333% 9.52381%; +} .emojione-1f602 { - background-position: 83.33333% 11.90476%; } + background-position: 83.33333% 11.90476%; +} .emojione-1f603 { - background-position: 83.33333% 14.28571%; } + background-position: 83.33333% 14.28571%; +} .emojione-1f604 { - background-position: 83.33333% 16.66667%; } + background-position: 83.33333% 16.66667%; +} .emojione-1f605 { - background-position: 83.33333% 19.04762%; } + background-position: 83.33333% 19.04762%; +} .emojione-1f606 { - background-position: 83.33333% 21.42857%; } + background-position: 83.33333% 21.42857%; +} .emojione-1f607 { - background-position: 83.33333% 23.80952%; } + background-position: 83.33333% 23.80952%; +} .emojione-1f608 { - background-position: 83.33333% 26.19048%; } + background-position: 83.33333% 26.19048%; +} .emojione-1f609 { - background-position: 83.33333% 28.57143%; } + background-position: 83.33333% 28.57143%; +} .emojione-1f60a { - background-position: 83.33333% 30.95238%; } + background-position: 83.33333% 30.95238%; +} .emojione-1f60b { - background-position: 83.33333% 33.33333%; } + background-position: 83.33333% 33.33333%; +} .emojione-1f60c { - background-position: 83.33333% 35.71429%; } + background-position: 83.33333% 35.71429%; +} .emojione-1f60d { - background-position: 83.33333% 38.09524%; } + background-position: 83.33333% 38.09524%; +} .emojione-1f60e { - background-position: 83.33333% 40.47619%; } + background-position: 83.33333% 40.47619%; +} .emojione-1f60f { - background-position: 83.33333% 42.85714%; } + background-position: 83.33333% 42.85714%; +} .emojione-1f610 { - background-position: 83.33333% 45.2381%; } + background-position: 83.33333% 45.2381%; +} .emojione-1f611 { - background-position: 83.33333% 47.61905%; } + background-position: 83.33333% 47.61905%; +} .emojione-1f612 { - background-position: 83.33333% 50%; } + background-position: 83.33333% 50%; +} .emojione-1f613 { - background-position: 83.33333% 52.38095%; } + background-position: 83.33333% 52.38095%; +} .emojione-1f614 { - background-position: 83.33333% 54.7619%; } + background-position: 83.33333% 54.7619%; +} .emojione-1f615 { - background-position: 83.33333% 57.14286%; } + background-position: 83.33333% 57.14286%; +} .emojione-1f616 { - background-position: 83.33333% 59.52381%; } + background-position: 83.33333% 59.52381%; +} .emojione-1f617 { - background-position: 83.33333% 61.90476%; } + background-position: 83.33333% 61.90476%; +} .emojione-1f618 { - background-position: 83.33333% 64.28571%; } + background-position: 83.33333% 64.28571%; +} .emojione-1f619 { - background-position: 83.33333% 66.66667%; } + background-position: 83.33333% 66.66667%; +} .emojione-1f61a { - background-position: 83.33333% 69.04762%; } + background-position: 83.33333% 69.04762%; +} .emojione-1f61b { - background-position: 83.33333% 71.42857%; } + background-position: 83.33333% 71.42857%; +} .emojione-1f61c { - background-position: 83.33333% 73.80952%; } + background-position: 83.33333% 73.80952%; +} .emojione-1f61d { - background-position: 83.33333% 76.19048%; } + background-position: 83.33333% 76.19048%; +} .emojione-1f61e { - background-position: 83.33333% 78.57143%; } + background-position: 83.33333% 78.57143%; +} .emojione-1f61f { - background-position: 83.33333% 80.95238%; } + background-position: 83.33333% 80.95238%; +} .emojione-1f620 { - background-position: 0% 83.33333%; } + background-position: 0% 83.33333%; +} .emojione-1f621 { - background-position: 2.38095% 83.33333%; } + background-position: 2.38095% 83.33333%; +} .emojione-1f622 { - background-position: 4.7619% 83.33333%; } + background-position: 4.7619% 83.33333%; +} .emojione-1f623 { - background-position: 7.14286% 83.33333%; } + background-position: 7.14286% 83.33333%; +} .emojione-1f624 { - background-position: 9.52381% 83.33333%; } + background-position: 9.52381% 83.33333%; +} .emojione-1f625 { - background-position: 11.90476% 83.33333%; } + background-position: 11.90476% 83.33333%; +} .emojione-1f626 { - background-position: 14.28571% 83.33333%; } + background-position: 14.28571% 83.33333%; +} .emojione-1f627 { - background-position: 16.66667% 83.33333%; } + background-position: 16.66667% 83.33333%; +} .emojione-1f628 { - background-position: 19.04762% 83.33333%; } + background-position: 19.04762% 83.33333%; +} .emojione-1f629 { - background-position: 21.42857% 83.33333%; } + background-position: 21.42857% 83.33333%; +} .emojione-1f62a { - background-position: 23.80952% 83.33333%; } + background-position: 23.80952% 83.33333%; +} .emojione-1f62b { - background-position: 26.19048% 83.33333%; } + background-position: 26.19048% 83.33333%; +} .emojione-1f62c { - background-position: 28.57143% 83.33333%; } + background-position: 28.57143% 83.33333%; +} .emojione-1f62d { - background-position: 30.95238% 83.33333%; } + background-position: 30.95238% 83.33333%; +} .emojione-1f62e { - background-position: 33.33333% 83.33333%; } + background-position: 33.33333% 83.33333%; +} .emojione-1f62f { - background-position: 35.71429% 83.33333%; } + background-position: 35.71429% 83.33333%; +} .emojione-1f630 { - background-position: 38.09524% 83.33333%; } + background-position: 38.09524% 83.33333%; +} .emojione-1f631 { - background-position: 40.47619% 83.33333%; } + background-position: 40.47619% 83.33333%; +} .emojione-1f632 { - background-position: 42.85714% 83.33333%; } + background-position: 42.85714% 83.33333%; +} .emojione-1f633 { - background-position: 45.2381% 83.33333%; } + background-position: 45.2381% 83.33333%; +} .emojione-1f634 { - background-position: 47.61905% 83.33333%; } + background-position: 47.61905% 83.33333%; +} .emojione-1f635 { - background-position: 50% 83.33333%; } + background-position: 50% 83.33333%; +} .emojione-1f636 { - background-position: 52.38095% 83.33333%; } + background-position: 52.38095% 83.33333%; +} .emojione-1f637 { - background-position: 54.7619% 83.33333%; } + background-position: 54.7619% 83.33333%; +} .emojione-1f638 { - background-position: 57.14286% 83.33333%; } + background-position: 57.14286% 83.33333%; +} .emojione-1f639 { - background-position: 59.52381% 83.33333%; } + background-position: 59.52381% 83.33333%; +} .emojione-1f63a { - background-position: 61.90476% 83.33333%; } + background-position: 61.90476% 83.33333%; +} .emojione-1f63b { - background-position: 64.28571% 83.33333%; } + background-position: 64.28571% 83.33333%; +} .emojione-1f63c { - background-position: 66.66667% 83.33333%; } + background-position: 66.66667% 83.33333%; +} .emojione-1f63d { - background-position: 69.04762% 83.33333%; } + background-position: 69.04762% 83.33333%; +} .emojione-1f63e { - background-position: 71.42857% 83.33333%; } + background-position: 71.42857% 83.33333%; +} .emojione-1f63f { - background-position: 73.80952% 83.33333%; } + background-position: 73.80952% 83.33333%; +} .emojione-1f640 { - background-position: 76.19048% 83.33333%; } + background-position: 76.19048% 83.33333%; +} .emojione-1f641 { - background-position: 78.57143% 83.33333%; } + background-position: 78.57143% 83.33333%; +} .emojione-1f642 { - background-position: 80.95238% 83.33333%; } + background-position: 80.95238% 83.33333%; +} .emojione-1f643 { - background-position: 83.33333% 83.33333%; } + background-position: 83.33333% 83.33333%; +} .emojione-1f644 { - background-position: 85.71429% 0%; } + background-position: 85.71429% 0%; +} .emojione-1f645-1f3fb { - background-position: 85.71429% 2.38095%; } + background-position: 85.71429% 2.38095%; +} .emojione-1f645-1f3fc { - background-position: 85.71429% 4.7619%; } + background-position: 85.71429% 4.7619%; +} .emojione-1f645-1f3fd { - background-position: 85.71429% 7.14286%; } + background-position: 85.71429% 7.14286%; +} .emojione-1f645-1f3fe { - background-position: 85.71429% 9.52381%; } + background-position: 85.71429% 9.52381%; +} .emojione-1f645-1f3ff { - background-position: 85.71429% 11.90476%; } + background-position: 85.71429% 11.90476%; +} .emojione-1f645 { - background-position: 85.71429% 14.28571%; } + background-position: 85.71429% 14.28571%; +} .emojione-1f646-1f3fb { - background-position: 85.71429% 16.66667%; } + background-position: 85.71429% 16.66667%; +} .emojione-1f646-1f3fc { - background-position: 85.71429% 19.04762%; } + background-position: 85.71429% 19.04762%; +} .emojione-1f646-1f3fd { - background-position: 85.71429% 21.42857%; } + background-position: 85.71429% 21.42857%; +} .emojione-1f646-1f3fe { - background-position: 85.71429% 23.80952%; } + background-position: 85.71429% 23.80952%; +} .emojione-1f646-1f3ff { - background-position: 85.71429% 26.19048%; } + background-position: 85.71429% 26.19048%; +} .emojione-1f646 { - background-position: 85.71429% 28.57143%; } + background-position: 85.71429% 28.57143%; +} .emojione-1f647-1f3fb { - background-position: 85.71429% 30.95238%; } + background-position: 85.71429% 30.95238%; +} .emojione-1f647-1f3fc { - background-position: 85.71429% 33.33333%; } + background-position: 85.71429% 33.33333%; +} .emojione-1f647-1f3fd { - background-position: 85.71429% 35.71429%; } + background-position: 85.71429% 35.71429%; +} .emojione-1f647-1f3fe { - background-position: 85.71429% 38.09524%; } + background-position: 85.71429% 38.09524%; +} .emojione-1f647-1f3ff { - background-position: 85.71429% 40.47619%; } + background-position: 85.71429% 40.47619%; +} .emojione-1f647 { - background-position: 85.71429% 42.85714%; } + background-position: 85.71429% 42.85714%; +} .emojione-1f648 { - background-position: 85.71429% 45.2381%; } + background-position: 85.71429% 45.2381%; +} .emojione-1f649 { - background-position: 85.71429% 47.61905%; } + background-position: 85.71429% 47.61905%; +} .emojione-1f64a { - background-position: 85.71429% 50%; } + background-position: 85.71429% 50%; +} .emojione-1f64b-1f3fb { - background-position: 85.71429% 52.38095%; } + background-position: 85.71429% 52.38095%; +} .emojione-1f64b-1f3fc { - background-position: 85.71429% 54.7619%; } + background-position: 85.71429% 54.7619%; +} .emojione-1f64b-1f3fd { - background-position: 85.71429% 57.14286%; } + background-position: 85.71429% 57.14286%; +} .emojione-1f64b-1f3fe { - background-position: 85.71429% 59.52381%; } + background-position: 85.71429% 59.52381%; +} .emojione-1f64b-1f3ff { - background-position: 85.71429% 61.90476%; } + background-position: 85.71429% 61.90476%; +} .emojione-1f64b { - background-position: 85.71429% 64.28571%; } + background-position: 85.71429% 64.28571%; +} .emojione-1f64c-1f3fb { - background-position: 85.71429% 66.66667%; } + background-position: 85.71429% 66.66667%; +} .emojione-1f64c-1f3fc { - background-position: 85.71429% 69.04762%; } + background-position: 85.71429% 69.04762%; +} .emojione-1f64c-1f3fd { - background-position: 85.71429% 71.42857%; } + background-position: 85.71429% 71.42857%; +} .emojione-1f64c-1f3fe { - background-position: 85.71429% 73.80952%; } + background-position: 85.71429% 73.80952%; +} .emojione-1f64c-1f3ff { - background-position: 85.71429% 76.19048%; } + background-position: 85.71429% 76.19048%; +} .emojione-1f64c { - background-position: 85.71429% 78.57143%; } + background-position: 85.71429% 78.57143%; +} .emojione-1f64d-1f3fb { - background-position: 85.71429% 80.95238%; } + background-position: 85.71429% 80.95238%; +} .emojione-1f64d-1f3fc { - background-position: 85.71429% 83.33333%; } + background-position: 85.71429% 83.33333%; +} .emojione-1f64d-1f3fd { - background-position: 0% 85.71429%; } + background-position: 0% 85.71429%; +} .emojione-1f64d-1f3fe { - background-position: 2.38095% 85.71429%; } + background-position: 2.38095% 85.71429%; +} .emojione-1f64d-1f3ff { - background-position: 4.7619% 85.71429%; } + background-position: 4.7619% 85.71429%; +} .emojione-1f64d { - background-position: 7.14286% 85.71429%; } + background-position: 7.14286% 85.71429%; +} .emojione-1f64e-1f3fb { - background-position: 9.52381% 85.71429%; } + background-position: 9.52381% 85.71429%; +} .emojione-1f64e-1f3fc { - background-position: 11.90476% 85.71429%; } + background-position: 11.90476% 85.71429%; +} .emojione-1f64e-1f3fd { - background-position: 14.28571% 85.71429%; } + background-position: 14.28571% 85.71429%; +} .emojione-1f64e-1f3fe { - background-position: 16.66667% 85.71429%; } + background-position: 16.66667% 85.71429%; +} .emojione-1f64e-1f3ff { - background-position: 19.04762% 85.71429%; } + background-position: 19.04762% 85.71429%; +} .emojione-1f64e { - background-position: 21.42857% 85.71429%; } + background-position: 21.42857% 85.71429%; +} .emojione-1f64f-1f3fb { - background-position: 23.80952% 85.71429%; } + background-position: 23.80952% 85.71429%; +} .emojione-1f64f-1f3fc { - background-position: 26.19048% 85.71429%; } + background-position: 26.19048% 85.71429%; +} .emojione-1f64f-1f3fd { - background-position: 28.57143% 85.71429%; } + background-position: 28.57143% 85.71429%; +} .emojione-1f64f-1f3fe { - background-position: 30.95238% 85.71429%; } + background-position: 30.95238% 85.71429%; +} .emojione-1f64f-1f3ff { - background-position: 33.33333% 85.71429%; } + background-position: 33.33333% 85.71429%; +} .emojione-1f64f { - background-position: 35.71429% 85.71429%; } + background-position: 35.71429% 85.71429%; +} .emojione-1f680 { - background-position: 38.09524% 85.71429%; } + background-position: 38.09524% 85.71429%; +} .emojione-1f681 { - background-position: 40.47619% 85.71429%; } + background-position: 40.47619% 85.71429%; +} .emojione-1f682 { - background-position: 42.85714% 85.71429%; } + background-position: 42.85714% 85.71429%; +} .emojione-1f683 { - background-position: 45.2381% 85.71429%; } + background-position: 45.2381% 85.71429%; +} .emojione-1f684 { - background-position: 47.61905% 85.71429%; } + background-position: 47.61905% 85.71429%; +} .emojione-1f685 { - background-position: 50% 85.71429%; } + background-position: 50% 85.71429%; +} .emojione-1f686 { - background-position: 52.38095% 85.71429%; } + background-position: 52.38095% 85.71429%; +} .emojione-1f687 { - background-position: 54.7619% 85.71429%; } + background-position: 54.7619% 85.71429%; +} .emojione-1f688 { - background-position: 57.14286% 85.71429%; } + background-position: 57.14286% 85.71429%; +} .emojione-1f689 { - background-position: 59.52381% 85.71429%; } + background-position: 59.52381% 85.71429%; +} .emojione-1f68a { - background-position: 61.90476% 85.71429%; } + background-position: 61.90476% 85.71429%; +} .emojione-1f68b { - background-position: 64.28571% 85.71429%; } + background-position: 64.28571% 85.71429%; +} .emojione-1f68c { - background-position: 66.66667% 85.71429%; } + background-position: 66.66667% 85.71429%; +} .emojione-1f68d { - background-position: 69.04762% 85.71429%; } + background-position: 69.04762% 85.71429%; +} .emojione-1f68e { - background-position: 71.42857% 85.71429%; } + background-position: 71.42857% 85.71429%; +} .emojione-1f68f { - background-position: 73.80952% 85.71429%; } + background-position: 73.80952% 85.71429%; +} .emojione-1f690 { - background-position: 76.19048% 85.71429%; } + background-position: 76.19048% 85.71429%; +} .emojione-1f691 { - background-position: 78.57143% 85.71429%; } + background-position: 78.57143% 85.71429%; +} .emojione-1f692 { - background-position: 80.95238% 85.71429%; } + background-position: 80.95238% 85.71429%; +} .emojione-1f693 { - background-position: 83.33333% 85.71429%; } + background-position: 83.33333% 85.71429%; +} .emojione-1f694 { - background-position: 85.71429% 85.71429%; } + background-position: 85.71429% 85.71429%; +} .emojione-1f695 { - background-position: 88.09524% 0%; } + background-position: 88.09524% 0%; +} .emojione-1f696 { - background-position: 88.09524% 2.38095%; } + background-position: 88.09524% 2.38095%; +} .emojione-1f697 { - background-position: 88.09524% 4.7619%; } + background-position: 88.09524% 4.7619%; +} .emojione-1f698 { - background-position: 88.09524% 7.14286%; } + background-position: 88.09524% 7.14286%; +} .emojione-1f699 { - background-position: 88.09524% 9.52381%; } + background-position: 88.09524% 9.52381%; +} .emojione-1f69a { - background-position: 88.09524% 11.90476%; } + background-position: 88.09524% 11.90476%; +} .emojione-1f69b { - background-position: 88.09524% 14.28571%; } + background-position: 88.09524% 14.28571%; +} .emojione-1f69c { - background-position: 88.09524% 16.66667%; } + background-position: 88.09524% 16.66667%; +} .emojione-1f69d { - background-position: 88.09524% 19.04762%; } + background-position: 88.09524% 19.04762%; +} .emojione-1f69e { - background-position: 88.09524% 21.42857%; } + background-position: 88.09524% 21.42857%; +} .emojione-1f69f { - background-position: 88.09524% 23.80952%; } + background-position: 88.09524% 23.80952%; +} .emojione-1f6a0 { - background-position: 88.09524% 26.19048%; } + background-position: 88.09524% 26.19048%; +} .emojione-1f6a1 { - background-position: 88.09524% 28.57143%; } + background-position: 88.09524% 28.57143%; +} .emojione-1f6a2 { - background-position: 88.09524% 30.95238%; } + background-position: 88.09524% 30.95238%; +} .emojione-1f6a3-1f3fb { - background-position: 88.09524% 33.33333%; } + background-position: 88.09524% 33.33333%; +} .emojione-1f6a3-1f3fc { - background-position: 88.09524% 35.71429%; } + background-position: 88.09524% 35.71429%; +} .emojione-1f6a3-1f3fd { - background-position: 88.09524% 38.09524%; } + background-position: 88.09524% 38.09524%; +} .emojione-1f6a3-1f3fe { - background-position: 88.09524% 40.47619%; } + background-position: 88.09524% 40.47619%; +} .emojione-1f6a3-1f3ff { - background-position: 88.09524% 42.85714%; } + background-position: 88.09524% 42.85714%; +} .emojione-1f6a3 { - background-position: 88.09524% 45.2381%; } + background-position: 88.09524% 45.2381%; +} .emojione-1f6a4 { - background-position: 88.09524% 47.61905%; } + background-position: 88.09524% 47.61905%; +} .emojione-1f6a5 { - background-position: 88.09524% 50%; } + background-position: 88.09524% 50%; +} .emojione-1f6a6 { - background-position: 88.09524% 52.38095%; } + background-position: 88.09524% 52.38095%; +} .emojione-1f6a7 { - background-position: 88.09524% 54.7619%; } + background-position: 88.09524% 54.7619%; +} .emojione-1f6a8 { - background-position: 88.09524% 57.14286%; } + background-position: 88.09524% 57.14286%; +} .emojione-1f6a9 { - background-position: 88.09524% 59.52381%; } + background-position: 88.09524% 59.52381%; +} .emojione-1f6aa { - background-position: 88.09524% 61.90476%; } + background-position: 88.09524% 61.90476%; +} .emojione-1f6ab { - background-position: 88.09524% 64.28571%; } + background-position: 88.09524% 64.28571%; +} .emojione-1f6ac { - background-position: 88.09524% 66.66667%; } + background-position: 88.09524% 66.66667%; +} .emojione-1f6ad { - background-position: 88.09524% 69.04762%; } + background-position: 88.09524% 69.04762%; +} .emojione-1f6ae { - background-position: 88.09524% 71.42857%; } + background-position: 88.09524% 71.42857%; +} .emojione-1f6af { - background-position: 88.09524% 73.80952%; } + background-position: 88.09524% 73.80952%; +} .emojione-1f6b0 { - background-position: 88.09524% 76.19048%; } + background-position: 88.09524% 76.19048%; +} .emojione-1f6b1 { - background-position: 88.09524% 78.57143%; } + background-position: 88.09524% 78.57143%; +} .emojione-1f6b2 { - background-position: 88.09524% 80.95238%; } + background-position: 88.09524% 80.95238%; +} .emojione-1f6b3 { - background-position: 88.09524% 83.33333%; } + background-position: 88.09524% 83.33333%; +} .emojione-1f6b4-1f3fb { - background-position: 88.09524% 85.71429%; } + background-position: 88.09524% 85.71429%; +} .emojione-1f6b4-1f3fc { - background-position: 0% 88.09524%; } + background-position: 0% 88.09524%; +} .emojione-1f6b4-1f3fd { - background-position: 2.38095% 88.09524%; } + background-position: 2.38095% 88.09524%; +} .emojione-1f6b4-1f3fe { - background-position: 4.7619% 88.09524%; } + background-position: 4.7619% 88.09524%; +} .emojione-1f6b4-1f3ff { - background-position: 7.14286% 88.09524%; } + background-position: 7.14286% 88.09524%; +} .emojione-1f6b4 { - background-position: 9.52381% 88.09524%; } + background-position: 9.52381% 88.09524%; +} .emojione-1f6b5-1f3fb { - background-position: 11.90476% 88.09524%; } + background-position: 11.90476% 88.09524%; +} .emojione-1f6b5-1f3fc { - background-position: 14.28571% 88.09524%; } + background-position: 14.28571% 88.09524%; +} .emojione-1f6b5-1f3fd { - background-position: 16.66667% 88.09524%; } + background-position: 16.66667% 88.09524%; +} .emojione-1f6b5-1f3fe { - background-position: 19.04762% 88.09524%; } + background-position: 19.04762% 88.09524%; +} .emojione-1f6b5-1f3ff { - background-position: 21.42857% 88.09524%; } + background-position: 21.42857% 88.09524%; +} .emojione-1f6b5 { - background-position: 23.80952% 88.09524%; } + background-position: 23.80952% 88.09524%; +} .emojione-1f6b6-1f3fb { - background-position: 26.19048% 88.09524%; } + background-position: 26.19048% 88.09524%; +} .emojione-1f6b6-1f3fc { - background-position: 28.57143% 88.09524%; } + background-position: 28.57143% 88.09524%; +} .emojione-1f6b6-1f3fd { - background-position: 30.95238% 88.09524%; } + background-position: 30.95238% 88.09524%; +} .emojione-1f6b6-1f3fe { - background-position: 33.33333% 88.09524%; } + background-position: 33.33333% 88.09524%; +} .emojione-1f6b6-1f3ff { - background-position: 35.71429% 88.09524%; } + background-position: 35.71429% 88.09524%; +} .emojione-1f6b6 { - background-position: 38.09524% 88.09524%; } + background-position: 38.09524% 88.09524%; +} .emojione-1f6b7 { - background-position: 40.47619% 88.09524%; } + background-position: 40.47619% 88.09524%; +} .emojione-1f6b8 { - background-position: 42.85714% 88.09524%; } + background-position: 42.85714% 88.09524%; +} .emojione-1f6b9 { - background-position: 45.2381% 88.09524%; } + background-position: 45.2381% 88.09524%; +} .emojione-1f6ba { - background-position: 47.61905% 88.09524%; } + background-position: 47.61905% 88.09524%; +} .emojione-1f6bb { - background-position: 50% 88.09524%; } + background-position: 50% 88.09524%; +} .emojione-1f6bc { - background-position: 52.38095% 88.09524%; } + background-position: 52.38095% 88.09524%; +} .emojione-1f6bd { - background-position: 54.7619% 88.09524%; } + background-position: 54.7619% 88.09524%; +} .emojione-1f6be { - background-position: 57.14286% 88.09524%; } + background-position: 57.14286% 88.09524%; +} .emojione-1f6bf { - background-position: 59.52381% 88.09524%; } + background-position: 59.52381% 88.09524%; +} .emojione-1f6c0-1f3fb { - background-position: 61.90476% 88.09524%; } + background-position: 61.90476% 88.09524%; +} .emojione-1f6c0-1f3fc { - background-position: 64.28571% 88.09524%; } + background-position: 64.28571% 88.09524%; +} .emojione-1f6c0-1f3fd { - background-position: 66.66667% 88.09524%; } + background-position: 66.66667% 88.09524%; +} .emojione-1f6c0-1f3fe { - background-position: 69.04762% 88.09524%; } + background-position: 69.04762% 88.09524%; +} .emojione-1f6c0-1f3ff { - background-position: 71.42857% 88.09524%; } + background-position: 71.42857% 88.09524%; +} .emojione-1f6c0 { - background-position: 73.80952% 88.09524%; } + background-position: 73.80952% 88.09524%; +} .emojione-1f6c1 { - background-position: 76.19048% 88.09524%; } + background-position: 76.19048% 88.09524%; +} .emojione-1f6c2 { - background-position: 78.57143% 88.09524%; } + background-position: 78.57143% 88.09524%; +} .emojione-1f6c3 { - background-position: 80.95238% 88.09524%; } + background-position: 80.95238% 88.09524%; +} .emojione-1f6c4 { - background-position: 83.33333% 88.09524%; } + background-position: 83.33333% 88.09524%; +} .emojione-1f6c5 { - background-position: 85.71429% 88.09524%; } + background-position: 85.71429% 88.09524%; +} .emojione-1f6cb { - background-position: 88.09524% 88.09524%; } + background-position: 88.09524% 88.09524%; +} .emojione-1f6cc { - background-position: 90.47619% 0%; } + background-position: 90.47619% 0%; +} .emojione-1f6cd { - background-position: 90.47619% 2.38095%; } + background-position: 90.47619% 2.38095%; +} .emojione-1f6ce { - background-position: 90.47619% 4.7619%; } + background-position: 90.47619% 4.7619%; +} .emojione-1f6cf { - background-position: 90.47619% 7.14286%; } + background-position: 90.47619% 7.14286%; +} .emojione-1f6d0 { - background-position: 90.47619% 9.52381%; } + background-position: 90.47619% 9.52381%; +} .emojione-1f6d1 { - background-position: 90.47619% 11.90476%; } + background-position: 90.47619% 11.90476%; +} .emojione-1f6d2 { - background-position: 90.47619% 14.28571%; } + background-position: 90.47619% 14.28571%; +} .emojione-1f6e0 { - background-position: 90.47619% 16.66667%; } + background-position: 90.47619% 16.66667%; +} .emojione-1f6e1 { - background-position: 90.47619% 19.04762%; } + background-position: 90.47619% 19.04762%; +} .emojione-1f6e2 { - background-position: 90.47619% 21.42857%; } + background-position: 90.47619% 21.42857%; +} .emojione-1f6e3 { - background-position: 90.47619% 23.80952%; } + background-position: 90.47619% 23.80952%; +} .emojione-1f6e4 { - background-position: 90.47619% 26.19048%; } + background-position: 90.47619% 26.19048%; +} .emojione-1f6e5 { - background-position: 90.47619% 28.57143%; } + background-position: 90.47619% 28.57143%; +} .emojione-1f6e9 { - background-position: 90.47619% 30.95238%; } + background-position: 90.47619% 30.95238%; +} .emojione-1f6eb { - background-position: 90.47619% 33.33333%; } + background-position: 90.47619% 33.33333%; +} .emojione-1f6ec { - background-position: 90.47619% 35.71429%; } + background-position: 90.47619% 35.71429%; +} .emojione-1f6f0 { - background-position: 90.47619% 38.09524%; } + background-position: 90.47619% 38.09524%; +} .emojione-1f6f3 { - background-position: 90.47619% 40.47619%; } + background-position: 90.47619% 40.47619%; +} .emojione-1f6f4 { - background-position: 90.47619% 42.85714%; } + background-position: 90.47619% 42.85714%; +} .emojione-1f6f5 { - background-position: 90.47619% 45.2381%; } + background-position: 90.47619% 45.2381%; +} .emojione-1f6f6 { - background-position: 90.47619% 47.61905%; } + background-position: 90.47619% 47.61905%; +} .emojione-1f910 { - background-position: 90.47619% 50%; } + background-position: 90.47619% 50%; +} .emojione-1f911 { - background-position: 90.47619% 52.38095%; } + background-position: 90.47619% 52.38095%; +} .emojione-1f912 { - background-position: 90.47619% 54.7619%; } + background-position: 90.47619% 54.7619%; +} .emojione-1f913 { - background-position: 90.47619% 57.14286%; } + background-position: 90.47619% 57.14286%; +} .emojione-1f914 { - background-position: 90.47619% 59.52381%; } + background-position: 90.47619% 59.52381%; +} .emojione-1f915 { - background-position: 90.47619% 61.90476%; } + background-position: 90.47619% 61.90476%; +} .emojione-1f916 { - background-position: 90.47619% 64.28571%; } + background-position: 90.47619% 64.28571%; +} .emojione-1f917 { - background-position: 90.47619% 66.66667%; } + background-position: 90.47619% 66.66667%; +} .emojione-1f918-1f3fb { - background-position: 90.47619% 69.04762%; } + background-position: 90.47619% 69.04762%; +} .emojione-1f918-1f3fc { - background-position: 90.47619% 71.42857%; } + background-position: 90.47619% 71.42857%; +} .emojione-1f918-1f3fd { - background-position: 90.47619% 73.80952%; } + background-position: 90.47619% 73.80952%; +} .emojione-1f918-1f3fe { - background-position: 90.47619% 76.19048%; } + background-position: 90.47619% 76.19048%; +} .emojione-1f918-1f3ff { - background-position: 90.47619% 78.57143%; } + background-position: 90.47619% 78.57143%; +} .emojione-1f918 { - background-position: 90.47619% 80.95238%; } + background-position: 90.47619% 80.95238%; +} .emojione-1f919-1f3fb { - background-position: 90.47619% 83.33333%; } + background-position: 90.47619% 83.33333%; +} .emojione-1f919-1f3fc { - background-position: 90.47619% 85.71429%; } + background-position: 90.47619% 85.71429%; +} .emojione-1f919-1f3fd { - background-position: 90.47619% 88.09524%; } + background-position: 90.47619% 88.09524%; +} .emojione-1f919-1f3fe { - background-position: 0% 90.47619%; } + background-position: 0% 90.47619%; +} .emojione-1f919-1f3ff { - background-position: 2.38095% 90.47619%; } + background-position: 2.38095% 90.47619%; +} .emojione-1f919 { - background-position: 4.7619% 90.47619%; } + background-position: 4.7619% 90.47619%; +} .emojione-1f91a-1f3fb { - background-position: 7.14286% 90.47619%; } + background-position: 7.14286% 90.47619%; +} .emojione-1f91a-1f3fc { - background-position: 9.52381% 90.47619%; } + background-position: 9.52381% 90.47619%; +} .emojione-1f91a-1f3fd { - background-position: 11.90476% 90.47619%; } + background-position: 11.90476% 90.47619%; +} .emojione-1f91a-1f3fe { - background-position: 14.28571% 90.47619%; } + background-position: 14.28571% 90.47619%; +} .emojione-1f91a-1f3ff { - background-position: 16.66667% 90.47619%; } + background-position: 16.66667% 90.47619%; +} .emojione-1f91a { - background-position: 19.04762% 90.47619%; } + background-position: 19.04762% 90.47619%; +} .emojione-1f91b-1f3fb { - background-position: 21.42857% 90.47619%; } + background-position: 21.42857% 90.47619%; +} .emojione-1f91b-1f3fc { - background-position: 23.80952% 90.47619%; } + background-position: 23.80952% 90.47619%; +} .emojione-1f91b-1f3fd { - background-position: 26.19048% 90.47619%; } + background-position: 26.19048% 90.47619%; +} .emojione-1f91b-1f3fe { - background-position: 28.57143% 90.47619%; } + background-position: 28.57143% 90.47619%; +} .emojione-1f91b-1f3ff { - background-position: 30.95238% 90.47619%; } + background-position: 30.95238% 90.47619%; +} .emojione-1f91b { - background-position: 33.33333% 90.47619%; } + background-position: 33.33333% 90.47619%; +} .emojione-1f91c-1f3fb { - background-position: 35.71429% 90.47619%; } + background-position: 35.71429% 90.47619%; +} .emojione-1f91c-1f3fc { - background-position: 38.09524% 90.47619%; } + background-position: 38.09524% 90.47619%; +} .emojione-1f91c-1f3fd { - background-position: 40.47619% 90.47619%; } + background-position: 40.47619% 90.47619%; +} .emojione-1f91c-1f3fe { - background-position: 42.85714% 90.47619%; } + background-position: 42.85714% 90.47619%; +} .emojione-1f91c-1f3ff { - background-position: 45.2381% 90.47619%; } + background-position: 45.2381% 90.47619%; +} .emojione-1f91c { - background-position: 47.61905% 90.47619%; } + background-position: 47.61905% 90.47619%; +} .emojione-1f91d-1f3fb { - background-position: 50% 90.47619%; } + background-position: 50% 90.47619%; +} .emojione-1f91d-1f3fc { - background-position: 52.38095% 90.47619%; } + background-position: 52.38095% 90.47619%; +} .emojione-1f91d-1f3fd { - background-position: 54.7619% 90.47619%; } + background-position: 54.7619% 90.47619%; +} .emojione-1f91d-1f3fe { - background-position: 57.14286% 90.47619%; } + background-position: 57.14286% 90.47619%; +} .emojione-1f91d-1f3ff { - background-position: 59.52381% 90.47619%; } + background-position: 59.52381% 90.47619%; +} .emojione-1f91d { - background-position: 61.90476% 90.47619%; } + background-position: 61.90476% 90.47619%; +} .emojione-1f91e-1f3fb { - background-position: 64.28571% 90.47619%; } + background-position: 64.28571% 90.47619%; +} .emojione-1f91e-1f3fc { - background-position: 66.66667% 90.47619%; } + background-position: 66.66667% 90.47619%; +} .emojione-1f91e-1f3fd { - background-position: 69.04762% 90.47619%; } + background-position: 69.04762% 90.47619%; +} .emojione-1f91e-1f3fe { - background-position: 71.42857% 90.47619%; } + background-position: 71.42857% 90.47619%; +} .emojione-1f91e-1f3ff { - background-position: 73.80952% 90.47619%; } + background-position: 73.80952% 90.47619%; +} .emojione-1f91e { - background-position: 76.19048% 90.47619%; } + background-position: 76.19048% 90.47619%; +} .emojione-1f920 { - background-position: 78.57143% 90.47619%; } + background-position: 78.57143% 90.47619%; +} .emojione-1f921 { - background-position: 80.95238% 90.47619%; } + background-position: 80.95238% 90.47619%; +} .emojione-1f922 { - background-position: 83.33333% 90.47619%; } + background-position: 83.33333% 90.47619%; +} .emojione-1f923 { - background-position: 85.71429% 90.47619%; } + background-position: 85.71429% 90.47619%; +} .emojione-1f924 { - background-position: 88.09524% 90.47619%; } + background-position: 88.09524% 90.47619%; +} .emojione-1f925 { - background-position: 90.47619% 90.47619%; } + background-position: 90.47619% 90.47619%; +} .emojione-1f926-1f3fb { - background-position: 92.85714% 0%; } + background-position: 92.85714% 0%; +} .emojione-1f926-1f3fc { - background-position: 92.85714% 2.38095%; } + background-position: 92.85714% 2.38095%; +} .emojione-1f926-1f3fd { - background-position: 92.85714% 4.7619%; } + background-position: 92.85714% 4.7619%; +} .emojione-1f926-1f3fe { - background-position: 92.85714% 7.14286%; } + background-position: 92.85714% 7.14286%; +} .emojione-1f926-1f3ff { - background-position: 92.85714% 9.52381%; } + background-position: 92.85714% 9.52381%; +} .emojione-1f926 { - background-position: 92.85714% 11.90476%; } + background-position: 92.85714% 11.90476%; +} .emojione-1f927 { - background-position: 92.85714% 14.28571%; } + background-position: 92.85714% 14.28571%; +} .emojione-1f930-1f3fb { - background-position: 92.85714% 16.66667%; } + background-position: 92.85714% 16.66667%; +} .emojione-1f930-1f3fc { - background-position: 92.85714% 19.04762%; } + background-position: 92.85714% 19.04762%; +} .emojione-1f930-1f3fd { - background-position: 92.85714% 21.42857%; } + background-position: 92.85714% 21.42857%; +} .emojione-1f930-1f3fe { - background-position: 92.85714% 23.80952%; } + background-position: 92.85714% 23.80952%; +} .emojione-1f930-1f3ff { - background-position: 92.85714% 26.19048%; } + background-position: 92.85714% 26.19048%; +} .emojione-1f930 { - background-position: 92.85714% 28.57143%; } + background-position: 92.85714% 28.57143%; +} .emojione-1f933-1f3fb { - background-position: 92.85714% 30.95238%; } + background-position: 92.85714% 30.95238%; +} .emojione-1f933-1f3fc { - background-position: 92.85714% 33.33333%; } + background-position: 92.85714% 33.33333%; +} .emojione-1f933-1f3fd { - background-position: 92.85714% 35.71429%; } + background-position: 92.85714% 35.71429%; +} .emojione-1f933-1f3fe { - background-position: 92.85714% 38.09524%; } + background-position: 92.85714% 38.09524%; +} .emojione-1f933-1f3ff { - background-position: 92.85714% 40.47619%; } + background-position: 92.85714% 40.47619%; +} .emojione-1f933 { - background-position: 92.85714% 42.85714%; } + background-position: 92.85714% 42.85714%; +} .emojione-1f934-1f3fb { - background-position: 92.85714% 45.2381%; } + background-position: 92.85714% 45.2381%; +} .emojione-1f934-1f3fc { - background-position: 92.85714% 47.61905%; } + background-position: 92.85714% 47.61905%; +} .emojione-1f934-1f3fd { - background-position: 92.85714% 50%; } + background-position: 92.85714% 50%; +} .emojione-1f934-1f3fe { - background-position: 92.85714% 52.38095%; } + background-position: 92.85714% 52.38095%; +} .emojione-1f934-1f3ff { - background-position: 92.85714% 54.7619%; } + background-position: 92.85714% 54.7619%; +} .emojione-1f934 { - background-position: 92.85714% 57.14286%; } + background-position: 92.85714% 57.14286%; +} .emojione-1f935-1f3fb { - background-position: 92.85714% 59.52381%; } + background-position: 92.85714% 59.52381%; +} .emojione-1f935-1f3fc { - background-position: 92.85714% 61.90476%; } + background-position: 92.85714% 61.90476%; +} .emojione-1f935-1f3fd { - background-position: 92.85714% 64.28571%; } + background-position: 92.85714% 64.28571%; +} .emojione-1f935-1f3fe { - background-position: 92.85714% 66.66667%; } + background-position: 92.85714% 66.66667%; +} .emojione-1f935-1f3ff { - background-position: 92.85714% 69.04762%; } + background-position: 92.85714% 69.04762%; +} .emojione-1f935 { - background-position: 92.85714% 71.42857%; } + background-position: 92.85714% 71.42857%; +} .emojione-1f936-1f3fb { - background-position: 92.85714% 73.80952%; } + background-position: 92.85714% 73.80952%; +} .emojione-1f936-1f3fc { - background-position: 92.85714% 76.19048%; } + background-position: 92.85714% 76.19048%; +} .emojione-1f936-1f3fd { - background-position: 92.85714% 78.57143%; } + background-position: 92.85714% 78.57143%; +} .emojione-1f936-1f3fe { - background-position: 92.85714% 80.95238%; } + background-position: 92.85714% 80.95238%; +} .emojione-1f936-1f3ff { - background-position: 92.85714% 83.33333%; } + background-position: 92.85714% 83.33333%; +} .emojione-1f936 { - background-position: 92.85714% 85.71429%; } + background-position: 92.85714% 85.71429%; +} .emojione-1f937-1f3fb { - background-position: 92.85714% 88.09524%; } + background-position: 92.85714% 88.09524%; +} .emojione-1f937-1f3fc { - background-position: 92.85714% 90.47619%; } + background-position: 92.85714% 90.47619%; +} .emojione-1f937-1f3fd { - background-position: 0% 92.85714%; } + background-position: 0% 92.85714%; +} .emojione-1f937-1f3fe { - background-position: 2.38095% 92.85714%; } + background-position: 2.38095% 92.85714%; +} .emojione-1f937-1f3ff { - background-position: 4.7619% 92.85714%; } + background-position: 4.7619% 92.85714%; +} .emojione-1f937 { - background-position: 7.14286% 92.85714%; } + background-position: 7.14286% 92.85714%; +} .emojione-1f938-1f3fb { - background-position: 9.52381% 92.85714%; } + background-position: 9.52381% 92.85714%; +} .emojione-1f938-1f3fc { - background-position: 11.90476% 92.85714%; } + background-position: 11.90476% 92.85714%; +} .emojione-1f938-1f3fd { - background-position: 14.28571% 92.85714%; } + background-position: 14.28571% 92.85714%; +} .emojione-1f938-1f3fe { - background-position: 16.66667% 92.85714%; } + background-position: 16.66667% 92.85714%; +} .emojione-1f938-1f3ff { - background-position: 19.04762% 92.85714%; } + background-position: 19.04762% 92.85714%; +} .emojione-1f938 { - background-position: 21.42857% 92.85714%; } + background-position: 21.42857% 92.85714%; +} .emojione-1f939-1f3fb { - background-position: 23.80952% 92.85714%; } + background-position: 23.80952% 92.85714%; +} .emojione-1f939-1f3fc { - background-position: 26.19048% 92.85714%; } + background-position: 26.19048% 92.85714%; +} .emojione-1f939-1f3fd { - background-position: 28.57143% 92.85714%; } + background-position: 28.57143% 92.85714%; +} .emojione-1f939-1f3fe { - background-position: 30.95238% 92.85714%; } + background-position: 30.95238% 92.85714%; +} .emojione-1f939-1f3ff { - background-position: 33.33333% 92.85714%; } + background-position: 33.33333% 92.85714%; +} .emojione-1f939 { - background-position: 35.71429% 92.85714%; } + background-position: 35.71429% 92.85714%; +} .emojione-1f93a { - background-position: 38.09524% 92.85714%; } + background-position: 38.09524% 92.85714%; +} .emojione-1f93c-1f3fb { - background-position: 40.47619% 92.85714%; } + background-position: 40.47619% 92.85714%; +} .emojione-1f93c-1f3fc { - background-position: 42.85714% 92.85714%; } + background-position: 42.85714% 92.85714%; +} .emojione-1f93c-1f3fd { - background-position: 45.2381% 92.85714%; } + background-position: 45.2381% 92.85714%; +} .emojione-1f93c-1f3fe { - background-position: 47.61905% 92.85714%; } + background-position: 47.61905% 92.85714%; +} .emojione-1f93c-1f3ff { - background-position: 50% 92.85714%; } + background-position: 50% 92.85714%; +} .emojione-1f93c { - background-position: 52.38095% 92.85714%; } + background-position: 52.38095% 92.85714%; +} .emojione-1f93d-1f3fb { - background-position: 54.7619% 92.85714%; } + background-position: 54.7619% 92.85714%; +} .emojione-1f93d-1f3fc { - background-position: 57.14286% 92.85714%; } + background-position: 57.14286% 92.85714%; +} .emojione-1f93d-1f3fd { - background-position: 59.52381% 92.85714%; } + background-position: 59.52381% 92.85714%; +} .emojione-1f93d-1f3fe { - background-position: 61.90476% 92.85714%; } + background-position: 61.90476% 92.85714%; +} .emojione-1f93d-1f3ff { - background-position: 64.28571% 92.85714%; } + background-position: 64.28571% 92.85714%; +} .emojione-1f93d { - background-position: 66.66667% 92.85714%; } + background-position: 66.66667% 92.85714%; +} .emojione-1f93e-1f3fb { - background-position: 69.04762% 92.85714%; } + background-position: 69.04762% 92.85714%; +} .emojione-1f93e-1f3fc { - background-position: 71.42857% 92.85714%; } + background-position: 71.42857% 92.85714%; +} .emojione-1f93e-1f3fd { - background-position: 73.80952% 92.85714%; } + background-position: 73.80952% 92.85714%; +} .emojione-1f93e-1f3fe { - background-position: 76.19048% 92.85714%; } + background-position: 76.19048% 92.85714%; +} .emojione-1f93e-1f3ff { - background-position: 78.57143% 92.85714%; } + background-position: 78.57143% 92.85714%; +} .emojione-1f93e { - background-position: 80.95238% 92.85714%; } + background-position: 80.95238% 92.85714%; +} .emojione-1f940 { - background-position: 83.33333% 92.85714%; } + background-position: 83.33333% 92.85714%; +} .emojione-1f941 { - background-position: 85.71429% 92.85714%; } + background-position: 85.71429% 92.85714%; +} .emojione-1f942 { - background-position: 88.09524% 92.85714%; } + background-position: 88.09524% 92.85714%; +} .emojione-1f943 { - background-position: 90.47619% 92.85714%; } + background-position: 90.47619% 92.85714%; +} .emojione-1f944 { - background-position: 92.85714% 92.85714%; } + background-position: 92.85714% 92.85714%; +} .emojione-1f945 { - background-position: 95.2381% 0%; } + background-position: 95.2381% 0%; +} .emojione-1f947 { - background-position: 95.2381% 2.38095%; } + background-position: 95.2381% 2.38095%; +} .emojione-1f948 { - background-position: 95.2381% 4.7619%; } + background-position: 95.2381% 4.7619%; +} .emojione-1f949 { - background-position: 95.2381% 7.14286%; } + background-position: 95.2381% 7.14286%; +} .emojione-1f94a { - background-position: 95.2381% 9.52381%; } + background-position: 95.2381% 9.52381%; +} .emojione-1f94b { - background-position: 95.2381% 11.90476%; } + background-position: 95.2381% 11.90476%; +} .emojione-1f950 { - background-position: 95.2381% 14.28571%; } + background-position: 95.2381% 14.28571%; +} .emojione-1f951 { - background-position: 95.2381% 16.66667%; } + background-position: 95.2381% 16.66667%; +} .emojione-1f952 { - background-position: 95.2381% 19.04762%; } + background-position: 95.2381% 19.04762%; +} .emojione-1f953 { - background-position: 95.2381% 21.42857%; } + background-position: 95.2381% 21.42857%; +} .emojione-1f954 { - background-position: 95.2381% 23.80952%; } + background-position: 95.2381% 23.80952%; +} .emojione-1f955 { - background-position: 95.2381% 26.19048%; } + background-position: 95.2381% 26.19048%; +} .emojione-1f956 { - background-position: 95.2381% 28.57143%; } + background-position: 95.2381% 28.57143%; +} .emojione-1f957 { - background-position: 95.2381% 30.95238%; } + background-position: 95.2381% 30.95238%; +} .emojione-1f958 { - background-position: 95.2381% 33.33333%; } + background-position: 95.2381% 33.33333%; +} .emojione-1f959 { - background-position: 95.2381% 35.71429%; } + background-position: 95.2381% 35.71429%; +} .emojione-1f95a { - background-position: 95.2381% 38.09524%; } + background-position: 95.2381% 38.09524%; +} .emojione-1f95b { - background-position: 95.2381% 40.47619%; } + background-position: 95.2381% 40.47619%; +} .emojione-1f95c { - background-position: 95.2381% 42.85714%; } + background-position: 95.2381% 42.85714%; +} .emojione-1f95d { - background-position: 95.2381% 45.2381%; } + background-position: 95.2381% 45.2381%; +} .emojione-1f95e { - background-position: 95.2381% 47.61905%; } + background-position: 95.2381% 47.61905%; +} .emojione-1f980 { - background-position: 95.2381% 50%; } + background-position: 95.2381% 50%; +} .emojione-1f981 { - background-position: 95.2381% 52.38095%; } + background-position: 95.2381% 52.38095%; +} .emojione-1f982 { - background-position: 95.2381% 54.7619%; } + background-position: 95.2381% 54.7619%; +} .emojione-1f983 { - background-position: 95.2381% 57.14286%; } + background-position: 95.2381% 57.14286%; +} .emojione-1f984 { - background-position: 95.2381% 59.52381%; } + background-position: 95.2381% 59.52381%; +} .emojione-1f985 { - background-position: 95.2381% 61.90476%; } + background-position: 95.2381% 61.90476%; +} .emojione-1f986 { - background-position: 95.2381% 64.28571%; } + background-position: 95.2381% 64.28571%; +} .emojione-1f987 { - background-position: 95.2381% 66.66667%; } + background-position: 95.2381% 66.66667%; +} .emojione-1f988 { - background-position: 95.2381% 69.04762%; } + background-position: 95.2381% 69.04762%; +} .emojione-1f989 { - background-position: 95.2381% 71.42857%; } + background-position: 95.2381% 71.42857%; +} .emojione-1f98a { - background-position: 95.2381% 73.80952%; } + background-position: 95.2381% 73.80952%; +} .emojione-1f98b { - background-position: 95.2381% 76.19048%; } + background-position: 95.2381% 76.19048%; +} .emojione-1f98c { - background-position: 95.2381% 78.57143%; } + background-position: 95.2381% 78.57143%; +} .emojione-1f98d { - background-position: 95.2381% 80.95238%; } + background-position: 95.2381% 80.95238%; +} .emojione-1f98e { - background-position: 95.2381% 83.33333%; } + background-position: 95.2381% 83.33333%; +} .emojione-1f98f { - background-position: 95.2381% 85.71429%; } + background-position: 95.2381% 85.71429%; +} .emojione-1f990 { - background-position: 95.2381% 88.09524%; } + background-position: 95.2381% 88.09524%; +} .emojione-1f991 { - background-position: 95.2381% 90.47619%; } + background-position: 95.2381% 90.47619%; +} .emojione-1f9c0 { - background-position: 95.2381% 92.85714%; } + background-position: 95.2381% 92.85714%; +} .emojione-203c { - background-position: 0% 95.2381%; } + background-position: 0% 95.2381%; +} .emojione-2049 { - background-position: 2.38095% 95.2381%; } + background-position: 2.38095% 95.2381%; +} .emojione-2122 { - background-position: 4.7619% 95.2381%; } + background-position: 4.7619% 95.2381%; +} .emojione-2139 { - background-position: 7.14286% 95.2381%; } + background-position: 7.14286% 95.2381%; +} .emojione-2194 { - background-position: 9.52381% 95.2381%; } + background-position: 9.52381% 95.2381%; +} .emojione-2195 { - background-position: 11.90476% 95.2381%; } + background-position: 11.90476% 95.2381%; +} .emojione-2196 { - background-position: 14.28571% 95.2381%; } + background-position: 14.28571% 95.2381%; +} .emojione-2197 { - background-position: 16.66667% 95.2381%; } + background-position: 16.66667% 95.2381%; +} .emojione-2198 { - background-position: 19.04762% 95.2381%; } + background-position: 19.04762% 95.2381%; +} .emojione-2199 { - background-position: 21.42857% 95.2381%; } + background-position: 21.42857% 95.2381%; +} .emojione-21a9 { - background-position: 23.80952% 95.2381%; } + background-position: 23.80952% 95.2381%; +} .emojione-21aa { - background-position: 26.19048% 95.2381%; } + background-position: 26.19048% 95.2381%; +} .emojione-231a { - background-position: 28.57143% 95.2381%; } + background-position: 28.57143% 95.2381%; +} .emojione-231b { - background-position: 30.95238% 95.2381%; } + background-position: 30.95238% 95.2381%; +} .emojione-2328 { - background-position: 33.33333% 95.2381%; } + background-position: 33.33333% 95.2381%; +} .emojione-23cf { - background-position: 35.71429% 95.2381%; } + background-position: 35.71429% 95.2381%; +} .emojione-23e9 { - background-position: 38.09524% 95.2381%; } + background-position: 38.09524% 95.2381%; +} .emojione-23ea { - background-position: 40.47619% 95.2381%; } + background-position: 40.47619% 95.2381%; +} .emojione-23eb { - background-position: 42.85714% 95.2381%; } + background-position: 42.85714% 95.2381%; +} .emojione-23ec { - background-position: 45.2381% 95.2381%; } + background-position: 45.2381% 95.2381%; +} .emojione-23ed { - background-position: 47.61905% 95.2381%; } + background-position: 47.61905% 95.2381%; +} .emojione-23ee { - background-position: 50% 95.2381%; } + background-position: 50% 95.2381%; +} .emojione-23ef { - background-position: 52.38095% 95.2381%; } + background-position: 52.38095% 95.2381%; +} .emojione-23f0 { - background-position: 54.7619% 95.2381%; } + background-position: 54.7619% 95.2381%; +} .emojione-23f1 { - background-position: 57.14286% 95.2381%; } + background-position: 57.14286% 95.2381%; +} .emojione-23f2 { - background-position: 59.52381% 95.2381%; } + background-position: 59.52381% 95.2381%; +} .emojione-23f3 { - background-position: 61.90476% 95.2381%; } + background-position: 61.90476% 95.2381%; +} .emojione-23f8 { - background-position: 64.28571% 95.2381%; } + background-position: 64.28571% 95.2381%; +} .emojione-23f9 { - background-position: 66.66667% 95.2381%; } + background-position: 66.66667% 95.2381%; +} .emojione-23fa { - background-position: 69.04762% 95.2381%; } + background-position: 69.04762% 95.2381%; +} .emojione-24c2 { - background-position: 71.42857% 95.2381%; } + background-position: 71.42857% 95.2381%; +} .emojione-25aa { - background-position: 73.80952% 95.2381%; } + background-position: 73.80952% 95.2381%; +} .emojione-25ab { - background-position: 76.19048% 95.2381%; } + background-position: 76.19048% 95.2381%; +} .emojione-25b6 { - background-position: 78.57143% 95.2381%; } + background-position: 78.57143% 95.2381%; +} .emojione-25c0 { - background-position: 80.95238% 95.2381%; } + background-position: 80.95238% 95.2381%; +} .emojione-25fb { - background-position: 83.33333% 95.2381%; } + background-position: 83.33333% 95.2381%; +} .emojione-25fc { - background-position: 85.71429% 95.2381%; } + background-position: 85.71429% 95.2381%; +} .emojione-25fd { - background-position: 88.09524% 95.2381%; } + background-position: 88.09524% 95.2381%; +} .emojione-25fe { - background-position: 90.47619% 95.2381%; } + background-position: 90.47619% 95.2381%; +} .emojione-2600 { - background-position: 92.85714% 95.2381%; } + background-position: 92.85714% 95.2381%; +} .emojione-2601 { - background-position: 95.2381% 95.2381%; } + background-position: 95.2381% 95.2381%; +} .emojione-2602 { - background-position: 97.61905% 0%; } + background-position: 97.61905% 0%; +} .emojione-2603 { - background-position: 97.61905% 2.38095%; } + background-position: 97.61905% 2.38095%; +} .emojione-2604 { - background-position: 97.61905% 4.7619%; } + background-position: 97.61905% 4.7619%; +} .emojione-260e { - background-position: 97.61905% 7.14286%; } + background-position: 97.61905% 7.14286%; +} .emojione-2611 { - background-position: 97.61905% 9.52381%; } + background-position: 97.61905% 9.52381%; +} .emojione-2614 { - background-position: 97.61905% 11.90476%; } + background-position: 97.61905% 11.90476%; +} .emojione-2615 { - background-position: 97.61905% 14.28571%; } + background-position: 97.61905% 14.28571%; +} .emojione-2618 { - background-position: 97.61905% 16.66667%; } + background-position: 97.61905% 16.66667%; +} .emojione-261d-1f3fb { - background-position: 97.61905% 19.04762%; } + background-position: 97.61905% 19.04762%; +} .emojione-261d-1f3fc { - background-position: 97.61905% 21.42857%; } + background-position: 97.61905% 21.42857%; +} .emojione-261d-1f3fd { - background-position: 97.61905% 23.80952%; } + background-position: 97.61905% 23.80952%; +} .emojione-261d-1f3fe { - background-position: 97.61905% 26.19048%; } + background-position: 97.61905% 26.19048%; +} .emojione-261d-1f3ff { - background-position: 97.61905% 28.57143%; } + background-position: 97.61905% 28.57143%; +} .emojione-261d { - background-position: 97.61905% 30.95238%; } + background-position: 97.61905% 30.95238%; +} .emojione-2620 { - background-position: 97.61905% 33.33333%; } + background-position: 97.61905% 33.33333%; +} .emojione-2622 { - background-position: 97.61905% 35.71429%; } + background-position: 97.61905% 35.71429%; +} .emojione-2623 { - background-position: 97.61905% 38.09524%; } + background-position: 97.61905% 38.09524%; +} .emojione-2626 { - background-position: 97.61905% 40.47619%; } + background-position: 97.61905% 40.47619%; +} .emojione-262a { - background-position: 97.61905% 42.85714%; } + background-position: 97.61905% 42.85714%; +} .emojione-262e { - background-position: 97.61905% 45.2381%; } + background-position: 97.61905% 45.2381%; +} .emojione-262f { - background-position: 97.61905% 47.61905%; } + background-position: 97.61905% 47.61905%; +} .emojione-2638 { - background-position: 97.61905% 50%; } + background-position: 97.61905% 50%; +} .emojione-2639 { - background-position: 97.61905% 52.38095%; } + background-position: 97.61905% 52.38095%; +} .emojione-263a { - background-position: 97.61905% 54.7619%; } + background-position: 97.61905% 54.7619%; +} .emojione-2648 { - background-position: 97.61905% 57.14286%; } + background-position: 97.61905% 57.14286%; +} .emojione-2649 { - background-position: 97.61905% 59.52381%; } + background-position: 97.61905% 59.52381%; +} .emojione-264a { - background-position: 97.61905% 61.90476%; } + background-position: 97.61905% 61.90476%; +} .emojione-264b { - background-position: 97.61905% 64.28571%; } + background-position: 97.61905% 64.28571%; +} .emojione-264c { - background-position: 97.61905% 66.66667%; } + background-position: 97.61905% 66.66667%; +} .emojione-264d { - background-position: 97.61905% 69.04762%; } + background-position: 97.61905% 69.04762%; +} .emojione-264e { - background-position: 97.61905% 71.42857%; } + background-position: 97.61905% 71.42857%; +} .emojione-264f { - background-position: 97.61905% 73.80952%; } + background-position: 97.61905% 73.80952%; +} .emojione-2650 { - background-position: 97.61905% 76.19048%; } + background-position: 97.61905% 76.19048%; +} .emojione-2651 { - background-position: 97.61905% 78.57143%; } + background-position: 97.61905% 78.57143%; +} .emojione-2652 { - background-position: 97.61905% 80.95238%; } + background-position: 97.61905% 80.95238%; +} .emojione-2653 { - background-position: 97.61905% 83.33333%; } + background-position: 97.61905% 83.33333%; +} .emojione-2660 { - background-position: 97.61905% 85.71429%; } + background-position: 97.61905% 85.71429%; +} .emojione-2663 { - background-position: 97.61905% 88.09524%; } + background-position: 97.61905% 88.09524%; +} .emojione-2665 { - background-position: 97.61905% 90.47619%; } + background-position: 97.61905% 90.47619%; +} .emojione-2666 { - background-position: 97.61905% 92.85714%; } + background-position: 97.61905% 92.85714%; +} .emojione-2668 { - background-position: 97.61905% 95.2381%; } + background-position: 97.61905% 95.2381%; +} .emojione-267b { - background-position: 0% 97.61905%; } + background-position: 0% 97.61905%; +} .emojione-267f { - background-position: 2.38095% 97.61905%; } + background-position: 2.38095% 97.61905%; +} .emojione-2692 { - background-position: 4.7619% 97.61905%; } + background-position: 4.7619% 97.61905%; +} .emojione-2693 { - background-position: 7.14286% 97.61905%; } + background-position: 7.14286% 97.61905%; +} .emojione-2694 { - background-position: 9.52381% 97.61905%; } + background-position: 9.52381% 97.61905%; +} .emojione-2696 { - background-position: 11.90476% 97.61905%; } + background-position: 11.90476% 97.61905%; +} .emojione-2697 { - background-position: 14.28571% 97.61905%; } + background-position: 14.28571% 97.61905%; +} .emojione-2699 { - background-position: 16.66667% 97.61905%; } + background-position: 16.66667% 97.61905%; +} .emojione-269b { - background-position: 19.04762% 97.61905%; } + background-position: 19.04762% 97.61905%; +} .emojione-269c { - background-position: 21.42857% 97.61905%; } + background-position: 21.42857% 97.61905%; +} .emojione-26a0 { - background-position: 23.80952% 97.61905%; } + background-position: 23.80952% 97.61905%; +} .emojione-26a1 { - background-position: 26.19048% 97.61905%; } + background-position: 26.19048% 97.61905%; +} .emojione-26aa { - background-position: 28.57143% 97.61905%; } + background-position: 28.57143% 97.61905%; +} .emojione-26ab { - background-position: 30.95238% 97.61905%; } + background-position: 30.95238% 97.61905%; +} .emojione-26b0 { - background-position: 33.33333% 97.61905%; } + background-position: 33.33333% 97.61905%; +} .emojione-26b1 { - background-position: 35.71429% 97.61905%; } + background-position: 35.71429% 97.61905%; +} .emojione-26bd { - background-position: 38.09524% 97.61905%; } + background-position: 38.09524% 97.61905%; +} .emojione-26be { - background-position: 40.47619% 97.61905%; } + background-position: 40.47619% 97.61905%; +} .emojione-26c4 { - background-position: 42.85714% 97.61905%; } + background-position: 42.85714% 97.61905%; +} .emojione-26c5 { - background-position: 45.2381% 97.61905%; } + background-position: 45.2381% 97.61905%; +} .emojione-26c8 { - background-position: 47.61905% 97.61905%; } + background-position: 47.61905% 97.61905%; +} .emojione-26ce { - background-position: 50% 97.61905%; } + background-position: 50% 97.61905%; +} .emojione-26cf { - background-position: 52.38095% 97.61905%; } + background-position: 52.38095% 97.61905%; +} .emojione-26d1 { - background-position: 54.7619% 97.61905%; } + background-position: 54.7619% 97.61905%; +} .emojione-26d3 { - background-position: 57.14286% 97.61905%; } + background-position: 57.14286% 97.61905%; +} .emojione-26d4 { - background-position: 59.52381% 97.61905%; } + background-position: 59.52381% 97.61905%; +} .emojione-26e9 { - background-position: 61.90476% 97.61905%; } + background-position: 61.90476% 97.61905%; +} .emojione-26ea { - background-position: 64.28571% 97.61905%; } + background-position: 64.28571% 97.61905%; +} .emojione-26f0 { - background-position: 66.66667% 97.61905%; } + background-position: 66.66667% 97.61905%; +} .emojione-26f1 { - background-position: 69.04762% 97.61905%; } + background-position: 69.04762% 97.61905%; +} .emojione-26f2 { - background-position: 71.42857% 97.61905%; } + background-position: 71.42857% 97.61905%; +} .emojione-26f3 { - background-position: 73.80952% 97.61905%; } + background-position: 73.80952% 97.61905%; +} .emojione-26f4 { - background-position: 76.19048% 97.61905%; } + background-position: 76.19048% 97.61905%; +} .emojione-26f5 { - background-position: 78.57143% 97.61905%; } + background-position: 78.57143% 97.61905%; +} .emojione-26f7 { - background-position: 80.95238% 97.61905%; } + background-position: 80.95238% 97.61905%; +} .emojione-26f8 { - background-position: 83.33333% 97.61905%; } + background-position: 83.33333% 97.61905%; +} .emojione-26f9-1f3fb { - background-position: 85.71429% 97.61905%; } + background-position: 85.71429% 97.61905%; +} .emojione-26f9-1f3fc { - background-position: 88.09524% 97.61905%; } + background-position: 88.09524% 97.61905%; +} .emojione-26f9-1f3fd { - background-position: 90.47619% 97.61905%; } + background-position: 90.47619% 97.61905%; +} .emojione-26f9-1f3fe { - background-position: 92.85714% 97.61905%; } + background-position: 92.85714% 97.61905%; +} .emojione-26f9-1f3ff { - background-position: 95.2381% 97.61905%; } + background-position: 95.2381% 97.61905%; +} .emojione-26f9 { - background-position: 97.61905% 97.61905%; } + background-position: 97.61905% 97.61905%; +} .emojione-26fa { - background-position: 100% 0%; } + background-position: 100% 0%; +} .emojione-26fd { - background-position: 100% 2.38095%; } + background-position: 100% 2.38095%; +} .emojione-2702 { - background-position: 100% 4.7619%; } + background-position: 100% 4.7619%; +} .emojione-2705 { - background-position: 100% 7.14286%; } + background-position: 100% 7.14286%; +} .emojione-2708 { - background-position: 100% 9.52381%; } + background-position: 100% 9.52381%; +} .emojione-2709 { - background-position: 100% 11.90476%; } + background-position: 100% 11.90476%; +} .emojione-270a-1f3fb { - background-position: 100% 14.28571%; } + background-position: 100% 14.28571%; +} .emojione-270a-1f3fc { - background-position: 100% 16.66667%; } + background-position: 100% 16.66667%; +} .emojione-270a-1f3fd { - background-position: 100% 19.04762%; } + background-position: 100% 19.04762%; +} .emojione-270a-1f3fe { - background-position: 100% 21.42857%; } + background-position: 100% 21.42857%; +} .emojione-270a-1f3ff { - background-position: 100% 23.80952%; } + background-position: 100% 23.80952%; +} .emojione-270a { - background-position: 100% 26.19048%; } + background-position: 100% 26.19048%; +} .emojione-270b-1f3fb { - background-position: 100% 28.57143%; } + background-position: 100% 28.57143%; +} .emojione-270b-1f3fc { - background-position: 100% 30.95238%; } + background-position: 100% 30.95238%; +} .emojione-270b-1f3fd { - background-position: 100% 33.33333%; } + background-position: 100% 33.33333%; +} .emojione-270b-1f3fe { - background-position: 100% 35.71429%; } + background-position: 100% 35.71429%; +} .emojione-270b-1f3ff { - background-position: 100% 38.09524%; } + background-position: 100% 38.09524%; +} .emojione-270b { - background-position: 100% 40.47619%; } + background-position: 100% 40.47619%; +} .emojione-270c-1f3fb { - background-position: 100% 42.85714%; } + background-position: 100% 42.85714%; +} .emojione-270c-1f3fc { - background-position: 100% 45.2381%; } + background-position: 100% 45.2381%; +} .emojione-270c-1f3fd { - background-position: 100% 47.61905%; } + background-position: 100% 47.61905%; +} .emojione-270c-1f3fe { - background-position: 100% 50%; } + background-position: 100% 50%; +} .emojione-270c-1f3ff { - background-position: 100% 52.38095%; } + background-position: 100% 52.38095%; +} .emojione-270c { - background-position: 100% 54.7619%; } + background-position: 100% 54.7619%; +} .emojione-270d-1f3fb { - background-position: 100% 57.14286%; } + background-position: 100% 57.14286%; +} .emojione-270d-1f3fc { - background-position: 100% 59.52381%; } + background-position: 100% 59.52381%; +} .emojione-270d-1f3fd { - background-position: 100% 61.90476%; } + background-position: 100% 61.90476%; +} .emojione-270d-1f3fe { - background-position: 100% 64.28571%; } + background-position: 100% 64.28571%; +} .emojione-270d-1f3ff { - background-position: 100% 66.66667%; } + background-position: 100% 66.66667%; +} .emojione-270d { - background-position: 100% 69.04762%; } + background-position: 100% 69.04762%; +} .emojione-270f { - background-position: 100% 71.42857%; } + background-position: 100% 71.42857%; +} .emojione-2712 { - background-position: 100% 73.80952%; } + background-position: 100% 73.80952%; +} .emojione-2714 { - background-position: 100% 76.19048%; } + background-position: 100% 76.19048%; +} .emojione-2716 { - background-position: 100% 78.57143%; } + background-position: 100% 78.57143%; +} .emojione-271d { - background-position: 100% 80.95238%; } + background-position: 100% 80.95238%; +} .emojione-2721 { - background-position: 100% 83.33333%; } + background-position: 100% 83.33333%; +} .emojione-2728 { - background-position: 100% 85.71429%; } + background-position: 100% 85.71429%; +} .emojione-2733 { - background-position: 100% 88.09524%; } + background-position: 100% 88.09524%; +} .emojione-2734 { - background-position: 100% 90.47619%; } + background-position: 100% 90.47619%; +} .emojione-2744 { - background-position: 100% 92.85714%; } + background-position: 100% 92.85714%; +} .emojione-2747 { - background-position: 100% 95.2381%; } + background-position: 100% 95.2381%; +} .emojione-274c { - background-position: 100% 97.61905%; } + background-position: 100% 97.61905%; +} .emojione-274e { - background-position: 0% 100%; } + background-position: 0% 100%; +} .emojione-2753 { - background-position: 2.38095% 100%; } + background-position: 2.38095% 100%; +} .emojione-2754 { - background-position: 4.7619% 100%; } + background-position: 4.7619% 100%; +} .emojione-2755 { - background-position: 7.14286% 100%; } + background-position: 7.14286% 100%; +} .emojione-2757 { - background-position: 9.52381% 100%; } + background-position: 9.52381% 100%; +} .emojione-2763 { - background-position: 11.90476% 100%; } + background-position: 11.90476% 100%; +} .emojione-2764 { - background-position: 14.28571% 100%; } + background-position: 14.28571% 100%; +} .emojione-2795 { - background-position: 16.66667% 100%; } + background-position: 16.66667% 100%; +} .emojione-2796 { - background-position: 19.04762% 100%; } + background-position: 19.04762% 100%; +} .emojione-2797 { - background-position: 21.42857% 100%; } + background-position: 21.42857% 100%; +} .emojione-27a1 { - background-position: 23.80952% 100%; } + background-position: 23.80952% 100%; +} .emojione-27b0 { - background-position: 26.19048% 100%; } + background-position: 26.19048% 100%; +} .emojione-27bf { - background-position: 28.57143% 100%; } + background-position: 28.57143% 100%; +} .emojione-2934 { - background-position: 30.95238% 100%; } + background-position: 30.95238% 100%; +} .emojione-2935 { - background-position: 33.33333% 100%; } + background-position: 33.33333% 100%; +} .emojione-2b05 { - background-position: 35.71429% 100%; } + background-position: 35.71429% 100%; +} .emojione-2b06 { - background-position: 38.09524% 100%; } + background-position: 38.09524% 100%; +} .emojione-2b07 { - background-position: 40.47619% 100%; } + background-position: 40.47619% 100%; +} .emojione-2b1b { - background-position: 42.85714% 100%; } + background-position: 42.85714% 100%; +} .emojione-2b1c { - background-position: 45.2381% 100%; } + background-position: 45.2381% 100%; +} .emojione-2b50 { - background-position: 47.61905% 100%; } + background-position: 47.61905% 100%; +} .emojione-2b55 { - background-position: 50% 100%; } + background-position: 50% 100%; +} .emojione-3030 { - background-position: 52.38095% 100%; } + background-position: 52.38095% 100%; +} .emojione-303d { - background-position: 54.7619% 100%; } + background-position: 54.7619% 100%; +} .emojione-3297 { - background-position: 57.14286% 100%; } + background-position: 57.14286% 100%; +} .emojione-3299 { - background-position: 59.52381% 100%; } + background-position: 59.52381% 100%; +} diff --git a/packages/rocketchat-emoji/emoji.css b/packages/rocketchat-emoji/emoji.css index 01dc97f89176b1594f4ece3785bf72f9b2adcca1..983e0d928e48e8f58b7313c003ec1c8a0d967d1e 100644 --- a/packages/rocketchat-emoji/emoji.css +++ b/packages/rocketchat-emoji/emoji.css @@ -5,7 +5,7 @@ width: 22px; position: relative; display: inline-block; - margin: 0 .15em; + margin: 0 0.15em; line-height: normal; vertical-align: middle; background-position: center; diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js b/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js deleted file mode 100644 index 277f6471a9aa30f5867ada1cf806327bdf423dee..0000000000000000000000000000000000000000 --- a/packages/rocketchat-file-upload/client/lib/FileUploadAmazonS3.js +++ /dev/null @@ -1,65 +0,0 @@ -/* globals FileUpload, FileUploadBase, Slingshot */ - -FileUpload.AmazonS3 = class FileUploadAmazonS3 extends FileUploadBase { - constructor(meta, file) { - super(meta, file); - this.uploader = new Slingshot.Upload('rocketchat-uploads', { rid: meta.rid }); - } - - start() { - this.uploader.send(this.file, (error, downloadUrl) => { - if (this.computation) { - this.computation.stop(); - } - - if (error) { - let uploading = Session.get('uploading'); - if (!Array.isArray(uploading)) { - uploading = []; - } - - const item = _.findWhere(uploading, { id: this.id }); - - if (_.isObject(item)) { - item.error = error.error; - item.percentage = 0; - } else { - uploading.push({ - error: error.error, - percentage: 0 - }); - } - - Session.set('uploading', uploading); - } else { - const file = _.pick(this.meta, 'type', 'size', 'name', 'identify', 'description'); - file._id = downloadUrl.substr(downloadUrl.lastIndexOf('/') + 1); - file.url = downloadUrl; - - Meteor.call('sendFileMessage', this.meta.rid, 's3', file, () => { - Meteor.setTimeout(() => { - const uploading = Session.get('uploading'); - if (uploading !== null) { - const item = _.findWhere(uploading, { - id: this.id - }); - return Session.set('uploading', _.without(uploading, item)); - } - }, 2000); - }); - } - }); - - this.computation = Tracker.autorun(() => { - this.onProgress(this.uploader.progress()); - }); - } - - onProgress() {} - - stop() { - if (this.uploader && this.uploader.xhr) { - this.uploader.xhr.abort(); - } - } -}; diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadFileSystem.js b/packages/rocketchat-file-upload/client/lib/FileUploadFileSystem.js deleted file mode 100644 index d19c5c03141f07085ab935fd9be36c21a69ee408..0000000000000000000000000000000000000000 --- a/packages/rocketchat-file-upload/client/lib/FileUploadFileSystem.js +++ /dev/null @@ -1,64 +0,0 @@ -/* globals FileUploadBase, UploadFS, FileUpload:true, FileSystemStore:true */ - -FileSystemStore = new UploadFS.store.Local({ - collection: RocketChat.models.Uploads.model, - name: 'fileSystem', - filter: new UploadFS.Filter({ - onCheck: FileUpload.validateFileUpload - }) -}); - -FileUpload.FileSystem = class FileUploadFileSystem extends FileUploadBase { - constructor(meta, file) { - super(meta, file); - this.handler = new UploadFS.Uploader({ - store: FileSystemStore, - data: file, - file: meta, - onError: (err) => { - const uploading = Session.get('uploading'); - if (uploading != null) { - const item = _.findWhere(uploading, { - id: this.id - }); - if (item != null) { - item.error = err.reason; - item.percentage = 0; - } - return Session.set('uploading', uploading); - } - }, - onComplete: (fileData) => { - const file = _.pick(fileData, '_id', 'type', 'size', 'name', 'identify', 'description'); - - file.url = fileData.url.replace(Meteor.absoluteUrl(), '/'); - - Meteor.call('sendFileMessage', this.meta.rid, null, file, () => { - Meteor.setTimeout(() => { - const uploading = Session.get('uploading'); - if (uploading != null) { - const item = _.findWhere(uploading, { - id: this.id - }); - return Session.set('uploading', _.without(uploading, item)); - } - }, 2000); - }); - } - }); - - this.handler.onProgress = (file, progress) => { - this.onProgress(progress); - }; - } - - start() { - return this.handler.start(); - } - - onProgress() {} - - stop() { - return this.handler.stop(); - } -}; diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js b/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js deleted file mode 100644 index d1daa62ec07f61942ed051caf70dab316e1fe154..0000000000000000000000000000000000000000 --- a/packages/rocketchat-file-upload/client/lib/FileUploadGoogleStorage.js +++ /dev/null @@ -1,65 +0,0 @@ -/* globals FileUpload, FileUploadBase, Slingshot */ - -FileUpload.GoogleCloudStorage = class FileUploadGoogleCloudStorage extends FileUploadBase { - constructor(meta, file) { - super(meta, file); - this.uploader = new Slingshot.Upload('rocketchat-uploads-gs', { rid: meta.rid }); - } - - start() { - this.uploader.send(this.file, (error, downloadUrl) => { - if (this.computation) { - this.computation.stop(); - } - - if (error) { - let uploading = Session.get('uploading'); - if (!Array.isArray(uploading)) { - uploading = []; - } - - const item = _.findWhere(uploading, { id: this.id }); - - if (_.isObject(item)) { - item.error = error.error; - item.percentage = 0; - } else { - uploading.push({ - error: error.error, - percentage: 0 - }); - } - - Session.set('uploading', uploading); - } else { - const file = _.pick(this.meta, 'type', 'size', 'name', 'identify', 'description'); - file._id = downloadUrl.substr(downloadUrl.lastIndexOf('/') + 1); - file.url = downloadUrl; - - Meteor.call('sendFileMessage', this.meta.rid, 'googleCloudStorage', file, () => { - Meteor.setTimeout(() => { - const uploading = Session.get('uploading'); - if (uploading !== null) { - const item = _.findWhere(uploading, { - id: this.id - }); - return Session.set('uploading', _.without(uploading, item)); - } - }, 2000); - }); - } - }); - - this.computation = Tracker.autorun(() => { - this.onProgress(this.uploader.progress()); - }); - } - - onProgress() {} - - stop() { - if (this.uploader && this.uploader.xhr) { - this.uploader.xhr.abort(); - } - } -}; diff --git a/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js b/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js deleted file mode 100644 index ac14bb7fafd0332528adaac25c47889f00816ade..0000000000000000000000000000000000000000 --- a/packages/rocketchat-file-upload/client/lib/FileUploadGridFS.js +++ /dev/null @@ -1,55 +0,0 @@ -/* globals FileUploadBase, UploadFS, FileUpload:true */ -FileUpload.GridFS = class FileUploadGridFS extends FileUploadBase { - constructor(meta, file) { - super(meta, file); - this.handler = new UploadFS.Uploader({ - store: Meteor.fileStore, - data: file, - file: meta, - onError: (err) => { - const uploading = Session.get('uploading'); - if (uploading != null) { - const item = _.findWhere(uploading, { - id: this.id - }); - if (item != null) { - item.error = err.reason; - item.percentage = 0; - } - return Session.set('uploading', uploading); - } - }, - onComplete: (fileData) => { - const file = _.pick(fileData, '_id', 'type', 'size', 'name', 'identify', 'description'); - - file.url = fileData.url.replace(Meteor.absoluteUrl(), '/'); - - Meteor.call('sendFileMessage', this.meta.rid, null, file, () => { - Meteor.setTimeout(() => { - const uploading = Session.get('uploading'); - if (uploading != null) { - const item = _.findWhere(uploading, { - id: this.id - }); - return Session.set('uploading', _.without(uploading, item)); - } - }, 2000); - }); - } - }); - - this.handler.onProgress = (file, progress) => { - this.onProgress(progress); - }; - } - - start() { - return this.handler.start(); - } - - onProgress() {} - - stop() { - return this.handler.stop(); - } -}; diff --git a/packages/rocketchat-file-upload/client/lib/fileUploadHandler.js b/packages/rocketchat-file-upload/client/lib/fileUploadHandler.js index 71f5fbdf15196f0c9def6ef48747cef643843fc0..73474ab5181f327e2401c651ff85a5eee3cb9b03 100644 --- a/packages/rocketchat-file-upload/client/lib/fileUploadHandler.js +++ b/packages/rocketchat-file-upload/client/lib/fileUploadHandler.js @@ -1,10 +1,36 @@ -/* globals FileUpload, fileUploadHandler:true */ +/* globals FileUploadBase, UploadFS, fileUploadHandler:true */ /* exported fileUploadHandler */ -fileUploadHandler = (meta, file) => { - const storageType = RocketChat.settings.get('FileUpload_Storage_Type'); +new UploadFS.Store({ + collection: RocketChat.models.Uploads.model, + name: 'Uploads', + filter: new UploadFS.Filter({ + onCheck: FileUpload.validateFileUpload + }) +}); - if (FileUpload[storageType] !== undefined) { - return new FileUpload[storageType](meta, file); +new UploadFS.Store({ + collection: RocketChat.models.Avatars.model, + name: 'Avatars', + filter: new UploadFS.Filter({ + onCheck: FileUpload.validateFileUpload + }) +}); + + +fileUploadHandler = (directive, meta, file) => { + const store = UploadFS.getStore(directive); + + if (store) { + return new FileUploadBase(store, meta, file); + } else { + console.error('Invalid file store', directive); } }; + +Tracker.autorun(function() { + if (Meteor.userId()) { + document.cookie = `rc_uid=${ escape(Meteor.userId()) }; path=/`; + document.cookie = `rc_token=${ escape(Accounts._storedLoginToken()) }; path=/`; + } +}); diff --git a/packages/rocketchat-file-upload/lib/FileUploadBase.js b/packages/rocketchat-file-upload/lib/FileUploadBase.js index 62263dd0c6d9b8f081726b213ca49611dcabf8eb..486fe8592d312f03156b0a18236ef4e397c6add1 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); @@ -15,10 +15,11 @@ UploadFS.config.defaultStorePermissions = new UploadFS.StorePermissions({ FileUploadBase = class FileUploadBase { - constructor(meta, file) { + constructor(store, meta, file) { this.id = Random.id(); this.meta = meta; this.file = file; + this.store = store; } getProgress() { @@ -29,11 +30,32 @@ FileUploadBase = class FileUploadBase { return this.meta.name; } - start() { - + start(callback) { + this.handler = new UploadFS.Uploader({ + store: this.store, + data: this.file, + file: this.meta, + onError: (err) => { + return callback(err); + }, + onComplete: (fileData) => { + const file = _.pick(fileData, '_id', 'type', 'size', 'name', 'identify', 'description'); + + file.url = fileData.url.replace(Meteor.absoluteUrl(), '/'); + return callback(null, file, this.store.options.name); + } + }); + + this.handler.onProgress = (file, progress) => { + this.onProgress(progress); + }; + + return this.handler.start(); } - stop() { + onProgress() {} + stop() { + return this.handler.stop(); } }; diff --git a/packages/rocketchat-file-upload/package.js b/packages/rocketchat-file-upload/package.js index 8ba5db02407d446f5dced554fd42bbe5dc5aac76..91951ea1076b69af24b2949c53765a1fece9de54 100644 --- a/packages/rocketchat-file-upload/package.js +++ b/packages/rocketchat-file-upload/package.js @@ -11,12 +11,13 @@ Package.onUse(function(api) { api.use('ecmascript'); api.use('rocketchat:file'); api.use('jalik:ufs'); + api.use('jalik:ufs-gridfs'); api.use('jalik:ufs-local@0.2.5'); api.use('edgee:slingshot'); api.use('ostrio:cookies'); - api.use('peerlibrary:aws-sdk'); api.use('rocketchat:lib'); api.use('random'); + api.use('accounts-base'); api.use('underscore'); api.use('tracker'); api.use('webapp'); @@ -27,19 +28,13 @@ Package.onUse(function(api) { api.addFiles('lib/FileUpload.js'); api.addFiles('lib/FileUploadBase.js'); - api.addFiles('client/lib/FileUploadAmazonS3.js', 'client'); - api.addFiles('client/lib/FileUploadFileSystem.js', 'client'); - api.addFiles('client/lib/FileUploadGoogleStorage.js', 'client'); - api.addFiles('client/lib/FileUploadGridFS.js', 'client'); api.addFiles('client/lib/fileUploadHandler.js', 'client'); api.addFiles('server/lib/FileUpload.js', 'server'); + api.addFiles('server/lib/proxy.js', 'server'); api.addFiles('server/lib/requests.js', 'server'); - api.addFiles('server/config/configFileUploadAmazonS3.js', 'server'); - api.addFiles('server/config/configFileUploadFileSystem.js', 'server'); - api.addFiles('server/config/configFileUploadGoogleStorage.js', 'server'); - api.addFiles('server/config/configFileUploadGridFS.js', 'server'); + api.addFiles('server/config/_configUploadStorage.js', 'server'); api.addFiles('server/methods/sendFileMessage.js', 'server'); api.addFiles('server/methods/getS3FileUrl.js', 'server'); diff --git a/packages/rocketchat-file-upload/server/config/AmazonS3.js b/packages/rocketchat-file-upload/server/config/AmazonS3.js new file mode 100644 index 0000000000000000000000000000000000000000..569e1460c94906aa6327db7c0c13851176f3049d --- /dev/null +++ b/packages/rocketchat-file-upload/server/config/AmazonS3.js @@ -0,0 +1,60 @@ +/* globals FileUpload */ + +import { FileUploadClass } from '../lib/FileUpload'; +import '../../ufs/AmazonS3/server.js'; + +const get = function(file, req, res) { + const fileUrl = this.store.getRedirectURL(file); + + if (fileUrl) { + res.setHeader('Location', fileUrl); + res.writeHead(302); + } + res.end(); +}; + +const AmazonS3Uploads = new FileUploadClass({ + name: 'AmazonS3:Uploads', + get + // store setted bellow +}); + +const AmazonS3Avatars = new FileUploadClass({ + name: 'AmazonS3:Avatars', + get + // store setted bellow +}); + +const configure = _.debounce(function() { + const Bucket = RocketChat.settings.get('FileUpload_S3_Bucket'); + const Acl = RocketChat.settings.get('FileUpload_S3_Acl'); + const AWSAccessKeyId = RocketChat.settings.get('FileUpload_S3_AWSAccessKeyId'); + const AWSSecretAccessKey = RocketChat.settings.get('FileUpload_S3_AWSSecretAccessKey'); + const URLExpiryTimeSpan = RocketChat.settings.get('FileUpload_S3_URLExpiryTimeSpan'); + const Region = RocketChat.settings.get('FileUpload_S3_Region'); + // const CDN = RocketChat.settings.get('FileUpload_S3_CDN'); + // const BucketURL = RocketChat.settings.get('FileUpload_S3_BucketURL'); + + if (!Bucket || !AWSAccessKeyId || !AWSSecretAccessKey) { + return; + } + + const config = { + connection: { + accessKeyId: AWSAccessKeyId, + secretAccessKey: AWSSecretAccessKey, + signatureVersion: 'v4', + params: { + Bucket, + ACL: Acl + }, + region: Region + }, + URLExpiryTimeSpan + }; + + AmazonS3Uploads.store = FileUpload.configureUploadsStore('AmazonS3', AmazonS3Uploads.name, config); + AmazonS3Avatars.store = FileUpload.configureUploadsStore('AmazonS3', AmazonS3Avatars.name, config); +}, 500); + +RocketChat.settings.get(/^FileUpload_S3_/, configure); diff --git a/packages/rocketchat-file-upload/server/config/FileSystem.js b/packages/rocketchat-file-upload/server/config/FileSystem.js new file mode 100644 index 0000000000000000000000000000000000000000..263baad99078edaf1365a23bfb6cc3708d254aac --- /dev/null +++ b/packages/rocketchat-file-upload/server/config/FileSystem.js @@ -0,0 +1,83 @@ +/* globals FileUpload, UploadFS */ + +import fs from 'fs'; +import { FileUploadClass } from '../lib/FileUpload'; + +const FileSystemUploads = new FileUploadClass({ + name: 'FileSystem:Uploads', + // store setted bellow + + get(file, req, res) { + const filePath = this.store.getFilePath(file._id, file); + + try { + const stat = Meteor.wrapAsync(fs.stat)(filePath); + + if (stat && stat.isFile()) { + file = FileUpload.addExtensionTo(file); + res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${ encodeURIComponent(file.name) }`); + res.setHeader('Last-Modified', file.uploadedAt.toUTCString()); + res.setHeader('Content-Type', file.type); + res.setHeader('Content-Length', file.size); + + this.store.getReadStream(file._id, file).pipe(res); + } + } catch (e) { + res.writeHead(404); + res.end(); + return; + } + } +}); + +const FileSystemAvatars = new FileUploadClass({ + name: 'FileSystem:Avatars', + // store setted bellow + + get(file, req, res) { + const reqModifiedHeader = req.headers['if-modified-since']; + if (reqModifiedHeader) { + if (reqModifiedHeader === (file.uploadedAt && file.uploadedAt.toUTCString())) { + res.setHeader('Last-Modified', reqModifiedHeader); + res.writeHead(304); + res.end(); + return; + } + } + + const filePath = this.store.getFilePath(file._id, file); + + try { + const stat = Meteor.wrapAsync(fs.stat)(filePath); + + if (stat && stat.isFile()) { + file = FileUpload.addExtensionTo(file); + res.setHeader('Content-Disposition', 'inline'); + res.setHeader('Last-Modified', file.uploadedAt.toUTCString()); + res.setHeader('Content-Type', file.type); + res.setHeader('Content-Length', file.size); + + this.store.getReadStream(file._id, file).pipe(res); + } + } catch (e) { + res.writeHead(404); + res.end(); + return; + } + } +}); + + +const createFileSystemStore = _.debounce(function() { + const options = { + path: RocketChat.settings.get('FileUpload_FileSystemPath') //'/tmp/uploads/photos', + }; + + FileSystemUploads.store = FileUpload.configureUploadsStore('Local', FileSystemUploads.name, options); + FileSystemAvatars.store = FileUpload.configureUploadsStore('Local', FileSystemAvatars.name, options); + + // DEPRECATED backwards compatibililty (remove) + UploadFS.getStores()['fileSystem'] = UploadFS.getStores()[FileSystemUploads.name]; +}, 500); + +RocketChat.settings.get('FileUpload_FileSystemPath', createFileSystemStore); diff --git a/packages/rocketchat-file-upload/server/config/GoogleStorage.js b/packages/rocketchat-file-upload/server/config/GoogleStorage.js new file mode 100644 index 0000000000000000000000000000000000000000..2010cfa68b99c8a3c6b258fcf2df548c694f2657 --- /dev/null +++ b/packages/rocketchat-file-upload/server/config/GoogleStorage.js @@ -0,0 +1,58 @@ +/* globals FileUpload */ + +import { FileUploadClass } from '../lib/FileUpload'; +import '../../ufs/GoogleStorage/server.js'; + + +const get = function(file, req, res) { + this.store.getRedirectURL(file, (err, fileUrl) => { + if (err) { + console.error(err); + } + + if (fileUrl) { + res.setHeader('Location', fileUrl); + res.writeHead(302); + } + res.end(); + }); +}; + +const GoogleCloudStorageUploads = new FileUploadClass({ + name: 'GoogleCloudStorage:Uploads', + get + // store setted bellow +}); + +const GoogleCloudStorageAvatars = new FileUploadClass({ + name: 'GoogleCloudStorage:Avatars', + get + // store setted bellow +}); + +const configure = _.debounce(function() { + const bucket = RocketChat.settings.get('FileUpload_GoogleStorage_Bucket'); + const accessId = RocketChat.settings.get('FileUpload_GoogleStorage_AccessId'); + const secret = RocketChat.settings.get('FileUpload_GoogleStorage_Secret'); + const URLExpiryTimeSpan = RocketChat.settings.get('FileUpload_S3_URLExpiryTimeSpan'); + + if (!bucket || !accessId || !secret) { + return; + } + + const config = { + connection: { + credentials: { + client_email: accessId, + private_key: secret + } + }, + bucket, + URLExpiryTimeSpan + }; + + GoogleCloudStorageUploads.store = FileUpload.configureUploadsStore('GoogleStorage', GoogleCloudStorageUploads.name, config); + GoogleCloudStorageAvatars.store = FileUpload.configureUploadsStore('GoogleStorage', GoogleCloudStorageAvatars.name, config); +}, 500); + +RocketChat.settings.get(/^FileUpload_GoogleStorage_/, configure); diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js b/packages/rocketchat-file-upload/server/config/GridFS.js similarity index 64% rename from packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js rename to packages/rocketchat-file-upload/server/config/GridFS.js index 21ac212db8eda3ff3efad45364afeb91a9b6777b..ce312f11d866f5a18817d896e538ecfc76bf053d 100644 --- a/packages/rocketchat-file-upload/server/config/configFileUploadGridFS.js +++ b/packages/rocketchat-file-upload/server/config/GridFS.js @@ -1,7 +1,13 @@ /* globals FileUpload, UploadFS */ -const stream = Npm.require('stream'); -const zlib = Npm.require('zlib'); -const util = Npm.require('util'); +import stream from 'stream'; +import zlib from 'zlib'; +import util from 'util'; + +import { FileUploadClass } from '../lib/FileUpload'; +import { Cookies } from 'meteor/ostrio:cookies'; + +const cookie = new Cookies(); + const logger = new Logger('FileUpload'); function ExtractRange(options) { @@ -124,7 +130,52 @@ const readFromGridFS = function(storeName, fileId, file, headers, req, res) { } }; -FileUpload.addHandler('rocketchat_uploads', { +const onRead = function(fileId, file, req, res) { + if (RocketChat.settings.get('FileUpload_ProtectFiles')) { + let uid; + let token; + + if (req && req.headers && req.headers.cookie) { + const rawCookies = req.headers.cookie; + + if (rawCookies) { + uid = cookie.get('rc_uid', rawCookies) ; + token = cookie.get('rc_token', rawCookies); + } + } + + if (!uid) { + uid = req.query.rc_uid; + token = req.query.rc_token; + } + + if (!uid || !token || !RocketChat.models.Users.findOneByIdAndLoginToken(uid, token)) { + res.writeHead(403); + return false; + } + } + + res.setHeader('content-disposition', `attachment; filename="${ encodeURIComponent(file.name) }"`); + return true; +}; + +FileUpload.configureUploadsStore('GridFS', 'GridFS:Uploads', { + collectionName: 'rocketchat_uploads', + onRead +}); + +// DEPRECATED: backwards compatibility (remove) +UploadFS.getStores()['rocketchat_uploads'] = UploadFS.getStores()['GridFS:Uploads']; + +FileUpload.configureUploadsStore('GridFS', 'GridFS:Avatars', { + collectionName: 'rocketchat_avatars', + onRead +}); + + +new FileUploadClass({ + name: 'GridFS:Uploads', + get(file, req, res) { file = FileUpload.addExtensionTo(file); const headers = { @@ -134,8 +185,32 @@ FileUpload.addHandler('rocketchat_uploads', { 'Content-Length': file.size }; return readFromGridFS(file.store, file._id, file, headers, req, res); - }, - delete(file) { - return Meteor.fileStore.delete(file._id); + } +}); + +new FileUploadClass({ + name: 'GridFS:Avatars', + + get(file, req, res) { + const reqModifiedHeader = req.headers['if-modified-since']; + if (reqModifiedHeader) { + if (reqModifiedHeader === (file.uploadedAt && file.uploadedAt.toUTCString())) { + res.setHeader('Last-Modified', reqModifiedHeader); + res.writeHead(304); + res.end(); + return; + } + } + + file = FileUpload.addExtensionTo(file); + const headers = { + 'Cache-Control': 'public, max-age=0', + 'Expires': '-1', + 'Content-Disposition': 'inline', + 'Last-Modified': file.uploadedAt.toUTCString(), + 'Content-Type': file.type, + 'Content-Length': file.size + }; + return readFromGridFS(file.store, file._id, file, headers, req, res); } }); diff --git a/packages/rocketchat-file-upload/server/config/Slingshot_DEPRECATED.js b/packages/rocketchat-file-upload/server/config/Slingshot_DEPRECATED.js new file mode 100644 index 0000000000000000000000000000000000000000..45d245a4d3a402e51ffb48f17a6f5a83c42a3247 --- /dev/null +++ b/packages/rocketchat-file-upload/server/config/Slingshot_DEPRECATED.js @@ -0,0 +1,114 @@ +/* globals Slingshot, FileUpload */ + +const configureSlingshot = _.debounce(() => { + const type = RocketChat.settings.get('FileUpload_Storage_Type'); + const bucket = RocketChat.settings.get('FileUpload_S3_Bucket'); + const acl = RocketChat.settings.get('FileUpload_S3_Acl'); + const accessKey = RocketChat.settings.get('FileUpload_S3_AWSAccessKeyId'); + const secretKey = RocketChat.settings.get('FileUpload_S3_AWSSecretAccessKey'); + const cdn = RocketChat.settings.get('FileUpload_S3_CDN'); + const region = RocketChat.settings.get('FileUpload_S3_Region'); + const bucketUrl = RocketChat.settings.get('FileUpload_S3_BucketURL'); + + delete Slingshot._directives['rocketchat-uploads']; + + if (type === 'AmazonS3' && !_.isEmpty(bucket) && !_.isEmpty(accessKey) && !_.isEmpty(secretKey)) { + if (Slingshot._directives['rocketchat-uploads']) { + delete Slingshot._directives['rocketchat-uploads']; + } + const config = { + bucket, + key(file, metaContext) { + const id = Random.id(); + const path = `${ RocketChat.settings.get('uniqueID') }/uploads/${ metaContext.rid }/${ this.userId }/${ id }`; + + const upload = { + _id: id, + rid: metaContext.rid, + AmazonS3: { + path + } + }; + + RocketChat.models.Uploads.insertFileInit(this.userId, 'AmazonS3:Uploads', file, upload); + + return path; + }, + AWSAccessKeyId: accessKey, + AWSSecretAccessKey: secretKey + }; + + if (!_.isEmpty(acl)) { + config.acl = acl; + } + + if (!_.isEmpty(cdn)) { + config.cdn = cdn; + } + + if (!_.isEmpty(region)) { + config.region = region; + } + + if (!_.isEmpty(bucketUrl)) { + config.bucketUrl = bucketUrl; + } + + try { + Slingshot.createDirective('rocketchat-uploads', Slingshot.S3Storage, config); + } catch (e) { + console.error('Error configuring S3 ->', e.message); + } + } +}, 500); + +RocketChat.settings.get('FileUpload_Storage_Type', configureSlingshot); +RocketChat.settings.get(/^FileUpload_S3_/, configureSlingshot); + + + +const createGoogleStorageDirective = _.debounce(() => { + const type = RocketChat.settings.get('FileUpload_Storage_Type'); + const bucket = RocketChat.settings.get('FileUpload_GoogleStorage_Bucket'); + const accessId = RocketChat.settings.get('FileUpload_GoogleStorage_AccessId'); + const secret = RocketChat.settings.get('FileUpload_GoogleStorage_Secret'); + + delete Slingshot._directives['rocketchat-uploads-gs']; + + if (type === 'GoogleCloudStorage' && !_.isEmpty(secret) && !_.isEmpty(accessId) && !_.isEmpty(bucket)) { + if (Slingshot._directives['rocketchat-uploads-gs']) { + delete Slingshot._directives['rocketchat-uploads-gs']; + } + + const config = { + bucket, + GoogleAccessId: accessId, + GoogleSecretKey: secret, + key(file, metaContext) { + const id = Random.id(); + const path = `${ RocketChat.settings.get('uniqueID') }/uploads/${ metaContext.rid }/${ this.userId }/${ id }`; + + const upload = { + _id: id, + rid: metaContext.rid, + GoogleStorage: { + path + } + }; + + RocketChat.models.Uploads.insertFileInit(this.userId, 'GoogleCloudStorage:Uploads', file, upload); + + return path; + } + }; + + try { + Slingshot.createDirective('rocketchat-uploads-gs', Slingshot.GoogleCloud, config); + } catch (e) { + console.error('Error configuring GoogleCloudStorage ->', e.message); + } + } +}, 500); + +RocketChat.settings.get('FileUpload_Storage_Type', createGoogleStorageDirective); +RocketChat.settings.get(/^FileUpload_GoogleStorage_/, createGoogleStorageDirective); diff --git a/packages/rocketchat-file-upload/server/config/_configUploadStorage.js b/packages/rocketchat-file-upload/server/config/_configUploadStorage.js new file mode 100644 index 0000000000000000000000000000000000000000..df56dc4d307a38d37edd0bf74a65e17fabca3708 --- /dev/null +++ b/packages/rocketchat-file-upload/server/config/_configUploadStorage.js @@ -0,0 +1,19 @@ +/* globals UploadFS */ + +import './AmazonS3.js'; +import './FileSystem.js'; +import './GoogleStorage.js'; +import './GridFS.js'; +import './Slingshot_DEPRECATED.js'; + +const configStore = _.debounce(() => { + const store = RocketChat.settings.get('FileUpload_Storage_Type'); + + if (store) { + console.log('Setting default file store to', store); + UploadFS.getStores().Avatars = UploadFS.getStore(`${ store }:Avatars`); + UploadFS.getStores().Uploads = UploadFS.getStore(`${ store }:Uploads`); + } +}, 1000); + +RocketChat.settings.get(/^FileUpload_/, configStore); diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js b/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js deleted file mode 100644 index daac0ea19932a009785e5e84f570348d34f8e558..0000000000000000000000000000000000000000 --- a/packages/rocketchat-file-upload/server/config/configFileUploadAmazonS3.js +++ /dev/null @@ -1,129 +0,0 @@ -/* globals Slingshot, FileUpload, AWS, SystemLogger */ -const crypto = Npm.require('crypto'); - -let S3accessKey; -let S3secretKey; -let S3expiryTimeSpan; - -const generateURL = function(file) { - if (!file || !file.s3) { - return; - } - const resourceURL = `/${ file.s3.bucket }/${ file.s3.path }${ file._id }`; - const expires = parseInt(new Date().getTime() / 1000) + Math.max(5, S3expiryTimeSpan); - const StringToSign = `GET\n\n\n${ expires }\n${ resourceURL }`; - const signature = crypto.createHmac('sha1', S3secretKey).update(new Buffer(StringToSign, 'utf-8')).digest('base64'); - return `${ file.url }?AWSAccessKeyId=${ encodeURIComponent(S3accessKey) }&Expires=${ expires }&Signature=${ encodeURIComponent(signature) }`; -}; - -FileUpload.addHandler('s3', { - get(file, req, res) { - const fileUrl = generateURL(file); - - if (fileUrl) { - res.setHeader('Location', fileUrl); - res.writeHead(302); - } - res.end(); - }, - delete(file) { - const s3 = new AWS.S3(); - const request = s3.deleteObject({ - Bucket: file.s3.bucket, - Key: file.s3.path + file._id - }); - request.send(); - } -}); - -const createS3Directive = _.debounce(() => { - const directiveName = 'rocketchat-uploads'; - - const type = RocketChat.settings.get('FileUpload_Storage_Type'); - const bucket = RocketChat.settings.get('FileUpload_S3_Bucket'); - const acl = RocketChat.settings.get('FileUpload_S3_Acl'); - const accessKey = RocketChat.settings.get('FileUpload_S3_AWSAccessKeyId'); - const secretKey = RocketChat.settings.get('FileUpload_S3_AWSSecretAccessKey'); - const cdn = RocketChat.settings.get('FileUpload_S3_CDN'); - const region = RocketChat.settings.get('FileUpload_S3_Region'); - const bucketUrl = RocketChat.settings.get('FileUpload_S3_BucketURL'); - - AWS.config.update({ - accessKeyId: RocketChat.settings.get('FileUpload_S3_AWSAccessKeyId'), - secretAccessKey: RocketChat.settings.get('FileUpload_S3_AWSSecretAccessKey') - }); - - if (type === 'AmazonS3' && !_.isEmpty(bucket) && !_.isEmpty(accessKey) && !_.isEmpty(secretKey)) { - if (Slingshot._directives[directiveName]) { - delete Slingshot._directives[directiveName]; - } - const config = { - bucket, - AWSAccessKeyId: accessKey, - AWSSecretAccessKey: secretKey, - key(file, metaContext) { - const path = `${ RocketChat.hostname }/${ metaContext.rid }/${ this.userId }/`; - - const upload = { s3: { - bucket, - region, - path - }}; - const fileId = RocketChat.models.Uploads.insertFileInit(metaContext.rid, this.userId, 's3', file, upload); - - return path + fileId; - } - }; - - if (!_.isEmpty(acl)) { - config.acl = acl; - } - - if (!_.isEmpty(cdn)) { - config.cdn = cdn; - } - - if (!_.isEmpty(region)) { - config.region = region; - } - - if (!_.isEmpty(bucketUrl)) { - config.bucketUrl = bucketUrl; - } - - try { - Slingshot.createDirective(directiveName, Slingshot.S3Storage, config); - } catch (e) { - SystemLogger.error('Error configuring S3 ->', e.message); - } - } else if (Slingshot._directives[directiveName]) { - delete Slingshot._directives[directiveName]; - } -}, 500); - -RocketChat.settings.get('FileUpload_Storage_Type', createS3Directive); - -RocketChat.settings.get('FileUpload_S3_Bucket', createS3Directive); - -RocketChat.settings.get('FileUpload_S3_Acl', createS3Directive); - -RocketChat.settings.get('FileUpload_S3_AWSAccessKeyId', function(key, value) { - S3accessKey = value; - createS3Directive(); -}); - -RocketChat.settings.get('FileUpload_S3_AWSSecretAccessKey', function(key, value) { - S3secretKey = value; - createS3Directive(); -}); - -RocketChat.settings.get('FileUpload_S3_URLExpiryTimeSpan', function(key, value) { - S3expiryTimeSpan = value; - createS3Directive(); -}); - -RocketChat.settings.get('FileUpload_S3_CDN', createS3Directive); - -RocketChat.settings.get('FileUpload_S3_Region', createS3Directive); - -RocketChat.settings.get('FileUpload_S3_BucketURL', createS3Directive); diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadFileSystem.js b/packages/rocketchat-file-upload/server/config/configFileUploadFileSystem.js deleted file mode 100644 index f97534577abf7a04fe239936cbcb7845258ce9f4..0000000000000000000000000000000000000000 --- a/packages/rocketchat-file-upload/server/config/configFileUploadFileSystem.js +++ /dev/null @@ -1,79 +0,0 @@ -/* globals FileSystemStore:true, FileUpload, UploadFS, RocketChatFile */ - -const storeName = 'fileSystem'; - -FileSystemStore = null; - -const createFileSystemStore = _.debounce(function() { - const stores = UploadFS.getStores(); - if (stores[storeName]) { - delete stores[storeName]; - } - FileSystemStore = new UploadFS.store.Local({ - collection: RocketChat.models.Uploads.model, - name: storeName, - path: RocketChat.settings.get('FileUpload_FileSystemPath'), //'/tmp/uploads/photos', - filter: new UploadFS.Filter({ - onCheck: FileUpload.validateFileUpload - }), - transformWrite(readStream, writeStream, fileId, file) { - if (RocketChatFile.enabled === false || !/^image\/((x-windows-)?bmp|p?jpeg|png)$/.test(file.type)) { - return readStream.pipe(writeStream); - } - - let stream = undefined; - - const identify = function(err, data) { - if (err != null) { - return stream.pipe(writeStream); - } - - file.identify = { - format: data.format, - size: data.size - }; - - if ([null, undefined, '', 'Unknown', 'Undefined'].indexOf(data.Orientation) === -1) { - return RocketChatFile.gm(stream).autoOrient().stream().pipe(writeStream); - } else { - return stream.pipe(writeStream); - } - }; - - stream = RocketChatFile.gm(readStream).identify(identify).stream(); - return; - } - }); -}, 500); - -RocketChat.settings.get('FileUpload_FileSystemPath', createFileSystemStore); - -const fs = Npm.require('fs'); - -FileUpload.addHandler(storeName, { - get(file, req, res) { - const filePath = FileSystemStore.getFilePath(file._id, file); - - try { - const stat = Meteor.wrapAsync(fs.stat)(filePath); - - if (stat && stat.isFile()) { - file = FileUpload.addExtensionTo(file); - res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${ encodeURIComponent(file.name) }`); - res.setHeader('Last-Modified', file.uploadedAt.toUTCString()); - res.setHeader('Content-Type', file.type); - res.setHeader('Content-Length', file.size); - - FileSystemStore.getReadStream(file._id, file).pipe(res); - } - } catch (e) { - res.writeHead(404); - res.end(); - return; - } - }, - - delete(file) { - return FileSystemStore.delete(file._id); - } -}); diff --git a/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js b/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js deleted file mode 100644 index 9e8d5219ba60514b8d739e9e2fb3f901a82893da..0000000000000000000000000000000000000000 --- a/packages/rocketchat-file-upload/server/config/configFileUploadGoogleStorage.js +++ /dev/null @@ -1,111 +0,0 @@ -/* globals FileUpload, Slingshot, SystemLogger */ - -const crypto = Npm.require('crypto'); - -function generateUrlParts({ file }) { - const accessId = RocketChat.settings.get('FileUpload_GoogleStorage_AccessId'); - const secret = RocketChat.settings.get('FileUpload_GoogleStorage_Secret'); - - if (!file || !file.googleCloudStorage || _.isEmpty(accessId) || _.isEmpty(secret)) { - return; - } - - return { - accessId: encodeURIComponent(accessId), - secret, - path: file.googleCloudStorage.path + file._id - }; -} - -function generateGetURL({ file }) { - const parts = generateUrlParts({ file }); - - if (!parts) { - return; - } - - const expires = new Date().getTime() + 120000; - const signature = crypto.createSign('RSA-SHA256').update(`GET\n\n\n${ expires }\n/${ file.googleCloudStorage.bucket }/${ parts.path }`).sign(parts.secret, 'base64'); - - return `${ file.url }?GoogleAccessId=${ parts.accessId }&Expires=${ expires }&Signature=${ encodeURIComponent(signature) }`; -} - -function generateDeleteUrl({ file }) { - const parts = generateUrlParts({ file }); - - if (!parts) { - return; - } - - const expires = new Date().getTime() + 5000; - const signature = crypto.createSign('RSA-SHA256').update(`DELETE\n\n\n${ expires }\n/${ file.googleCloudStorage.bucket }/${ encodeURIComponent(parts.path) }`).sign(parts.secret, 'base64'); - - return `https://${ file.googleCloudStorage.bucket }.storage.googleapis.com/${ encodeURIComponent(parts.path) }?GoogleAccessId=${ parts.accessId }&Expires=${ expires }&Signature=${ encodeURIComponent(signature) }`; -} - -FileUpload.addHandler('googleCloudStorage', { - get(file, req, res) { - const fileUrl = generateGetURL({ file }); - - if (fileUrl) { - res.setHeader('Location', fileUrl); - res.writeHead(302); - } - res.end(); - }, - delete(file) { - if (!file || !file.googleCloudStorage) { - console.warn('Failed to delete a file which is uploaded to Google Cloud Storage, the file and googleCloudStorage properties are not defined.'); - return; - } - - const url = generateDeleteUrl({ file }); - - if (_.isEmpty(url)) { - console.warn('Failed to delete a file which is uploaded to Google Cloud Storage, failed to generate a delete url.'); - return; - } - - HTTP.call('DELETE', url); - } -}); - -const createGoogleStorageDirective = _.debounce(() => { - const directiveName = 'rocketchat-uploads-gs'; - - const type = RocketChat.settings.get('FileUpload_Storage_Type'); - const bucket = RocketChat.settings.get('FileUpload_GoogleStorage_Bucket'); - const accessId = RocketChat.settings.get('FileUpload_GoogleStorage_AccessId'); - const secret = RocketChat.settings.get('FileUpload_GoogleStorage_Secret'); - - if (type === 'GoogleCloudStorage' && !_.isEmpty(secret) && !_.isEmpty(accessId) && !_.isEmpty(bucket)) { - if (Slingshot._directives[directiveName]) { - delete Slingshot._directives[directiveName]; - } - - const config = { - bucket, - GoogleAccessId: accessId, - GoogleSecretKey: secret, - key: function _googleCloudStorageKey(file, metaContext) { - const path = `${ RocketChat.settings.get('uniqueID') }/${ metaContext.rid }/${ this.userId }/`; - const fileId = RocketChat.models.Uploads.insertFileInit(metaContext.rid, this.userId, 'googleCloudStorage', file, { googleCloudStorage: { bucket, path }}); - - return path + fileId; - } - }; - - try { - Slingshot.createDirective(directiveName, Slingshot.GoogleCloud, config); - } catch (e) { - SystemLogger.error('Error configuring GoogleCloudStorage ->', e.message); - } - } else { - delete Slingshot._directives[directiveName]; - } -}, 500); - -RocketChat.settings.get('FileUpload_Storage_Type', createGoogleStorageDirective); -RocketChat.settings.get('FileUpload_GoogleStorage_Bucket', createGoogleStorageDirective); -RocketChat.settings.get('FileUpload_GoogleStorage_AccessId', createGoogleStorageDirective); -RocketChat.settings.get('FileUpload_GoogleStorage_Secret', createGoogleStorageDirective); diff --git a/packages/rocketchat-file-upload/server/lib/FileUpload.js b/packages/rocketchat-file-upload/server/lib/FileUpload.js index cb0aadbc06c9d3f37783c48d74d24572e3a3a045..748e8fa7336d279bfff56ca41b153f20e8045de9 100644 --- a/packages/rocketchat-file-upload/server/lib/FileUpload.js +++ b/packages/rocketchat-file-upload/server/lib/FileUpload.js @@ -1,43 +1,297 @@ -/* globals FileUpload:true */ +/* globals UploadFS */ + +import fs from 'fs'; +import stream from 'stream'; import mime from 'mime-type/with-db'; +import Future from 'fibers/future'; + +Object.assign(FileUpload, { + handlers: {}, + + configureUploadsStore(store, name, options) { + const type = name.split(':').pop(); + const stores = UploadFS.getStores(); + delete stores[name]; + + return new UploadFS.store[store](Object.assign({ + name + }, options, FileUpload[`default${ type }`]())); + }, + + defaultUploads() { + return { + collection: RocketChat.models.Uploads.model, + filter: new UploadFS.Filter({ + onCheck: FileUpload.validateFileUpload + }), + getPath(file) { + return `${ RocketChat.settings.get('uniqueID') }/uploads/${ file.rid }/${ file.userId }/${ file._id }`; + }, + // transformWrite: FileUpload.uploadsTransformWrite + onValidate: FileUpload.uploadsOnValidate + }; + }, + + defaultAvatars() { + return { + collection: RocketChat.models.Avatars.model, + // filter: new UploadFS.Filter({ + // onCheck: FileUpload.validateFileUpload + // }), + // transformWrite: FileUpload.avatarTransformWrite, + getPath(file) { + return `${ RocketChat.settings.get('uniqueID') }/avatars/${ file.userId }`; + }, + onValidate: FileUpload.avatarsOnValidate, + onFinishUpload: FileUpload.avatarsOnFinishUpload + }; + }, + + avatarTransformWrite(readStream, writeStream/*, fileId, file*/) { + if (RocketChatFile.enabled === false || RocketChat.settings.get('Accounts_AvatarResize') !== true) { + return readStream.pipe(writeStream); + } + const height = RocketChat.settings.get('Accounts_AvatarSize'); + const width = height; + return RocketChatFile.gm(readStream).background('#ffffff').resize(width, `${ height }^`).gravity('Center').crop(width, height).extent(width, height).stream('jpeg').pipe(writeStream); + }, + + avatarsOnValidate(file) { + if (RocketChatFile.enabled === false || RocketChat.settings.get('Accounts_AvatarResize') !== true) { + return; + } + + const tmpFile = UploadFS.getTempFilePath(file._id); + + const fut = new Future(); + + const height = RocketChat.settings.get('Accounts_AvatarSize'); + const width = height; + + RocketChatFile.gm(tmpFile).background('#ffffff').resize(width, `${ height }^`).gravity('Center').crop(width, height).extent(width, height).setFormat('jpeg').write(tmpFile, Meteor.bindEnvironment((err) => { + if (err != null) { + console.error(err); + } + + const size = fs.lstatSync(tmpFile).size; + this.getCollection().direct.update({_id: file._id}, {$set: {size}}); + fut.return(); + })); + + return fut.wait(); + }, + + uploadsTransformWrite(readStream, writeStream, fileId, file) { + if (RocketChatFile.enabled === false || !/^image\/.+/.test(file.type)) { + return readStream.pipe(writeStream); + } + + let stream = undefined; -FileUpload.handlers = {}; + const identify = function(err, data) { + if (err) { + return stream.pipe(writeStream); + } -FileUpload.addHandler = function(store, handler) { - this.handlers[store] = handler; -}; + file.identify = { + format: data.format, + size: data.size + }; -FileUpload.delete = function(fileId) { - const file = RocketChat.models.Uploads.findOneById(fileId); + if (data.Orientation && !['', 'Unknown', 'Undefined'].includes(data.Orientation)) { + RocketChatFile.gm(stream).autoOrient().stream().pipe(writeStream); + } else { + stream.pipe(writeStream); + } + }; - if (!file) { - return; + stream = RocketChatFile.gm(readStream).identify(identify).stream(); + }, + + uploadsOnValidate(file) { + if (RocketChatFile.enabled === false || !/^image\/((x-windows-)?bmp|p?jpeg|png)$/.test(file.type)) { + return; + } + + const tmpFile = UploadFS.getTempFilePath(file._id); + + const fut = new Future(); + + const identify = Meteor.bindEnvironment((err, data) => { + if (err != null) { + console.error(err); + return fut.return(); + } + + file.identify = { + format: data.format, + size: data.size + }; + + if ([null, undefined, '', 'Unknown', 'Undefined'].includes(data.Orientation)) { + return fut.return(); + } + + RocketChatFile.gm(tmpFile).autoOrient().write(tmpFile, Meteor.bindEnvironment((err) => { + if (err != null) { + console.error(err); + } + + const size = fs.lstatSync(tmpFile).size; + this.getCollection().direct.update({_id: file._id}, {$set: {size}}); + fut.return(); + })); + }); + + RocketChatFile.gm(tmpFile).identify(identify); + + return fut.wait(); + }, + + avatarsOnFinishUpload(file) { + // update file record to match user's username + const user = RocketChat.models.Users.findOneById(file.userId); + const oldAvatar = RocketChat.models.Avatars.findOneByName(user.username); + if (oldAvatar) { + RocketChat.models.Avatars.deleteFile(oldAvatar._id); + } + RocketChat.models.Avatars.updateFileNameById(file._id, user.username); + // console.log('upload finished ->', file); + }, + + addExtensionTo(file) { + if (mime.lookup(file.name) === file.type) { + return file; + } + + const ext = mime.extension(file.type); + if (ext && false === new RegExp(`\.${ ext }$`, 'i').test(file.name)) { + file.name = `${ file.name }.${ ext }`; + } + + return file; + }, + + getStore(modelName) { + const storageType = RocketChat.settings.get('FileUpload_Storage_Type'); + const handlerName = `${ storageType }:${ modelName }`; + + return this.getStoreByName(handlerName); + }, + + getStoreByName(handlerName) { + if (this.handlers[handlerName] == null) { + console.error(`Upload handler "${ handlerName }" does not exists`); + } + + return this.handlers[handlerName]; + }, + + get(file, req, res, next) { + if (file.store && this.handlers && this.handlers[file.store] && this.handlers[file.store].get) { + this.handlers[file.store].get(file, req, res, next); + } else { + res.writeHead(404); + res.end(); + return; + } } +}); + - this.handlers[file.store].delete(file); +export class FileUploadClass { + constructor({ name, model, store, get, insert, getStore }) { + this.name = name; + this.model = model || this.getModelFromName(); + this._store = store || UploadFS.getStore(name); + this.get = get; - return RocketChat.models.Uploads.remove(file._id); -}; + if (insert) { + this.insert = insert; + } -FileUpload.get = function(file, req, res, next) { - if (file.store && this.handlers && this.handlers[file.store] && this.handlers[file.store].get) { - this.handlers[file.store].get.call(this, file, req, res, next); - } else { - res.writeHead(404); - res.end(); - return; + if (getStore) { + this.getStore = getStore; + } + + FileUpload.handlers[name] = this; } -}; -FileUpload.addExtensionTo = function(file) { - if (mime.lookup(file.name) === file.type) { - return file; + getStore() { + return this._store; + } + + get store() { + return this.getStore(); + } + + set store(store) { + this._store = store; + } + + getModelFromName() { + return RocketChat.models[this.name.split(':')[1]]; + } + + delete(fileId) { + if (this.store && this.store.delete) { + this.store.delete(fileId); + } + + return this.model.deleteFile(fileId); } - const ext = mime.extension(file.type); - if (ext && false === new RegExp(`\.${ ext }$`, 'i').test(file.name)) { - file.name = `${ file.name }.${ ext }`; + deleteById(fileId) { + const file = this.model.findOneById(fileId); + + if (!file) { + return; + } + + const store = FileUpload.getStoreByName(file.store); + + return store.delete(file._id); } - return file; -}; + deleteByName(fileName) { + const file = this.model.findOneByName(fileName); + + if (!file) { + return; + } + + const store = FileUpload.getStoreByName(file.store); + + return store.delete(file._id); + } + + insert(fileData, streamOrBuffer, cb) { + const fileId = this.store.create(fileData); + const token = this.store.createToken(fileId); + const tmpFile = UploadFS.getTempFilePath(fileId); + + try { + if (streamOrBuffer instanceof stream) { + streamOrBuffer.pipe(fs.createWriteStream(tmpFile)); + } else if (streamOrBuffer instanceof Buffer) { + fs.writeFileSync(tmpFile, streamOrBuffer); + } else { + throw new Error('Invalid file type'); + } + + const file = Meteor.call('ufsComplete', fileId, this.name, token); + + if (cb) { + cb(null, file); + } + + return file; + } catch (e) { + if (cb) { + cb(e); + } else { + throw e; + } + } + } +} diff --git a/packages/rocketchat-file-upload/server/lib/proxy.js b/packages/rocketchat-file-upload/server/lib/proxy.js new file mode 100644 index 0000000000000000000000000000000000000000..c5636478c757e2c523e6a1c5148064e8427fd0af --- /dev/null +++ b/packages/rocketchat-file-upload/server/lib/proxy.js @@ -0,0 +1,91 @@ +/* globals UploadFS, InstanceStatus */ + +import http from 'http'; +import URL from 'url'; + +const logger = new Logger('UploadProxy'); + +WebApp.connectHandlers.stack.unshift({ + route: '', + handle: Meteor.bindEnvironment(function(req, res, next) { + // Quick check to see if request should be catch + if (req.url.indexOf(UploadFS.config.storesPath) === -1) { + return next(); + } + + logger.debug('Upload URL:', req.url); + + if (req.method !== 'POST') { + return next(); + } + + // Remove store path + const parsedUrl = URL.parse(req.url); + const path = parsedUrl.pathname.substr(UploadFS.config.storesPath.length + 1); + + // Get store + const regExp = new RegExp('^\/([^\/\?]+)\/([^\/\?]+)$'); + const match = regExp.exec(path); + + // Request is not valid + if (match === null) { + res.writeHead(400); + res.end(); + return; + } + + // Get store + const store = UploadFS.getStore(match[1]); + if (!store) { + res.writeHead(404); + res.end(); + return; + } + + // Get file + const fileId = match[2]; + const file = store.getCollection().findOne({_id: fileId}); + if (file === undefined) { + res.writeHead(404); + res.end(); + return; + } + + if (file.instanceId === InstanceStatus.id()) { + logger.debug('Correct instance'); + return next(); + } + + // Proxy to other instance + const instance = InstanceStatus.getCollection().findOne({_id: file.instanceId}); + + if (instance == null) { + res.writeHead(404); + res.end(); + return; + } + + if (instance.extraInformation.host === process.env.INSTANCE_IP && RocketChat.isDocker() === false) { + instance.extraInformation.host = 'localhost'; + } + + logger.debug('Wrong instance, proxing to:', `${ instance.extraInformation.host }:${ instance.extraInformation.port }`); + + const options = { + hostname: instance.extraInformation.host, + port: instance.extraInformation.port, + path: req.url, + method: 'POST' + }; + + const proxy = http.request(options, function(proxy_res) { + proxy_res.pipe(res, { + end: true + }); + }); + + req.pipe(proxy, { + end: true + }); + }) +}); diff --git a/packages/rocketchat-file-upload/server/methods/getS3FileUrl.js b/packages/rocketchat-file-upload/server/methods/getS3FileUrl.js index ef48ddc2163b815644a57dd9712f39e474327a69..421740d18b8407e78a8a494aa9f1d055fe719ef3 100644 --- a/packages/rocketchat-file-upload/server/methods/getS3FileUrl.js +++ b/packages/rocketchat-file-upload/server/methods/getS3FileUrl.js @@ -1,37 +1,18 @@ -const crypto = Npm.require('crypto'); +/* globals UploadFS */ + let protectedFiles; -let S3accessKey; -let S3secretKey; -let S3expiryTimeSpan; RocketChat.settings.get('FileUpload_ProtectFiles', function(key, value) { protectedFiles = value; }); -RocketChat.settings.get('FileUpload_S3_AWSAccessKeyId', function(key, value) { - S3accessKey = value; -}); - -RocketChat.settings.get('FileUpload_S3_AWSSecretAccessKey', function(key, value) { - S3secretKey = value; -}); - -RocketChat.settings.get('FileUpload_S3_URLExpiryTimeSpan', function(key, value) { - S3expiryTimeSpan = value; -}); - Meteor.methods({ getS3FileUrl(fileId) { if (protectedFiles && !Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendFileMessage' }); } const file = RocketChat.models.Uploads.findOneById(fileId); - const resourceURL = `/${ file.s3.bucket }/${ file.s3.path }${ file._id }`; - const expires = parseInt(new Date().getTime() / 1000) + Math.max(5, S3expiryTimeSpan); - const StringToSign = `GET\n\n\n${ expires }\n${ resourceURL }`; - const signature = crypto.createHmac('sha1', S3secretKey).update(new Buffer(StringToSign, 'utf-8')).digest('base64'); - return { - url:`${ file.url }?AWSAccessKeyId=${ encodeURIComponent(S3accessKey) }&Expires=${ expires }&Signature=${ encodeURIComponent(signature) }` - }; + + return UploadFS.getStore('AmazonS3:Uploads').getRedirectURL(file); } }); diff --git a/packages/rocketchat-file-upload/ufs/AmazonS3/client.js b/packages/rocketchat-file-upload/ufs/AmazonS3/client.js new file mode 100644 index 0000000000000000000000000000000000000000..6d9f66f467a46a0e537ea81371c104c57d2dc8d7 --- /dev/null +++ b/packages/rocketchat-file-upload/ufs/AmazonS3/client.js @@ -0,0 +1,6 @@ +import {UploadFS} from 'meteor/jalik:ufs'; + +export class AmazonS3Store extends UploadFS.Store {} + +// Add store to UFS namespace +UploadFS.store.AmazonS3 = AmazonS3Store; diff --git a/packages/rocketchat-file-upload/ufs/AmazonS3/server.js b/packages/rocketchat-file-upload/ufs/AmazonS3/server.js new file mode 100644 index 0000000000000000000000000000000000000000..f69b05a883c408385164dad28c41d8bd2895f436 --- /dev/null +++ b/packages/rocketchat-file-upload/ufs/AmazonS3/server.js @@ -0,0 +1,156 @@ +import {_} from 'meteor/underscore'; +import {UploadFS} from 'meteor/jalik:ufs'; +import S3 from 'aws-sdk/clients/s3'; +import stream from 'stream'; + +/** + * AmazonS3 store + * @param options + * @constructor + */ +export class AmazonS3Store extends UploadFS.Store { + + constructor(options) { + // Default options + // options.secretAccessKey, + // options.accessKeyId, + // options.region, + // options.sslEnabled // optional + + options = _.extend({ + httpOptions: { + timeout: 6000, + agent: false + } + }, options); + + super(options); + + const classOptions = options; + + const s3 = new S3(options.connection); + + options.getPath = options.getPath || function(file) { + return file._id; + }; + + this.getPath = function(file) { + if (file.AmazonS3) { + return file.AmazonS3.path; + } + // Compatibility + // TODO: Migration + if (file.s3) { + return file.s3.path + file._id; + } + }; + + this.getRedirectURL = function(file) { + const params = { + Key: this.getPath(file), + Expires: classOptions.URLExpiryTimeSpan + }; + + return s3.getSignedUrl('getObject', params); + }; + + /** + * Creates the file in the collection + * @param file + * @param callback + * @return {string} + */ + this.create = function(file, callback) { + check(file, Object); + + if (file._id == null) { + file._id = Random.id(); + } + + file.AmazonS3 = { + path: this.options.getPath(file) + }; + + file.store = this.options.name; // assign store to file + return this.getCollection().insert(file, callback); + }; + + /** + * Removes the file + * @param fileId + * @param callback + */ + this.delete = function(fileId, callback) { + const file = this.getCollection().findOne({_id: fileId}); + const params = { + Key: this.getPath(file) + }; + + s3.deleteObject(params, (err, data) => { + if (err) { + console.error(err); + } + + callback && callback(err, data); + }); + }; + + /** + * Returns the file read stream + * @param fileId + * @param file + * @param options + * @return {*} + */ + this.getReadStream = function(fileId, file, options = {}) { + const params = { + Key: this.getPath(file) + }; + + if (options.start && options.end) { + params.Range = `${ options.start } - ${ options.end }`; + } + + return s3.getObject(params).createReadStream(); + }; + + /** + * Returns the file write stream + * @param fileId + * @param file + * @param options + * @return {*} + */ + this.getWriteStream = function(fileId, file/*, options*/) { + const writeStream = new stream.PassThrough(); + writeStream.length = file.size; + + writeStream.on('newListener', (event, listener) => { + if (event === 'finish') { + process.nextTick(() => { + writeStream.removeListener(event, listener); + writeStream.on('real_finish', listener); + }); + } + }); + + s3.putObject({ + Key: this.getPath(file), + Body: writeStream, + ContentType: file.type + + }, (error) => { + if (error) { + console.error(error); + } + + writeStream.emit('real_finish'); + }); + + return writeStream; + }; + } +} + +// Add store to UFS namespace +UploadFS.store.AmazonS3 = AmazonS3Store; diff --git a/packages/rocketchat-file-upload/ufs/GoogleStorage/client.js b/packages/rocketchat-file-upload/ufs/GoogleStorage/client.js new file mode 100644 index 0000000000000000000000000000000000000000..2c11dc0d68285e726060680703520fc113b85b8a --- /dev/null +++ b/packages/rocketchat-file-upload/ufs/GoogleStorage/client.js @@ -0,0 +1,6 @@ +import {UploadFS} from 'meteor/jalik:ufs'; + +export class GoogleStorageStore extends UploadFS.Store {} + +// Add store to UFS namespace +UploadFS.store.GoogleStorage = GoogleStorageStore; diff --git a/packages/rocketchat-file-upload/ufs/GoogleStorage/server.js b/packages/rocketchat-file-upload/ufs/GoogleStorage/server.js new file mode 100644 index 0000000000000000000000000000000000000000..8a764caac732301980d4a504846e44c6997264c9 --- /dev/null +++ b/packages/rocketchat-file-upload/ufs/GoogleStorage/server.js @@ -0,0 +1,123 @@ +import {UploadFS} from 'meteor/jalik:ufs'; +import gcStorage from '@google-cloud/storage'; + +/** + * GoogleStorage store + * @param options + * @constructor + */ +export class GoogleStorageStore extends UploadFS.Store { + + constructor(options) { + super(options); + + const gcs = gcStorage(options.connection); + this.bucket = gcs.bucket(options.bucket); + + options.getPath = options.getPath || function(file) { + return file._id; + }; + + this.getPath = function(file) { + if (file.GoogleStorage) { + return file.GoogleStorage.path; + } + // Compatibility + // TODO: Migration + if (file.googleCloudStorage) { + return file.googleCloudStorage.path + file._id; + } + }; + + this.getRedirectURL = function(file, callback) { + const params = { + action: 'read', + responseDisposition: 'inline', + expires: Date.now()+this.options.URLExpiryTimeSpan*1000 + }; + + this.bucket.file(this.getPath(file)).getSignedUrl(params, callback); + }; + + /** + * Creates the file in the collection + * @param file + * @param callback + * @return {string} + */ + this.create = function(file, callback) { + check(file, Object); + + if (file._id == null) { + file._id = Random.id(); + } + + file.GoogleStorage = { + path: this.options.getPath(file) + }; + + file.store = this.options.name; // assign store to file + return this.getCollection().insert(file, callback); + }; + + /** + * Removes the file + * @param fileId + * @param callback + */ + this.delete = function(fileId, callback) { + const file = this.getCollection().findOne({_id: fileId}); + this.bucket.file(this.getPath(file)).delete(function(err, data) { + if (err) { + console.error(err); + } + + callback && callback(err, data); + }); + }; + + /** + * Returns the file read stream + * @param fileId + * @param file + * @param options + * @return {*} + */ + this.getReadStream = function(fileId, file, options = {}) { + const config = {}; + + if (options.start != null) { + config.start = options.start; + } + + if (options.end != null) { + config.end = options.end; + } + + return this.bucket.file(this.getPath(file)).createReadStream(config); + }; + + /** + * Returns the file write stream + * @param fileId + * @param file + * @param options + * @return {*} + */ + this.getWriteStream = function(fileId, file/*, options*/) { + return this.bucket.file(this.getPath(file)).createWriteStream({ + gzip: false, + metadata: { + contentType: file.type, + contentDisposition: `inline; filename=${ file.name }` + // metadata: { + // custom: 'metadata' + // } + } + }); + }; + } +} + +// Add store to UFS namespace +UploadFS.store.GoogleStorage = GoogleStorageStore; diff --git a/packages/rocketchat-file/file.server.js b/packages/rocketchat-file/file.server.js index 500513a58e54b032ab8c5951f0ee79a859c74bb7..1fb0487184e69d7012386dba23efe0260c9b7dcd 100644 --- a/packages/rocketchat-file/file.server.js +++ b/packages/rocketchat-file/file.server.js @@ -105,6 +105,7 @@ RocketChatFile.GridFS = class { this.store = new Grid(db, mongo); this.findOneSync = Meteor.wrapAsync(this.store.collection(this.name).findOne.bind(this.store.collection(this.name))); this.removeSync = Meteor.wrapAsync(this.store.remove.bind(this.store)); + this.countSync = Meteor.wrapAsync(this.store._col.count.bind(this.store._col)); this.getFileSync = Meteor.wrapAsync(this.getFile.bind(this)); } diff --git a/packages/rocketchat-github-enterprise/github-enterprise-login-button.css b/packages/rocketchat-github-enterprise/github-enterprise-login-button.css index d8593c928a4d5cc10dc4e86feb83d8697f4f6457..3213c93ad8ff911d7319006b5e850d10d53e6d82 100644 --- a/packages/rocketchat-github-enterprise/github-enterprise-login-button.css +++ b/packages/rocketchat-github-enterprise/github-enterprise-login-button.css @@ -1,4 +1,4 @@ -.icon-github_enterprise:before { +.icon-github_enterprise::before { content: ""; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wJGBYxHYxl31wAAAHpSURBVDjLpZI/aFNRFMZ/973bJqGRPopV4qNq/+SpTYnWRhCKilShg9BGcHOM+GfQoZuLk4iLgw4qZNBaHLuIdBNHl7Ta1qdNFI3SihnaNG1MpH3vuiQYQnwZvHCG893zffc751z4z6PX5T5gA1DAKnAaOAQEgAfAVeCpl+CeCrlRuEC6maO4h0A1wl4tPAHMqNUthvrDdHYY7A3t4rDVjeO6rBU2FaABM1WCrBNoi48Mi+nH9yj+KtPibAKwJXfQ5vcRG7soUnYmWEuQgAEIYBv4cGpoILI0Z4tyYYPegS6UguyijZQ6J45GSNmZHzUcJYD2ii2Ajv7efZ8WZ6ZwXFj79hXpayW4O0SL1Nl/8jzZlZ9dQLFS70pgvZKIyGD0yvu5eRmMnrk1PjI81ir1qBACTdPevXj95mVuNX8XKDQc/+T334bZZ104cvzYw2s3J3qAL5WXSsDbf61NNMBu+wOBs+VSyQ84Nfhg028ZGx3/qyy0lC7lgi7lghBitoon03lvB8l0/k7Wnk+8mny0cyXzEcfZxgwfZPTyRMHsOzAFXE9YhtNQIJnOx4FpJXT1eSkn2g0frqMoFrfoCXcqlCOAGwnLuO/l4JymcWl5uRxzXUKghBAiZ5r+WaV4lrCM555zqO+x2d0ftGmpiA/0k70AAAAASUVORK5CYII=); height: 1em; diff --git a/packages/rocketchat-gitlab/gitlab-login-button.css b/packages/rocketchat-gitlab/gitlab-login-button.css index d417a55741ade9a67448825f562bd96bbc9334e9..c73a1ee23c91fa39cb9ee784bfea81595875dd39 100644 --- a/packages/rocketchat-gitlab/gitlab-login-button.css +++ b/packages/rocketchat-gitlab/gitlab-login-button.css @@ -1,3 +1,3 @@ #login-buttons-image-gitlab { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wJGBYxHYxl31wAAAHpSURBVDjLpZI/aFNRFMZ/973bJqGRPopV4qNq/+SpTYnWRhCKilShg9BGcHOM+GfQoZuLk4iLgw4qZNBaHLuIdBNHl7Ta1qdNFI3SihnaNG1MpH3vuiQYQnwZvHCG893zffc751z4z6PX5T5gA1DAKnAaOAQEgAfAVeCpl+CeCrlRuEC6maO4h0A1wl4tPAHMqNUthvrDdHYY7A3t4rDVjeO6rBU2FaABM1WCrBNoi48Mi+nH9yj+KtPibAKwJXfQ5vcRG7soUnYmWEuQgAEIYBv4cGpoILI0Z4tyYYPegS6UguyijZQ6J45GSNmZHzUcJYD2ii2Ajv7efZ8WZ6ZwXFj79hXpayW4O0SL1Nl/8jzZlZ9dQLFS70pgvZKIyGD0yvu5eRmMnrk1PjI81ir1qBACTdPevXj95mVuNX8XKDQc/+T334bZZ104cvzYw2s3J3qAL5WXSsDbf61NNMBu+wOBs+VSyQ84Nfhg028ZGx3/qyy0lC7lgi7lghBitoon03lvB8l0/k7Wnk+8mny0cyXzEcfZxgwfZPTyRMHsOzAFXE9YhtNQIJnOx4FpJXT1eSkn2g0frqMoFrfoCXcqlCOAGwnLuO/l4JymcWl5uRxzXUKghBAiZ5r+WaV4lrCM555zqO+x2d0ftGmpiA/0k70AAAAASUVORK5CYII=); + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wJGBYxHYxl31wAAAHpSURBVDjLpZI/aFNRFMZ/973bJqGRPopV4qNq/+SpTYnWRhCKilShg9BGcHOM+GfQoZuLk4iLgw4qZNBaHLuIdBNHl7Ta1qdNFI3SihnaNG1MpH3vuiQYQnwZvHCG893zffc751z4z6PX5T5gA1DAKnAaOAQEgAfAVeCpl+CeCrlRuEC6maO4h0A1wl4tPAHMqNUthvrDdHYY7A3t4rDVjeO6rBU2FaABM1WCrBNoi48Mi+nH9yj+KtPibAKwJXfQ5vcRG7soUnYmWEuQgAEIYBv4cGpoILI0Z4tyYYPegS6UguyijZQ6J45GSNmZHzUcJYD2ii2Ajv7efZ8WZ6ZwXFj79hXpayW4O0SL1Nl/8jzZlZ9dQLFS70pgvZKIyGD0yvu5eRmMnrk1PjI81ir1qBACTdPevXj95mVuNX8XKDQc/+T334bZZ104cvzYw2s3J3qAL5WXSsDbf61NNMBu+wOBs+VSyQ84Nfhg028ZGx3/qyy0lC7lgi7lghBitoon03lvB8l0/k7Wnk+8mny0cyXzEcfZxgwfZPTyRMHsOzAFXE9YhtNQIJnOx4FpJXT1eSkn2g0frqMoFrfoCXcqlCOAGwnLuO/l4JymcWl5uRxzXUKghBAiZ5r+WaV4lrCM555zqO+x2d0ftGmpiA/0k70AAAAASUVORK5CYII=); } 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 e171621ad42f49f5aeb214b231cd0a9f40a36a58..d4e27909db33494d0eb875980b57a4088320664f 100644 --- a/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json +++ b/packages/rocketchat-google-natural-language/.npm/package/npm-shrinkwrap.json @@ -322,8 +322,8 @@ "from": "graceful-readlink@>=1.0.0" }, "grpc": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.3.1.tgz", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.3.2.tgz", "from": "grpc@>=1.1.0 <2.0.0", "dependencies": { "node-pre-gyp": { @@ -373,8 +373,8 @@ } }, "npmlog": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.0.2.tgz", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.0.tgz", "from": "npmlog@>=4.0.2 <5.0.0", "dependencies": { "are-we-there-yet": { @@ -439,7 +439,7 @@ "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "from": "gauge@>=2.7.1 <2.8.0", + "from": "gauge@>=2.7.3 <2.8.0", "dependencies": { "aproba": { "version": "1.1.1", @@ -498,8 +498,8 @@ } }, "wide-align": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.0.tgz", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", "from": "wide-align@>=1.1.0 <2.0.0" } } @@ -517,8 +517,8 @@ "from": "rc@>=1.1.7 <2.0.0", "dependencies": { "deep-extend": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", "from": "deep-extend@>=0.4.0 <0.5.0" }, "ini": { @@ -855,14 +855,14 @@ "from": "inherits@>=2.0.0 <3.0.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.0 <4.0.0", "dependencies": { "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", "dependencies": { "balanced-match": { "version": "0.4.2", @@ -963,7 +963,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "from": "inherits@>=2.0.1 <2.1.0" + "from": "inherits@>=2.0.0 <2.1.0" } } }, @@ -978,14 +978,14 @@ "from": "inherits@>=2.0.0 <3.0.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.0 <4.0.0", "dependencies": { "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", "dependencies": { "balanced-match": { "version": "0.4.2", @@ -1006,7 +1006,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "from": "once@>=1.0.0 <2.0.0", + "from": "once@>=1.3.3 <2.0.0", "dependencies": { "wrappy": { "version": "1.0.2", @@ -1033,7 +1033,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "from": "inherits@>=2.0.1 <2.1.0" + "from": "inherits@>=2.0.0 <2.1.0" }, "isarray": { "version": "1.0.0", @@ -1246,8 +1246,8 @@ "from": "methmeth@>=1.1.0 <2.0.0" }, "mime": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.6.tgz", "from": "mime@>=1.2.11 <2.0.0" }, "mime-db": { diff --git a/packages/rocketchat-i18n/i18n/ar.i18n.json b/packages/rocketchat-i18n/i18n/ar.i18n.json index 3dbd8af238ace9d58bd7615a609ccbd816e7a2bd..5e4f6947acbb74a5bd6992c48b6951ab03932658 100644 --- a/packages/rocketchat-i18n/i18n/ar.i18n.json +++ b/packages/rocketchat-i18n/i18n/ar.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "السماح بتعديل الملف الشخصي للعضو", "Accounts_AvatarResize": "تغيير حجم الصور الرمزية", "Accounts_AvatarSize": "حجم الصورة الرمزية", - "Accounts_AvatarStorePath": "مسار تخزين الصورة الرمزية", - "Accounts_AvatarStoreType": "نوع تخزين الصورة الرمزية", "Accounts_BlockedDomainsList": "محظور قائمة المجالات", "Accounts_BlockedDomainsList_Description": "قائمة مفصولة بفواصل من المجالات سدت", "Accounts_BlockedUsernameList": "قائمة اﻷسماء المحظورة", diff --git a/packages/rocketchat-i18n/i18n/az.i18n.json b/packages/rocketchat-i18n/i18n/az.i18n.json new file mode 100644 index 0000000000000000000000000000000000000000..7cb0319c6130987a905ab2007a1e1127d8fce369 --- /dev/null +++ b/packages/rocketchat-i18n/i18n/az.i18n.json @@ -0,0 +1,3 @@ +{ + "#channel": "#kanal" +} \ No newline at end of file diff --git a/packages/rocketchat-i18n/i18n/ca.i18n.json b/packages/rocketchat-i18n/i18n/ca.i18n.json index 436bcca1e9de01d37326825effe7f30fbfa4fe04..d9b22df36000fb5f1d8811e5e0b3ffaad7ebf913 100644 --- a/packages/rocketchat-i18n/i18n/ca.i18n.json +++ b/packages/rocketchat-i18n/i18n/ca.i18n.json @@ -17,7 +17,8 @@ "Accessing_permissions": "L'accés als permisos", "Account_SID": "Compte SID", "Accounts": "Comptes", - "Accounts_AllowAnonymousAccess": "Permet accés anònim", + "Accounts_AllowAnonymousRead": "Permetre lectura anònima", + "Accounts_AllowAnonymousWrite": "Permetre escriptura anònima", "Accounts_AllowDeleteOwnAccount": "Permetre als usuaris eliminar el seu propi compte", "Accounts_AllowedDomainsList": "Llista de dominis permesos", "Accounts_AllowedDomainsList_Description": "Llista dels dominis permesos separada per comes ", @@ -28,13 +29,12 @@ "Accounts_AllowUserProfileChange": "Permetre modificar el perfil d'usuari", "Accounts_AvatarResize": "Canviar la mida dels avatars", "Accounts_AvatarSize": "Mida d'avatar", - "Accounts_AvatarStorePath": "Ruta d'emmagatzematge dels avatars", - "Accounts_AvatarStoreType": "Tipus d'emmagatzematge dels avatars", "Accounts_BlockedDomainsList": "Llista de dominis bloquejats", "Accounts_BlockedDomainsList_Description": "Llista de dominis bloquejats separada per comes", "Accounts_BlockedUsernameList": "Llista de noms d'usuari bloquejats", "Accounts_BlockedUsernameList_Description": "Llista separada per comes de noms d'usuari bloquejats (no distingeix majúscules/minúscules)", "Accounts_CustomFields_Description": "Ha de ser un objecte JSON vàlid on les claus són els noms dels camps i contenen un diccionari amb les opcions del camp. Exemple:
{\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} ", + "Accounts_DefaultUsernamePrefixSuggestion": "Prefix suggerit per al nom d'usuari per defecte", "Accounts_denyUnverifiedEmail": "Denegar correu electrònic sense verificar", "Accounts_EmailVerification": "Verificació de correu electrònic", "Accounts_EmailVerification_Description": "Assegura't que la configuració SMTP és correcta per fer servir aquesta funcionalitat", @@ -265,6 +265,7 @@ "BotHelpers_userFields": "Camps d'usuari", "BotHelpers_userFields_Description": "CSV de camps d'usuari que poden ser accedits pels mètodes helper dels bots.", "Branch": "Branca", + "Broadcast_Connected_Instances": "Difusió de les instàncies connectades", "Bugsnag_api_key": "Clau API Bugsnag", "busy": "ocupat", "Busy": "Ocupat", @@ -360,6 +361,7 @@ "CROWD_Reject_Unauthorized": "Rebutja no autoritzat", "CRM_Integration": "Integració CRM", "Current_Chats": "Xats actuals", + "Current_Status": "Estat actual", "Custom": "Personalitzat", "Custom_Emoji": "Emoticona personalitzada", "Custom_Emoji_Add": "Afegir nova emoticona", @@ -668,11 +670,11 @@ "Iframe_Integration_receive_enable": "Activa recepció", "Iframe_Integration_receive_enable_Description": "Permetre que la finestra pare enviï ordres a Rocket.Chat.", "Iframe_Integration_receive_origin": "Rebre orígens", - "Iframe_Integration_receive_origin_Description": "Només les pàgines de les quals es proporciona l'origen podran enviar ordres o `*` per a totes. Es poden utilitzar múltiples valors separats per `,`. Exemple `http://localhost,https://localhost`", + "Iframe_Integration_receive_origin_Description": "Origens amb prefix del protocol, separats per comes, dels quals es permet rebre comandes. Exemple 'http://localhost, https://localhost', o * per permetre rebre de qualsevol lloc.", "Iframe_Integration_send_enable": "Activa enviament", "Iframe_Integration_send_enable_Description": "Envia esdeveniments a la finestra pare", "Iframe_Integration_send_target_origin": "Envia l'origen a l'objectiu", - "Iframe_Integration_send_target_origin_Description": "Només les pàgines amb l'origen proporcionat podran rebre esdeveniments o `*` per a totes. Exemple `http://localhost`", + "Iframe_Integration_send_target_origin_Description": "Origens amb prefix del protocol on les comandes són enviades. Exemple 'https://localhost', o * per permetre enviar a qualsevol lloc.", "Importer_Archived": "Arxivat", "Importer_CSV_Information": "L'importador CSV requereix un format específic, si us plau llegiu la documentació sobre com estructurar l'arxiu .zip:", "Importer_HipChatEnterprise_Information": "L'arxiu pujat ha de ser un tar.gz desencriptat. Si us plau, llegiu la documentació per a més informació:", @@ -704,6 +706,7 @@ "Install_FxOs_follow_instructions": "Si us plau, confirma la instal·lació de l'aplicació al teu dispositiu (polsi \"Instal·lar\" quan se us demani).", "Installation": "Instal·lació", "Installed_at": "Instal·lat a", + "Instance_Record": "Registre d'instància", "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Instruccions als visitants, ompliu el formulari per enviar un missatge", "Impersonate_user": "Suplantar usuari", "Impersonate_user_description": "Quan s'activa, la integració publica com a l'usuari que ha desencadenat la integració", @@ -1124,6 +1127,7 @@ "or": "o", "Open_your_authentication_app_and_enter_the_code": "Obre l'app d'autenticació i entra el codi. També pots utilitzar un dels codis de recuperació.", "Order": "Ordre", + "Or_talk_as_anonymous": "O parla com a anònim", "OS_Arch": "Arquitectura del sistema", "OS_Cpus": "Recompte de CPU", "OS_Freemem": "Memòria RAM lliure", @@ -1228,7 +1232,6 @@ "Register": "Crea un compte nou", "Registration": "Registre", "Registration_Succeeded": "Registre reeixit", - "Register_or_login_to_send_messages": "Registra't o identifica't per enviar missatges", "Registration_via_Admin": "Registre via Admin", "Regular_Expressions": "Expressions regulars", "Release": "Llançament", @@ -1254,6 +1257,7 @@ "Reset_password": "Reinicialitza la contrasenya", "Restart": "Reinicia (restart)", "Restart_the_server": "Reinicia el servidor", + "Retry_Count": "Comptador de reintents", "Role": "Rol", "Role_Editing": "Edició de rols", "Role_removed": "Rol eliminat", @@ -1363,6 +1367,7 @@ "Showing_archived_results": "

Mostrant %s resultats arxivats

", "Showing_online_users": "Mostrant-ne __total_showing__, En línia: __online__, Total: __total__ usuaris", "Showing_results": "

Mostrant %s resultats

", + "Sign_in_to_start_talking": "Identifica't per començar a parlar", "since_creation": "des de %s", "Site_Name": "Nom del lloc", "Site_Url": "URL del lloc", @@ -1557,6 +1562,7 @@ "Unread_Rooms": "Sales no llegides", "Unread_Rooms_Mode": "Mode de sales no llegides", "Unstar_Message": "Esborra el destacat", + "Updated_at": "Actualitzat el", "Upload_file_description": "Descripció de l'arxiu", "Upload_file_name": "Nom de l'arxiu", "Upload_file_question": "Pujar l'arxiu?", diff --git a/packages/rocketchat-i18n/i18n/cs.i18n.json b/packages/rocketchat-i18n/i18n/cs.i18n.json index ba2dab094d9aaf1329a074484b881bde67931f71..2c48a17fa984f3136960a6db1dbb25b497252c4b 100644 --- a/packages/rocketchat-i18n/i18n/cs.i18n.json +++ b/packages/rocketchat-i18n/i18n/cs.i18n.json @@ -17,7 +17,8 @@ "Accessing_permissions": "Přístup k oprávnění", "Account_SID": "SID účtu", "Accounts": "Účty", - "Accounts_AllowAnonymousAccess": "Povolit anonymní přístup", + "Accounts_AllowAnonymousRead": "Povolit anonymům číst", + "Accounts_AllowAnonymousWrite": "Povolit anonymům zapisovat", "Accounts_AllowDeleteOwnAccount": "Povolit uživatelům odstranit vlastní účet", "Accounts_AllowedDomainsList": "Seznam povolených domén", "Accounts_AllowedDomainsList_Description": "Čárkami oddělený seznam povolených domén", @@ -28,13 +29,12 @@ "Accounts_AllowUserProfileChange": "Povolit úpravy profilu", "Accounts_AvatarResize": "Rozměry avataru", "Accounts_AvatarSize": "Velikost avataru", - "Accounts_AvatarStorePath": "Cesta k úložišti avatarů", - "Accounts_AvatarStoreType": "Typ úložiště avatarů", "Accounts_BlockedDomainsList": "Seznam blokovaných domén", "Accounts_BlockedDomainsList_Description": "Čárkami oddělený seznam blokovaných domén", "Accounts_BlockedUsernameList": "Zakázaná uživatelská jména", "Accounts_BlockedUsernameList_Description": "čárkou oddělený seznam uživatelských jmen (na velikosti písmen nezáleží)", "Accounts_CustomFields_Description": "Validní JSON obsahující klíče polí s nastavením. Například:
{\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}", + "Accounts_DefaultUsernamePrefixSuggestion": "Výchozí návrh prefixu uživatelského jména", "Accounts_denyUnverifiedEmail": "Zakázat neověřené e-mailové adresy", "Accounts_EmailVerification": "Ověření e-mailu", "Accounts_EmailVerification_Description": "Pro použití této funkce se ujistěte, že máte správné nastavení SMTP", @@ -265,6 +265,7 @@ "BotHelpers_userFields": "Uživatelská pole", "BotHelpers_userFields_Description": "CSV uživatelských polí, která budou přístupná botům", "Branch": "Větev", + "Broadcast_Connected_Instances": "Připojené instance", "Bugsnag_api_key": "Bugsnag API klíč", "busy": "zaneprázdněný", "Busy": "Zaneprázdněný", @@ -360,6 +361,7 @@ "CROWD_Reject_Unauthorized": "Zamítnout neutorizované", "CRM_Integration": "Integrace CRM", "Current_Chats": "Aktuální Místnosti", + "Current_Status": "Aktuální stav", "Custom": "Vlastní", "Custom_Emoji": "Vlastní emotikona", "Custom_Emoji_Add": "Přidat novou emotikonu", @@ -704,6 +706,7 @@ "Install_FxOs_follow_instructions": "Prosím potvrďte instalaci aplikace na Vašem přístroji (stiskněte tlačítko \"Install\" po výzvě).", "Installation": "Instalace", "Installed_at": "instalováno v", + "Instance_Record": "ID Instance", "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Pokyny pro Vaše návštěvníky k vyplnění formulář pro odeslání zprávy", "Impersonate_user": "Vydávat se za uživatele", "Impersonate_user_description": "Pokud je povoleno, integrace posílá za uživatele, který ji vyvolal", @@ -1124,6 +1127,7 @@ "or": "nebo", "Open_your_authentication_app_and_enter_the_code": "Otevřete autentizační aplikaci a zadejte vygenerovaný kód. Můžete použít jeden ze svých záložních kódů.", "Order": "Objednat", + "Or_talk_as_anonymous": "Vydávat sezaanonyma", "OS_Arch": "Architektura OS", "OS_Cpus": "Počet CPU OS", "OS_Freemem": "Volná paměť OS", @@ -1161,7 +1165,7 @@ "Please_answer_survey": "Věnujte prosím chvilku času ohodnocení chatu.", "Please_enter_value_for_url": "Prosím, zadejte URL Vašeho avataru.", "Please_enter_your_new_password_below": "Níže zadejte své nové heslo:", - "Please_enter_your_password": "Zadejte prosím heslo znovu", + "Please_enter_your_password": "Zadejte prosím své současné heslo", "please_enter_valid_domain": "Prosím zadejte platnou doménu", "Please_fill_a_label": "Prosím, vyplňte štítek", "Please_fill_a_name": "Prosím vyplňte název", @@ -1228,7 +1232,6 @@ "Register": "Zaregistrovat nový účet", "Registration": "Registrace", "Registration_Succeeded": "Registrace úspěšná", - "Register_or_login_to_send_messages": "Pro odeslání zpráv je třeba se zaregistrovat nebo přihlásit", "Registration_via_Admin": "Registrace přes Admin", "Regular_Expressions": "Regulární výrazy", "Release": "Verze", @@ -1254,6 +1257,7 @@ "Reset_password": "Obnovit heslo", "Restart": "Restartovat", "Restart_the_server": "Restartovat server", + "Retry_Count": "Počet opakování", "Role": "Role", "Role_Editing": "Editace Role", "Role_removed": "Role odstraněna", @@ -1363,6 +1367,7 @@ "Showing_archived_results": "

Zobrazeno %s archivovaných výsledků

", "Showing_online_users": "Viditelných __total_showing__ z __total__ uživatelů", "Showing_results": "

Zobrazeno %s výsledků

", + "Sign_in_to_start_talking": "Pro konverzaci se přihlašte", "since_creation": "od %s", "Site_Name": "Jméno stránky", "Site_Url": "URL stránky", @@ -1557,6 +1562,7 @@ "Unread_Rooms": "Nepřečtené místnosti", "Unread_Rooms_Mode": "Mód Nepřečtených místností", "Unstar_Message": "Odebrat hvězdičku", + "Updated_at": "Poslední aktualizace", "Upload_file_description": "Popis souboru", "Upload_file_name": "Název souboru", "Upload_file_question": "Nahrát soubor?", diff --git a/packages/rocketchat-i18n/i18n/de-AT.i18n.json b/packages/rocketchat-i18n/i18n/de-AT.i18n.json index 95bf6f0116731ae1a8db1a1a98390008a1ecc203..692513c0e910bd4f1fa8dd15af90480a79455ce7 100644 --- a/packages/rocketchat-i18n/i18n/de-AT.i18n.json +++ b/packages/rocketchat-i18n/i18n/de-AT.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "Benutzern das Ändern des Profils erlauben", "Accounts_AvatarResize": "Größe des Profilbilds anpassen", "Accounts_AvatarSize": "Größe des Profilbilds", - "Accounts_AvatarStorePath": "Speicherpfad des Profilbilds", - "Accounts_AvatarStoreType": "Speichertyp des Profilbilds", "Accounts_BlockedDomainsList": "Liste geblockter Domains", "Accounts_BlockedDomainsList_Description": "Kommata getrennte Liste von geblockten Domains", "Accounts_BlockedUsernameList": "Liste gesperrter Benutzernamen", @@ -55,6 +53,7 @@ "Accounts_OAuth_Custom_id": "ID", "Accounts_OAuth_Custom_Identity_Path": "Identitätspfad", "Accounts_OAuth_Custom_Login_Style": "Anmeldungsart", + "Accounts_OAuth_Custom_Merge_Users": "BenutzerInnen zusammenführen", "Accounts_OAuth_Custom_Scope": "Bereich", "Accounts_OAuth_Custom_Secret": "Secret", "Accounts_OAuth_Custom_Token_Path": "Pfad des Token", @@ -118,12 +117,12 @@ "Add_agent": "Agent hinzufügen", "Add_custom_oauth": "Benutzerdefiniertes OAuth-Konto hinzufügen", "Add_manager": "Manager hinzufügen", - "Add_user": "Benutzer hinzufügen", - "Add_User": "Benutzer hinzufügen", - "Add_users": "Benutzer hinzufügen", + "Add_user": "BenutzerIn hinzufügen", + "Add_User": "BenutzerIn hinzufügen", + "Add_users": "BenutzerInnen hinzufügen", "Adding_OAuth_Services": "Hinzufügen von OAuth-Services", "Adding_permission": "Berechtigung hinzufügen", - "Adding_user": "Benutzer hinzufügen", + "Adding_user": "Füge BenutzerIn hinzu", "Additional_emails": "Zusätzliche E-Mails", "Additional_Feedback": "Zusätzliches Feedback", "Administration": "Administration", @@ -147,6 +146,7 @@ "And_more": "Und __length__ mehr", "Animals_and_Nature": "Tiere & Natur", "API": "API", + "API_Allow_Infinite_Count": "Erlauben, alles zu bekommen", "API_Analytics": "Analytics", "API_Embed": "Einbetten", "API_EmbedDisabledFor": "Einbettungen für Benutzer deaktivieren", @@ -205,6 +205,7 @@ "Back_to_integrations": "Zurück zu Integrationen", "Back_to_login": "Zurück zum Login", "Back_to_permissions": "Zurück zu den Berechtigungen", + "Block_User": "BenutzerIn sperren", "Body": "Body", "bold": "fett", "Branch": "Branch", @@ -328,7 +329,7 @@ "Edit_Department": "Abteilung bearbeiten", "edited": "bearbeitet", "Editing_room": "Raum bearbeiten", - "Editing_user": "Benutzer bearbeiten", + "Editing_user": "Benutzern bearbeiten", "Email": "E-Mail", "Email_address_to_send_offline_messages": "E-Mail-Adresse zum Senden von Offline-Nachrichten", "Email_already_exists": "Die E-Mail-Adresse existiert bereits.", @@ -354,6 +355,7 @@ "Enter_a_room_name": "Raumnamen eingeben", "Enter_a_username": "Geben Sie einen Benutzernamen ein", "Enter_name_here": "Namen hier eingeben", + "Enter_Normal": "Normaler Modus (mit Eingabetaste senden)", "Enter_to": "Enter-Taste: ", "Error": "Fehler", "error-action-not-allowed": "__action__ ist nicht erlaubt", @@ -461,6 +463,7 @@ "Force_SSL": "SSL erzwingen", "Force_SSL_Description": "*Achtung!* _Force SSL_ solte niemals mit einem Reverse-Proxy verwendet werden. Falls Sie einen Reverse-Proxy verwenden, sollten Sie die Weiterleitung DORT einrichten. Dies Option existiert für Anwendungen wie Heroku, die keine Weiterleitungskonfigurationen für Reverse-Proxy erlauben.", "Forgot_password": "Passwort vergessen?", + "Forward_to_user": "An BenutzerIn weiterleiten", "Frequently_Used": "Häufig verwendet", "From": "Absender", "From_Email": "Absender", @@ -523,6 +526,9 @@ "Integration_added": "Die Integration wurde hinzugefügt.", "Integration_Incoming_WebHook": "Eingehende WebHook-Integration", "Integration_New": "Neue Integration", + "Integrations_Outgoing_Type_RoomJoined": "BenutzerIn hat den Raum betreten", + "Integrations_Outgoing_Type_RoomLeft": "BenutzerIn hat den Raum verlassen", + "Integrations_Outgoing_Type_UserCreated": "BenutzerIn angelegt.", "Integration_Outgoing_WebHook": "Ausgehende WebHook-Integration", "Integration_Word_Trigger_Placement_Description": "Soll das auslösende Wort irgendwo im Satz stehen und nicht nur am Anfang? ", "Integration_updated": "Die Integration wurde aktualisiert.\n", @@ -548,7 +554,7 @@ "Invitation_Subject": "Einladungsbetreff", "Invitation_Subject_Default": "Sie wurden zu [Site_Name] eingeladen", "Invite_user_to_join_channel": "Benutzer in diesen Raum einladen", - "Invite_Users": "Benutzer einladen", + "Invite_Users": "BenutzerInnen einladen", "is_also_typing": "schreibt auch", "is_also_typing_female": "schreibt auch", "is_also_typing_male": "schreibt auch", @@ -790,6 +796,7 @@ "Notification_Duration": "Dauer der Benachrichtigung", "Notifications": "Benachrichtigungen", "Notify_all_in_this_room": "Alle Benutzer in diesem Raum benachrichtigen", + "Notify_active_in_this_room": "Aktive Benutzer/innen benachrichtigen", "Num_Agents": "# Agents", "Number_of_messages": "Anzahl der Nachrichten", "OAuth_Application": "OAuth-Anwendung", diff --git a/packages/rocketchat-i18n/i18n/de.i18n.json b/packages/rocketchat-i18n/i18n/de.i18n.json index 24e7b29e401dcc777e1c19727e05da759fc595ea..545e16b1e447dea64256fa3c8e82a40ff33ce5f1 100644 --- a/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/packages/rocketchat-i18n/i18n/de.i18n.json @@ -17,6 +17,8 @@ "Accessing_permissions": "Zugriff auf Berechtigungen", "Account_SID": "Konto-SID", "Accounts": "Konten", + "Accounts_AllowAnonymousRead": "Erlaube Anonymes lesen", + "Accounts_AllowAnonymousWrite": "Erlaube Anonymes schreiben", "Accounts_AllowDeleteOwnAccount": "Benutzern erlauben, ihr Konto zu löschen", "Accounts_AllowedDomainsList": "Liste von erlaubten Domains", "Accounts_AllowedDomainsList_Description": "Durch Kommata getrennte Liste von erlaubten Domains", @@ -27,8 +29,6 @@ "Accounts_AllowUserProfileChange": "Benutzern das Ändern des Profils erlauben", "Accounts_AvatarResize": "Größe des Profilbilds anpassen", "Accounts_AvatarSize": "Größe des Profilbilds", - "Accounts_AvatarStorePath": "Speicherpfad des Profilbilds", - "Accounts_AvatarStoreType": "Speichertyp des Profilbilds", "Accounts_BlockedDomainsList": "Liste geblockter Domains", "Accounts_BlockedDomainsList_Description": "Kommata getrennte Liste von geblockten Domains", "Accounts_BlockedUsernameList": "Liste gesperrter Benutzernamen", @@ -103,6 +103,8 @@ "Accounts_OAuth_Wordpress_id": "WordPress-ID", "Accounts_OAuth_Wordpress_secret": "Geheimer WordPress Schlüssel", "Accounts_PasswordReset": "Passwort zurücksetzen", + "Accounts_OAuth_Proxy_host": "Proxy Host", + "Accounts_OAuth_Proxy_services": "Proxy Service", "Accounts_Registration_AuthenticationServices_Default_Roles": "Standardrolle für Authentifizierungsdienste", "Accounts_Registration_AuthenticationServices_Default_Roles_Description": "Standardrollen die Benutzern zugewiesen werden, die sich über Authentifizierungsdienste registrieren", "Accounts_Registration_AuthenticationServices_Enabled": "Anmeldung mit Authentifizierungsdiensten", @@ -130,12 +132,12 @@ "Add_custom_oauth": "Benutzerdefiniertes OAuth-Konto hinzufügen", "Add_Domain": "Domain hinzufügen", "Add_manager": "Manager hinzufügen", - "Add_user": "Benutzer hinzufügen", - "Add_User": "Benutzer hinzufügen", - "Add_users": "Benutzer hinzufügen", + "Add_user": "BenutzerIn hinzufügen", + "Add_User": "BenutzerIn hinzufügen", + "Add_users": "BenutzerInnen hinzufügen", "Adding_OAuth_Services": "Hinzufügen von OAuth-Services", "Adding_permission": "Berechtigung hinzufügen", - "Adding_user": "Benutzer hinzufügen", + "Adding_user": "Füge BenutzerIn hinzu", "Additional_emails": "Zusätzliche E-Mails", "Additional_Feedback": "Zusätzliches Feedback", "Administration": "Administration", @@ -166,6 +168,7 @@ "Animals_and_Nature": "Tiere & Natur", "Announcement": "Ankündigung", "API": "API", + "API_Allow_Infinite_Count": "Erlauben, alles zu bekommen", "API_Allow_Infinite_Count_Description": "Erlaube Rückgabe von REST API Ergebnissen in einem einzigen Abruf", "API_Analytics": "Analytics", "API_CORS_Origin": "CORS Origin", @@ -175,6 +178,7 @@ "API_Drupal_URL_Description": "Beispiel: https://domain.de (ohne schließenden /)", "API_Embed": "Einbetten", "API_Embed_Description": "Eingebettete Link Vorschau für Links die von Benutzern gepostet wurden aktiv.", + "API_EmbedCacheExpirationDays": "Tage bis zum Ablauf den eingebetteten Caches", "API_EmbedDisabledFor": "Einbettungen für Benutzer deaktivieren", "API_EmbedDisabledFor_Description": "Durch Kommata getrennte Liste von Benutzernamen", "API_EmbedIgnoredHosts": "Ignorierte Hosts einbetten", @@ -182,9 +186,11 @@ "API_EmbedSafePorts": "Sichere Ports", "API_EmbedSafePorts_Description": "Kommagetrennte Liste der Ports, für die eine Vorschau erlaubt ist.", "API_Enable_CORS": "Aktiviere CORS", + "API_Enable_Direct_Message_History_EndPoint": "Aktiviere den Endpunkt für den Verlauf von Direkt Nachrichten", "API_GitHub_Enterprise_URL": "Server-URL", "API_GitHub_Enterprise_URL_Description": "Beispiel: http://domain.com (ohne Schrägstrich am Ende)", "API_Gitlab_URL": "GitLab-URL", + "API_Shield_Types": "Shield Typ", "API_Token": "API-Token", "API_Upper_Count_Limit": "Max. Anzahl an Einträgen", "API_Upper_Count_Limit_Description": "Max. Anzahl an Einträgen die die REST API zurückliefen soll (sofern ohne Einschränkung)? ", @@ -205,6 +211,7 @@ "Assign_admin": "Admin zuweisen", "at": "am", "Attachment_File_Uploaded": "Datei hochgeladen", + "Attribute_handling": "Behandlung von Attributen", "Auth_Token": "Auth-Token", "Author": "Autor", "Authorization_URL": "Autorisierungs-URL", @@ -237,8 +244,10 @@ "Back": "Zurück", "Back_to_applications": "Zurück zu den Anwendungen", "Back_to_integrations": "Zurück zu Integrationen", + "Back_to_integration_detail": "Zurück zu den Integrations Details", "Back_to_login": "Zurück zum Login", "Back_to_permissions": "Zurück zu den Berechtigungen", + "Block_User": "BenutzerIn sperren", "Body": "Body", "bold": "fett", "bot_request": "Bot-Anfrage", @@ -257,10 +266,13 @@ "Cancel": "Abbrechen", "Cancel_message_input": "Abbrechen", "Cannot_invite_users_to_direct_rooms": "Benutzer können nicht in private Nachrichtenräume eingeladen werden.", + "CAS_autoclose": "Login Pupp automatisch schließen", + "CAS_base_url_Description": "Haupt URL des externen Singe Sign On Services e.g: https://sso.example.undef/sso/", "CAS_button_color": "Hintergrundfarbe des Login-Buttons", "CAS_button_label_color": "Farbe des Login-Button-Texts", "CAS_button_label_text": "Text des Login-Buttons", "CAS_enabled": "Aktiviert", + "CAS_login_url_Description": "Login URL des externen Singe Sign On Services e.g: https://sso.example.undef/sso/login", "CAS_popup_height": "Höhe des Login Pop Up", "CAS_popup_width": "Breite des Login Pop Up", "CAS_Sync_User_Data_Enabled": "Benutzerdaten immer synchronisieren", @@ -291,8 +303,10 @@ "Choose_the_username_that_this_integration_will_post_as": "Wählen Sie den Benutzernamen, der die Integration veröffentlicht.", "clear": "löschen", "clear_cache_now": "Zwischenspeicher jetzt leeren", + "clear_history": "Verlauf löschen", "Clear_all_unreads_question": "Möchten Sie alle ungelesenen Nachrichten löschen?", "Click_here": "Hier klicken", + "Click_here_for_more_info": "Hier klicken für weitere Informationen", "Client_ID": "Client-ID", "Client_Secret": "Client-Schlüssel", "Clients_will_refresh_in_a_few_seconds": "Clients werden in wenigen Sekunden aktualisiert", @@ -336,15 +350,23 @@ "Custom_Fields": "Benutzerdefinierte Felder", "Custom_oauth_helper": "Bei der Einrichtung muss eine Rückruf-URL angegeben werden. Benutze dafür folgende URL:
%s
", "Custom_oauth_unique_name": "Name des OAuth-Kontos", + "Custom_Scripts": "Benutzerdefinierte Skripte", "Custom_Script_Logged_In": "Benutzerdefiniertes Script für angemeldete Benutzer", "Custom_Script_Logged_Out": "Benutzerdefiniertes Script für abgemeldete Benutzer", + "Custom_Sounds": "Benutzerdefinierte Töne", + "Custom_Sound_Add": "Benutzerdefinierte Töne hinzufügen", + "Custom_Sound_Delete_Warning": "Ein gelöschter Ton kann nicht wiederhergestellt werden.", + "Custom_Sound_Error_Invalid_Sound": "Fehlerhafter Ton", "Custom_Translations": "Benutzerdefinierte Übersetzungen", "Dashboard": "Dashboard", "Date": "Datum", + "Date_From": "von", + "Date_to": "bis", "days": "Tage", "DB_Migration": "Datenbankmigration", "DB_Migration_Date": "Datenbankmigrationsdatum", "Deactivate": "Deaktivieren", + "Decline": "ablehnen", "Default": "Voreinstellung", "Delete": "Löschen", "Delete_message": "Nachricht löschen", @@ -366,6 +388,8 @@ "Desktop_Notifications_Enabled": "Desktop-Benachrichtigungen sind aktiviert.", "Direct_message_someone": "Jemandem eine private Nachricht schicken", "Direct_Messages": "Private Nachrichten", + "Disable_Notifications": "Benachrichtigungen deaktivieren", + "Disable_two-factor_authentication": "2 Faktor Authentifizierung deaktivieren", "Display_offline_form": "Offline Nachricht anzeigen", "Displays_action_text": "Zeigt Aktionstext", "Do_you_want_to_change_to_s_question": "Möchten Sie dies zu %s ändern?", @@ -384,7 +408,7 @@ "Edit_Department": "Abteilung bearbeiten", "edited": "bearbeitet", "Editing_room": "Raum bearbeiten", - "Editing_user": "Benutzer bearbeiten", + "Editing_user": "BenutzerIn bearbeiten", "Email": "E-Mail", "Email_address_to_send_offline_messages": "E-Mail-Adresse zum Senden von Offline-Nachrichten", "Email_already_exists": "Die E-Mail-Adresse existiert bereits.", @@ -403,7 +427,9 @@ "Empty_title": "Es wurde kein Titel angegeben.", "Enable": "Aktivieren", "Enable_Desktop_Notifications": "Aktivieren", + "Enable_two-factor_authentication": "2-Faktor Authentifizierung aktivieren", "Enabled": "Aktiviert", + "Enable_Svg_Favicon": "SVG Favicon aktivieren", "Encrypted_message": "Verschlüsselte Nachricht", "End_OTR": "OTR beenden", "Enter_a_regex": "Regex eingeben", @@ -411,6 +437,7 @@ "Enter_a_username": "Benutzernamen eingeben", "Enter_Behaviour": "Verhalten der Enter-Taste:", "Enter_name_here": "Namen hier eingeben", + "Enter_Normal": "Normaler Modus (mit Eingabetaste senden)", "Enter_to": "Enter-Taste: ", "Error": "Fehler", "error-action-not-allowed": "__action__ ist nicht erlaubt", @@ -437,9 +464,11 @@ "error-invalid-channel-start-with-chars": "Ungültiger Kanal. Beginnen Sie mit @ oder #", "error-invalid-custom-field": "Ungültiges benutzerdefiniertes Feld", "error-invalid-custom-field-name": "Unzulässiger Name für ein benutzerdefiniertes Feld. Benutze nur Buchstaben, Nummern, Binde- und Unterstriche.", + "error-invalid-date": "Das eingegebene Datum ist fehlerhaft.", "error-invalid-description": "Ungültige Beschreibung", "error-invalid-domain": "Ungültige Domain", "error-invalid-email": "Ungültige E-Mail-Adresse: __email__", + "error-invalid-email-address": "Fehlerhafte E-Mail-Adresse", "error-invalid-file-height": "Ungültige Dateihöhe", "error-invalid-file-type": "Ungültiges Dateiformat", "error-direct-message-file-upload-not-allowed": "Dateiaustausch ist in direkten Nachrichten nicht möglich.", @@ -499,8 +528,10 @@ "File_exceeds_allowed_size_of_bytes": "Die Datei ist größer als das erlaubte Maximum von __size__ Bytes", "File_not_allowed_direct_messages": "Dateiaustausch ist in direkten Nachrichten nicht möglich.", "File_type_is_not_accepted": "Feldtyp nicht akzeptiert.", + "File_uploaded": "Datei hochladen", "FileUpload": "Dateien hochladen", "FileUpload_Enabled": "Hochladen von Dateien aktivieren", + "FileUpload_Disabled": "Datei Uploads ", "FileUpload_Enabled_Direct": "Dateiaustausch ist in direkten Nachrichten möglich.", "FileUpload_File_Empty": "Datei ist leer", "FileUpload_FileSystemPath": "Systempfad", @@ -524,17 +555,22 @@ "Follow_social_profiles": "Folge uns in sozialen Netzwerken, fork uns auf GitHub und teile deine Gedanken über die Rocket.Chat-App auf unserem Trello-Board.", "Food_and_Drink": "Essen & Trinken", "Footer": "Fußzeile", + "Fonts": "Schriften", "For_your_security_you_must_enter_your_current_password_to_continue": "Geben Sie zu Ihrer Sicherheit Ihr aktuelles Passwort ein um fortzufahren.", "Force_SSL": "SSL erzwingen", "Force_SSL_Description": "*Achtung!* _Force SSL_ solte niemals mit einem Reverse-Proxy verwendet werden. Falls Sie einen Reverse-Proxy verwenden, sollten Sie die Weiterleitung DORT einrichten. Dies Option existiert für Anwendungen wie Heroku, die keine Weiterleitungskonfigurationen für Reverse-Proxy erlauben.", "Forgot_password": "Passwort vergessen?", + "Forgot_Password_Email_Subject": "[Site_Name] - Passwort Wiederherstellung", + "Forgot_Password_Email": "Hier Klicken um das Passwort zurückzusetzen.", + "Forgot_password_section": "Passwort vergessen", "Forward": "Weiterleiten", "Forward_chat": "Chat weiterleiten", "Forward_to_department": "An Abteilung weiterleiten", - "Forward_to_user": "An Benutzer weiterleiten", + "Forward_to_user": "An BenutzerIn weiterleiten", "Frequently_Used": "Häufig verwendet", + "Friday": "Freitag", "From": "Absender", - "From_Email": "Absender", + "From_Email": "E-Mail Absender", "From_email_warning": "Warnung: Der Absender ist Gegenstand deiner Mail-Server-Einstellungen.", "General": "Allgemeines", "github_no_public_email": "Sie haben keine öffentliche E-Mail-Adresse in Ihrem GitHub-Account.", @@ -545,6 +581,7 @@ "Guest_Pool": "Gästepool", "Hash": "Hash", "Header": "Kopfzeile", + "Header_and_Footer": "Kopf und Fusszeile", "Hidden": "Versteckt", "Hide_Avatars": "Avatar verstecken", "Hide_flextab": "Rechte Seitenleiste über Klick verstecken", @@ -552,6 +589,7 @@ "Hide_Private_Warning": "Sind sie sicher, das Gespräch mit \"%s\" zu verstecken?", "Hide_room": "Raum verstecken", "Hide_Room_Warning": "Sind sie sicher, den Raum \"%s\" zu verstecken?", + "Hide_roles": "Rollen verstecken", "Hide_usernames": "Benutzernamen ausblenden", "Highlights": "Hervorhebungen", "Highlights_How_To": "Um benachrichtigt zu werden, wenn ein Wort oder Ausdruck erwähnt wird, fügen Sie ihn hier hinzu. Sie können Wörter und Ausdrücke mit Kommata trennen. Die Wörter zur Hervorhebung beachten die Groß- und Kleinschreibung nicht.", @@ -598,9 +636,17 @@ "Installation": "Installation", "Installed_at": "Installationsdatum", "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Anweisungen an Ihre Besucher: Füllen Sie das Formular aus, um eine Nachricht zu senden.", + "Integration_Advanced_Settings": "Erweiterte Einstellungen", "Integration_added": "Die Integration wurde hinzugefügt.", "Integration_Incoming_WebHook": "Eingehende WebHook-Integration", "Integration_New": "Neue Integration", + "Integrations_Outgoing_Type_FileUploaded": "Hochgeladene Dateien", + "Integrations_Outgoing_Type_RoomArchived": "Archivierte Räume", + "Integrations_Outgoing_Type_RoomCreated": "Raum erstellt (öffentlich und privat)", + "Integrations_Outgoing_Type_RoomJoined": "BenutzerIn hat den Raum betreten", + "Integrations_Outgoing_Type_RoomLeft": "BenutzerIn hat den Raum verlassen", + "Integrations_Outgoing_Type_SendMessage": "Nachricht gesendet", + "Integrations_Outgoing_Type_UserCreated": "BenutzerIn angelegt.", "Integration_Outgoing_WebHook": "Ausgehende WebHook-Integration", "Integration_Word_Trigger_Placement_Description": "Soll das auslösende Wort irgendwo im Satz stehen und nicht nur am Anfang? ", "Integration_updated": "Die Integration wurde aktualisiert.\n", @@ -618,15 +664,17 @@ "Invalid_pass": "Es muss ein Passwort angegeben werden.", "Invalid_room_name": "%s ist kein zulässiger Raumname.
Verwenden Sie nur Buchstaben, Zahlen oder Binde- und Unterstriche.", "Invalid_secret_URL_message": "Die angegebene URL ist ungültig.", + "Invalid_two_factor_code": "Fehlerhafter 2-Faktor Code", "invisible": "unsichtbar", "Invisible": "Unsichtbar", + "Invitation": "Einladung", "Invitation_HTML": "Einladungstext (HTML)", "Invitation_HTML_Default": "

Sie wurden eingeladen zu

[Site_Name]

Besuchen Sie zu [Site_URL] und probieren Sie heute die beste verfügbare Open-Source-Chat-Lösung aus!

", "Invitation_HTML_Description": "Sie können die folgenden Platzhalter verwenden:
", "Invitation_Subject": "Einladungsbetreff", "Invitation_Subject_Default": "Sie wurden zu [Site_Name] eingeladen", - "Invite_user_to_join_channel": "Benutzer in diesen Kanal einladen", - "Invite_Users": "Benutzer einladen", + "Invite_user_to_join_channel": "BenutzerIn in diesen Kanal einladen", + "Invite_Users": "BenutzerInnen einladen", "is_also_typing": "schreibt auch", "is_also_typing_female": "schreibt auch", "is_also_typing_male": "schreibt auch", @@ -639,6 +687,7 @@ "Jitsi_Enable_Channels": "Aktivieren in Kanälen", "join": "Beitreten", "Join_audio_call": "Anruf beitreiten", + "Join_Chat": "Chat beitreten", "Join_default_channels": "Standardkanälen beitreten", "Join_the_Community": "Trete der Community bei", "Join_the_given_channel": "Diesem Kanal beitreten", @@ -704,7 +753,7 @@ "LDAP_Sync_User_Data": "Daten synchronisieren", "LDAP_Sync_User_Data_Description": "Bei der Anmeldung die Benutzerdaten mit dem Server synchronisieren (Beispiel: Name, E-Mail).", "LDAP_Sync_User_Data_FieldMap": "Nutzerdaten-Feldkarte", - "LDAP_Sync_User_Data_FieldMap_Description": "Konfigurieren Sie, wie Benutzer-Account-Felder (wie die E-Mail-Adresse) aus einem LDAP-Datensatz (falls gefunden) geladen werden.
Beispiel: {\"cn\":\"name\", \"mail\":\"email\"} nimmt einen von Menschen lesbaren Namen von dem cn-Attribut und die E-Mail-Adresse vom Mail-Attribut.
Verfügbare Felder beinhalten den Namen und die E-Mail-Adresse.", + "LDAP_Sync_User_Data_FieldMap_Description": "Konfigurieren Sie, wie Benutzer-Account-Felder (wie die E-Mail-Adresse) aus einem LDAP-Datensatz (falls gefunden) geladen werden.
Beispiel: {\"cn\":\"name\", \"mail\":\"email\"} nimmt einen von Menschen lesbaren Namen von dem cn-Attribut und die E-Mail-Adresse vom Mail-Attribut. Zusätzlich ist die Verwendung von Variablen möglich, wie z.B.: `{ \"#{givenName} #{sn}\": \"name\", \"mail\": \"email\" }`. Hierbei wird eine Kombination des Vor- und Nachnamens verwendet.
Verfügbare Felder in Rocket.Chat sind `name` und `email`.", "LDAP_Sync_Users": "Benutzer synchronisieren", "LDAP_Test_Connection": "Testverbindung", "LDAP_Unique_Identifier_Field": "Eindeutige Kennung des Felds", @@ -718,6 +767,7 @@ "Leave_Private_Warning": "Sind sie sicher, das Gespräch mit \"%s\" zu verlassen?", "Leave_room": "Raum verlassen", "Leave_Room_Warning": "Sind sie sicher, den Raum \"%s\" zu verlassen?", + "Leave_the_current_channel": "Aktuellen Kanal verlassen", "line": "Zeilen", "List_of_Channels": "Liste der Kanäle", "List_of_Direct_Messages": "Liste der Direktnachrichten", @@ -810,7 +860,9 @@ "Message_ShowFormattingTips": "Formatierungstipps anzeigen", "Message_starring": "Markieren von Nachrichten", "Message_TimeFormat": "Zeitformat", + "Message_TimeAndDateFormat": "Zeit und Datumsformat", "Message_TimeFormat_Description": "Siehe auch: Moment.js", + "Message_TimeAndDateFormat_Description": "Siehe auch: Moment.js", "Message_too_long": "Diese Nachricht ist zu lang.", "Message_VideoRecorderEnabled": "Videoaufnahmen eingeschaltet", "Message_VideoRecorderEnabledDescription": "Videoformat auf webm beim \"Datei hochladen\" einschränken? (Video abspielen funktioniert dann in fast allen Browsern)", @@ -818,11 +870,14 @@ "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Nachrichten, die an den eingehenden Webhook gesendet werden, werden hier veröffentlicht.", "Meta": "Metadaten", "Meta_fb_app_id": "Facebook-App-ID", + "Meta_custom": "Benutzerdefinierte Meta Tags", "Meta_google-site-verification": "Google-Seiten-Verifizierung", "Meta_language": "Sprache", "Meta_msvalidate01": "MSValidate.01", "Meta_robots": "Roboter", + "Min_length_is": "Die minimale länge beträgt %s", "minutes": "Minuten", + "Mobile": "Mobil", "Monday": "Montag", "Monitor_history_for_changes_on": "Verlaufsänderungen beobachten für", "More_channels": "Mehr Kanäle", @@ -876,6 +931,7 @@ "Notification_Duration": "Benachrichtigungsdauer", "Notifications": "Benachrichtigungen", "Notify_all_in_this_room": "Alle Benutzer in diesem Raum benachrichtigen", + "Notify_active_in_this_room": "Aktive Benutzer/innen benachrichtigen", "Num_Agents": "# Agents", "Number_of_messages": "Nachrichtenanzahl", "OAuth_Application": "OAuth-Anwendung", @@ -886,16 +942,19 @@ "Off_the_record_conversation_is_not_available_for_your_browser_or_device": "Off-the-record-Gespräche sind für Ihren Browser oder Ihr Gerät nicht verfügbar.", "Office_Hours": "Bürozeiten", "Office_hours_enabled": "Bürozeiten aktiviert", + "Office_hours_updated": "Bürozeiten aktualisiert", "Offline": "Offline", "Offline_DM_Email": "Sie haben eine private Nachricht von __user__ erhalten.", "Offline_form": "Offline-Formular", "Offline_form_unavailable_message": "Nachricht, dass Offline Formular ungültig", + "Offline_Link_Message": "gehe zur Nachricht", "Offline_Mention_Email": "Sie wurden von __user__ in #__room__ erwähnt.", "Offline_message": "Offline-Nachricht", "Offline_success_message": "Nachricht, dass Offline Nachricht erfolgreich", "Offline_unavailable": "offline - nicht verfügbar", "On": "Ein", "Online": "Online", + "Only_On_Desktop": "Desktop Modus (senden mit der \"Enter\" Taste nur auf dem Desktop PC)", "Only_you_can_see_this_message": "Nur Sie können diese Nachricht sehen.", "Oops!": "Hoppla", "Open": "Öffnen", @@ -941,10 +1000,12 @@ "Please_enter_value_for_url": "Bitte geben Sie eine URL für Ihr Profilbild ein.", "Please_enter_your_new_password_below": "Bitte geben Sie Ihr neues Passwort ein:", "Please_enter_your_password": "Bitte Passwort eingeben", + "please_enter_valid_domain": "Bitte eine valide Domain eingeben", "Please_fill_a_label": "Bitte Bezeichnung ausfüllen", "Please_fill_a_name": "Bitte geben Sie einen Namen ein.", "Please_fill_a_username": "Bitte geben Sie einen Benutzernamen ein.", "Please_fill_name_and_email": "Bitte geben Sie einen Namen und eine E-Mail-Adresse ein.", + "Please_select_an_user": "Bitte einen Benutzer auswählen", "Please_select_enabled_yes_or_no": "Bitte wählen Sie eine Option für \"aktiviert\".", "Please_wait": "Bitte warten", "Please_wait_activation": "Bitte warten, der Vorgang kann einige Zeit in Anspruch nehmen.", @@ -988,14 +1049,23 @@ "quote": "Zitat", "Quote": "Zitieren", "Random": "Zufällig", + "React_when_read_only": "Reaktionen erlauben", "Reacted_with": "Reagierte mit", "Reactions": "Reaktionen", + "Read_only": "Nur lesend", + "Read_only_channel": "Kanal nur lesbar", + "Read_only_group": "Nur lesbar Gruppe", "Record": "Aufnehmen", "Redirect_URI": "Weiterleitungs-URL", + "Refresh_oauth_services": "oAuth Service aktualisieren", "Refresh_keys": "Schlüssel aktualisieren", "Refresh_your_page_after_install_to_enable_screen_sharing": "Aktualisieren Sie die Seite nach der Installation, um die Bildschirmübertragung zu aktivieren.", + "Regenerate_codes": "Codes neu generieren", "Register": "Neues Konto registrieren", + "Registration": "Registrierung", "Registration_Succeeded": "Ihre Registrierung war erfolgreich.", + "Registration_via_Admin": "Registrierung via Admin", + "Regular_Expressions": "Reguläre Ausdrücke", "Release": "Veröffentlichung", "Remove": "Entfernen", "Remove_Admin": "Admin entfernen", @@ -1003,8 +1073,10 @@ "Remove_as_owner": "als Besitzer entfernen", "Remove_custom_oauth": "OAuth-Konto entfernen", "Remove_from_room": "Aus dem Raum entfernen", + "Remove_last_admin": "Entferne den letzen Admin", "Remove_someone_from_room": "Jemanden aus dem Raum entfernen", "Removed": "Entfernt", + "Reply": "Antwort", "Report_Abuse": "Missbrauch melden", "Report_exclamation_mark": "Melden!", "Report_sent": "Bericht gesendet", @@ -1028,10 +1100,16 @@ "room_changed_topic": "Das Thema des Raums wurde von __user_by__ zu __room_topic__ geändert.", "Room_description_changed_successfully": "Raumbeschreibung erfolgreich geändert", "Room_has_been_deleted": "Der Raum wurde gelöscht.", + "Room_has_been_archived": "Der Raum wurde archiviert.", + "Room_has_been_unarchived": "Der Raum wurde dearchiviert.", "Room_Info": "Raum", + "room_is_blocked": "Der räum ist geblockt", + "room_is_read_only": "Der Raum ist nur lesbar", + "room_name": "Raum Name", "Room_name_changed": "__user_by__ hat den Raumnamen zu  __room_name__ geändert.", "Room_name_changed_successfully": "Der Raumname wurde erfolgreich geändert.", "Room_not_found": "Der Raum konnte nicht gefunden werden.", + "Room_password_changed_successfully": "Das Raum Passwort wurde erfolgreich geändert", "Room_topic_changed_successfully": "Das Thema des Raums wurde erfolgreich geändert.", "Room_type_changed_successfully": "Der Raumtyp wurde erfolgreich geändert.", "Room_unarchived": "Der Raum wurde wiederhergestellt.", @@ -1046,6 +1124,8 @@ "SAML_Custom_Generate_Username": "Benutzernamen generieren", "SAML_Custom_Issuer": "Benutzerdefinierter Aussteller", "SAML_Custom_Provider": "Benutzerdefinierter Provider", + "SAML_Custom_Public_Cert": "Öffentliches Zertifikat", + "SAML_Custom_Private_Key": "Privater Schlüssel", "Saturday": "Samstag", "Save": "Speichern", "Save_changes": "Änderungen speichern", @@ -1099,6 +1179,7 @@ "Show_all": "Alle Nutzer zeigen", "Show_more": "Mehr Nutzer zeigen", "show_offline_users": "Zeige Benutzer an, die offline sind", + "Show_on_registration_page": "Auf der Registrierungsseite anzeigen", "Show_only_online": "Nur Online-Nutzer zeigen", "Show_preregistration_form": "Vorregistrierungsformular zeigen", "Showing_archived_results": "%s archivierte Räume", @@ -1117,6 +1198,7 @@ "Slash_Topic_Description": "Thema setzen", "Slash_Topic_Params": "Themennachricht", "Smarsh_Enabled": "Smarsh aktiviert", + "Smarsh_MissingEmail_Email": "Fehlende E-Mail", "Smileys_and_People": "Gesichter & Personen", "SMS_Enabled": "SMS aktiviert", "SMTP": "SMTP", @@ -1125,6 +1207,7 @@ "SMTP_Port": "SMTP-Port", "SMTP_Test_Button": "SMTP-Einstellungen testen", "SMTP_Username": "SMTP-Benutzername", + "Snippet_Added": "Erstellt am %s", "Sound": "Ton", "SSL": "SSL", "Star_Message": "Nachricht markieren", @@ -1165,7 +1248,9 @@ "Symbols": "Symbole", "Sync_success": "Die Synchronisierung war erfolgreich.", "Sync_Users": "Benutzer synchronisieren", + "System_messages": "System Nachrichten", "Tag": "Tag", + "TargetRoom": "Ziel Raum!", "Test_Connection": "Testverbindung", "Test_Desktop_Notifications": "Desktop-Benachrichtigungen testen", "Thank_you_exclamation_mark": "Vielen Dank!", @@ -1200,11 +1285,16 @@ "There_are_no_agents_added_to_this_department_yet": "Es wurden bisher keine Agenten zu dieser Abteilung hinzugefügt.", "There_are_no_integrations": "Es sind keine Integrationen vorhanden.", "There_are_no_users_in_this_role": "Es sind dieser Rolle keine Benutzer zugeordnet.", + "This_conversation_is_already_closed": "Die Unterhaltung wurde bereits beendet.", "This_email_has_already_been_used_and_has_not_been_verified__Please_change_your_password": "Diese E-Mail wurde bereits verschickt, aber noch nicht bestätigt. Bitte ändern Sie Ihr Passwort.", "This_is_a_desktop_notification": "Das ist eine Desktop-Benachrichtigung.", "This_is_a_push_test_messsage": "Dies ist eine Test-Push-Nachricht.", "This_room_has_been_archived_by__username_": "Dieser Raum wurde von __username__ archiviert", "This_room_has_been_unarchived_by__username_": "Dieser Raum wurde von __username__ unarchiviert", + "Two-factor_authentication": "2-Faktor Authentifizierung", + "Two-factor_authentication_disabled": "2-Faktor Authentifizierung deaktiviert", + "Two-factor_authentication_enabled": "2-Faktor Authentifizierung aktiviert", + "Two-factor_authentication_is_currently_disabled": "2-Faktor Authentifizierung ist momentan deaktiviert", "Thursday": "Donnerstag", "Time_in_seconds": "Zeit in Sekunden", "Title": "Titel", @@ -1216,6 +1306,8 @@ "To_users": "An die Benutzer", "Topic": "Thema", "Travel_and_Places": "Reisen & Orte", + "Translated": "übersetzt", + "Translations": "Übersetzungen", "Trigger_removed": "Auslöser entfernt", "Trigger_Words": "Trigger Words", "Triggers": "Auslöser", @@ -1228,7 +1320,9 @@ "Type_your_new_password": "Geben Sie Ihr neues Passwort ein", "UI_DisplayRoles": "Rollen anzeigen", "UI_Merge_Channels_Groups": "Führe private Gruppen und Kanäle zusammen", + "UI_Use_Real_Name": "Benutze den Realen Namen", "Unarchive": "Wiederherstellen", + "Unblock_User": "Benutzer entsperren", "Unmute_someone_in_room": "Jemanden das Chatten in einem Raum wieder erlauben", "Unmute_user": "Benutzern das Chatten erlauben ", "Unnamed": "Unbenannt", @@ -1273,6 +1367,7 @@ "User_left_male": "Der Benutzer __user_left__ hat den Kanal verlassen.", "User_logged_out": "Der Benutzer wurde abgemeldet.", "User_management": "Benutzerverwaltung", + "User_muted": "Benutzer stummgeschaltet", "User_muted_by": "Dem Benutzer __user_muted__ wurde das Chatten von __user_by__ verboten.", "User_not_found": "Der Benutzer konnte nicht gefunden werden.", "User_not_found_or_incorrect_password": "Entweder konnte der Benutzer nicht gefunden werden oder Sie haben ein falsches Passwort angegeben.", @@ -1297,15 +1392,23 @@ "Username_title": "Benutzernamen festlegen", "Username_wants_to_start_otr_Do_you_want_to_accept": "__username__ möchte ein OTR-Gespräch starten. Möchten Sie annehmen?", "Users": "Benutzer", + "Users_added": "Die Benutzer wurden hinzugefügt", "Users_in_role": "Zugeordnete Nutzer", "UTF8_Names_Slugify": "UTF8-Namen-Slugify", "UTF8_Names_Validation": "UTF8-Namen-Verifizierung", "UTF8_Names_Validation_Description": "Erlauben Sie keine Sonderzeichen und Leerzeichen. Sie können - _ und . verwenden, aber nicht am Ende eines Namens.", + "Validate_email_address": "E-Mail-Adresse bestätigen", + "Verification": "Überprüfung ", "Verification_email_sent": "Bestätigungsmail gesendet", + "Verification_Email_Subject": "[Site_Name] - Bestätige dein Benutzerkonto", + "Verification_Email": "Klicke hier um dein Benutzerkonto zu bestätigen.", "Verified": "Verifiziert", + "Verify": "überprüfen", "Version": "Version", "Video_Chat_Window": "Video-Chat", + "Video_Conference": "Video-Konferenz", "Videocall_declined": "Videoanruf abgelehnt.", + "Videocall_enabled": "Videoanruf aktiviert", "View_All": "Alle ansehen", "View_Logs": "Logs anzeigen", "View_mode": "Ansichts-Modus", @@ -1319,6 +1422,7 @@ "Visitor_page_URL": "URL der Besucherseite", "Visitor_time_on_site": "Besucherzeit auf der Seite", "Wait_activation_warning": "Bevor Sie sich anmelden können, muss das Konto von einem Administrator manuell aktiviert werden.", + "Warnings": "Warnungen", "We_are_offline_Sorry_for_the_inconvenience": "Wir sind offline. Entschuldigen Sie die Unannehmlichkeiten.", "We_have_sent_password_email": "Wir haben Ihnen eine Anleitung zum Zurücksetzen des Passworts an Ihre E-Mail-Adresse gesendet. Wenn Sie keine E-Mail erhalten haben, versuchen Sie es bitte noch einmal.", "We_have_sent_registration_email": "Wir haben Ihnen eine Bestätigungsmail gesendet. Wenn Sie keine E-Mail erhalten haben, versuchen Sie es bitte noch einmal.", @@ -1375,4 +1479,4 @@ "your_message_optional": "ihre optionale Nachricht", "Your_password_is_wrong": "Falsches Passwort", "Your_push_was_sent_to_s_devices": "Die Push-Nachricht wurde an %s Geräte gesendet." -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/el.i18n.json b/packages/rocketchat-i18n/i18n/el.i18n.json index d4046ac1362dfa83ecaf2388bc5cde88c9c61d0f..ba47065b067990421852ccb0acacfbb914e2cc49 100644 --- a/packages/rocketchat-i18n/i18n/el.i18n.json +++ b/packages/rocketchat-i18n/i18n/el.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "Επιτρέψτε Προφίλ Χρήστη Αλλαγή", "Accounts_AvatarResize": "Αλλαγή μεγέθους Avatars", "Accounts_AvatarSize": "Avatar Μέγεθος", - "Accounts_AvatarStorePath": "Path Avatar αποθήκευσης", - "Accounts_AvatarStoreType": "Avatar Τύπος αποθήκευσης", "Accounts_BlockedDomainsList": "Λίστα αποκλεισμένων τομέων", "Accounts_BlockedDomainsList_Description": "Διαχωρισμένες με κόμμα λίστα των αποκλεισμένων περιοχών", "Accounts_BlockedUsernameList": "Λίστα αποκλεισμένων Όνομα Χρήστη", diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 6500fda913b10097ab826641ca9a7a211857e7eb..1b2888536b9a029fe99343300f2577241197d464 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -29,8 +29,6 @@ "Accounts_AllowUserProfileChange": "Allow User Profile Change", "Accounts_AvatarResize": "Resize Avatars", "Accounts_AvatarSize": "Avatar Size", - "Accounts_AvatarStorePath": "Avatar Storage Path", - "Accounts_AvatarStoreType": "Avatar Storage Type", "Accounts_BlockedDomainsList": "Blocked Domains List", "Accounts_BlockedDomainsList_Description": "Comma-separated list of blocked domains", "Accounts_BlockedUsernameList": "Blocked Username List", @@ -753,7 +751,9 @@ "Integrations_for_all_channels": "Enter all_public_channels to listen on all public channels, all_private_groups to listen on all private groups, and all_direct_messages to listen to all direct messages.", "InternalHubot": "Internal Hubot", "InternalHubot_ScriptsToLoad": "Scripts to load", - "InternalHubot_ScriptsToLoad_Description": "Please enter a comma separated list of scripts to load from https://github.com/github/hubot-scripts/tree/master/src/scripts", + "InternalHubot_ScriptsToLoad_Description": "Please enter a comma separated list of scripts to load from your custom folder", + "InternalHubot_PathToLoadCustomScripts": "Folder to load the scripts", + "InternalHubot_reload": "Reload the scripts", "InternalHubot_Username_Description": "This must be a valid username of a bot registered on your server.", "Invalid_confirm_pass": "The password confirmation does not match password", "Invalid_email": "The email entered is invalid", @@ -875,7 +875,7 @@ "LDAP_Sync_User_Data": "Sync Data", "LDAP_Sync_User_Data_Description": "Keep user data in sync with server on login (eg: name, email).", "LDAP_Sync_User_Data_FieldMap": "User Data Field Map", - "LDAP_Sync_User_Data_FieldMap_Description": "Configure how user account fields (like email) are populated from a record in LDAP (once found).
As an example, `{\"cn\":\"name\", \"mail\":\"email\"}` will choose a person's human readable name from the cn attribute, and their email from the mail attribute.
Available fields include `name`, and `email`.", + "LDAP_Sync_User_Data_FieldMap_Description": "Configure how user account fields (like email) are populated from a record in LDAP (once found).
As an example, `{\"cn\":\"name\", \"mail\":\"email\"}` will choose a person's human readable name from the cn attribute, and their email from the mail attribute. Additionally it is possible to use variables, for example: `{ \"#{givenName} #{sn}\": \"name\", \"mail\": \"email\" }` uses a combination of the user's first name and last name for the rocket chat `name` field.
Available fields in Rocket.Chat: `name`, and `email`.", "LDAP_Sync_Users": "Sync Users", "LDAP_Test_Connection": "Test Connection", "LDAP_Unique_Identifier_Field": "Unique Identifier Field", @@ -1237,6 +1237,7 @@ "Registration_via_Admin": "Registration via Admin", "Regular_Expressions": "Regular Expressions", "Release": "Release", + "Reload": "Reload", "Remove": "Remove", "Remove_Admin": "Remove Admin", "Remove_as_moderator": "Remove as moderator", diff --git a/packages/rocketchat-i18n/i18n/es.i18n.json b/packages/rocketchat-i18n/i18n/es.i18n.json index e5dceb0f4260ca4bede8b1b3338d7e963e327b8c..915652d161499b9bbc7b218cb133bd465108d40c 100644 --- a/packages/rocketchat-i18n/i18n/es.i18n.json +++ b/packages/rocketchat-i18n/i18n/es.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "Permitir al Usuario modificar su Perfil", "Accounts_AvatarResize": "Cambiar el Tamaño de los Avatars", "Accounts_AvatarSize": "Tamaño de Avatar", - "Accounts_AvatarStorePath": "Ruta de almacenamiento de los avatares", - "Accounts_AvatarStoreType": "Tipo de Almacenamiento de Avatar", "Accounts_BlockedDomainsList": "Lista de dominios bloqueados", "Accounts_BlockedDomainsList_Description": "Lista de dominios bloqueados separada por comas", "Accounts_BlockedUsernameList": "Lista de nombres de usuario bloqueados", diff --git a/packages/rocketchat-i18n/i18n/fa.i18n.json b/packages/rocketchat-i18n/i18n/fa.i18n.json index 040e0fbb69747d11d8bfcd8d881e95de77358d57..db2d3e05f9a7528a506b7a559bd02c88bfa5e3eb 100644 --- a/packages/rocketchat-i18n/i18n/fa.i18n.json +++ b/packages/rocketchat-i18n/i18n/fa.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "کاربر اجازه می دهد مشخصات تغییر", "Accounts_AvatarResize": "تغییر اندازه آواتار ها", "Accounts_AvatarSize": "آواتار حجم", - "Accounts_AvatarStorePath": "مسیر آواتار ذخیره سازی", - "Accounts_AvatarStoreType": "آواتار نوع ذخیره سازی", "Accounts_BlockedDomainsList": "مسدود شده فهرست دامنه", "Accounts_BlockedDomainsList_Description": "جدا شده با کاما از حوزه های مسدود شده", "Accounts_BlockedUsernameList": "مسدود شده نام کاربری", diff --git a/packages/rocketchat-i18n/i18n/fi.i18n.json b/packages/rocketchat-i18n/i18n/fi.i18n.json index 2b0f459d43b82c879e547fefa8c6e6f5106fbb9b..fbfdaa6bc41159ff936e633022d46d07fd84711b 100644 --- a/packages/rocketchat-i18n/i18n/fi.i18n.json +++ b/packages/rocketchat-i18n/i18n/fi.i18n.json @@ -26,8 +26,6 @@ "Accounts_AllowUserProfileChange": "Salli käyttäjän profiilin muutos", "Accounts_AvatarResize": "Muuta avatarien kokoa", "Accounts_AvatarSize": "Avatarin koko", - "Accounts_AvatarStorePath": "Avatarin tallennuspolku", - "Accounts_AvatarStoreType": "Avatarien tallennusmuoto", "Accounts_BlockedDomainsList": "Estettyjen verkkotunnusten lista", "Accounts_BlockedDomainsList_Description": "Pilkuilla eroteltu lista estetyistä verkkotunnuksista", "Accounts_BlockedUsernameList": "Estettyjen käyttäjätunnusten lista", diff --git a/packages/rocketchat-i18n/i18n/fr.i18n.json b/packages/rocketchat-i18n/i18n/fr.i18n.json index 7d9b43e0c9fcb5533db0a5d4bffa9cd8a4ecb787..e3944df550d252e8ad23739fa45496a5f0a14a08 100644 --- a/packages/rocketchat-i18n/i18n/fr.i18n.json +++ b/packages/rocketchat-i18n/i18n/fr.i18n.json @@ -10,13 +10,15 @@ "__username__is_no_longer__role__defined_by__user_by_": "__user_by__ a retiré le rôle __role__ à __username__", "__username__was_set__role__by__user_by_": "__user_by__ a donné le rôle __role__ à __username__", "Accept": "Accepter", - "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Accepter les demande de chat en direct même si il n'y a pas d'assistant en ligne", - "Accept_with_no_online_agents": "Accepter sans assistant enligne", + "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Accepter les demandes de chat en ligne même si il n'y a pas d'agent en ligne", + "Accept_with_no_online_agents": "Accepter sans agent en ligne", "Access_not_authorized": "Accès non autorisé", "Access_Token_URL": "URL du jeton d'accès", "Accessing_permissions": "Accès aux permissions", "Account_SID": "SID du compte", "Accounts": "Comptes", + "Accounts_AllowAnonymousRead": "Autoriser la lecture anonyme", + "Accounts_AllowAnonymousWrite": "Autoriser l'écriture anonyme", "Accounts_AllowDeleteOwnAccount": "Autoriser les utilisateurs à supprimer leur propre compte", "Accounts_AllowedDomainsList": "Liste des domaines autorisés", "Accounts_AllowedDomainsList_Description": "Liste des domaines autorisés, séparés par des virgules", @@ -27,13 +29,12 @@ "Accounts_AllowUserProfileChange": "Autoriser la modification de profil", "Accounts_AvatarResize": "Redimensionner les avatars", "Accounts_AvatarSize": "Taille de l'avatar", - "Accounts_AvatarStorePath": "Chemin de stockage des avatars", - "Accounts_AvatarStoreType": "Type de stockage des avatars", "Accounts_BlockedDomainsList": "Liste des domaines bloqués", "Accounts_BlockedDomainsList_Description": "Liste de domaines bloqués, séparés par des virgules", "Accounts_BlockedUsernameList": "Liste des noms d'utilisateurs bloqués", "Accounts_BlockedUsernameList_Description": "Liste de noms d'utilisateurs bloqués (insensible à la casse), séparés par des virgules", "Accounts_CustomFields_Description": "Devrait être un JSON valide où les clés sont les noms des champs contenant un dictionnaire de champs de paramétrage. Exemple :
\n{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"eleve\",\n  \"options\": [\"enseignant\", \"eleve\"],\n  \"required\": true,\n  \"modifyRecordField\": {\n   \"array\": true,\n   \"field\": \"roles\"\n  }\n },\n \"twitter\": {\n  \"type\": \"text\",\n  \"required\": true,\n  \"minLength\": 2,\n  \"maxLength\": 10\n }\n} ", + "Accounts_DefaultUsernamePrefixSuggestion": "Suggestion par défaut du préfixe du nom d'utilisateur", "Accounts_denyUnverifiedEmail": "Refuser les e-mails non vérifiés", "Accounts_EmailVerification": "Vérification de l'adresse e-mail", "Accounts_EmailVerification_Description": "Vous devez avoir des paramètres SMTP corrects pour utiliser cette fonctionnalité", @@ -137,8 +138,8 @@ "Additional_Feedback": "Commentaires supplémentaires", "Administration": "Administration", "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "Après l'authentification par OAuth2, les utilisateurs seront redirigés vers cette URL", - "Agent": "Assistant", - "Agent_added": "Assistant ajouté", + "Agent": "Agent", + "Agent_added": "Agent ajouté", "Agent_removed": "Assistant supprimé", "Alias": "Alias", "Alias_Format": "Format d'alias", @@ -377,8 +378,8 @@ "Delete_Room_Warning": "Supprimer un salon supprimera également tous les messages postés dans le salon. Cette action est irréversible.", "Delete_User_Warning": "Supprimer un utilisateur va également supprimer tous les messages de celui-ci. Cette action est irréversible.", "Deleted": "Supprimé !", - "Department": "Département", - "Department_removed": "Département supprimé", + "Department": "Service", + "Department_removed": "Service supprimé", "Departments": "Départements", "Deployment_ID": "ID de déploiement", "Description": "Description", @@ -409,7 +410,7 @@ "Duration": "Durée", "Edit": "Modifier", "Edit_Custom_Field": "Modifier le champ personnalisé", - "Edit_Department": "Éditer le département", + "Edit_Department": "Éditer le service", "Edit_Trigger": "Éditer le déclencheur", "edited": "modifié", "Editing_room": "Modification du salon", @@ -459,7 +460,7 @@ "error-could-not-change-name": "Impossible de modifier le nom", "error-could-not-change-username": "Impossible de modifier le nom d'utilisateur", "error-delete-protected-role": "Impossible de supprimer un rôle protégé", - "error-department-not-found": "Département introuvable", + "error-department-not-found": "Service introuvable", "error-duplicate-channel-name": "Un canal avec le nom '__channel_name__' existe déjà", "error-email-domain-blacklisted": "Le domaine de l'adresse e-mail est sur liste noire", "error-email-send-failed": "Erreur lors de la tentative d'envoi d'e-mail : __message__", @@ -581,7 +582,7 @@ "Forgot_password_section": "Mot de passe oublié", "Forward": "Transmettre", "Forward_chat": "Transmettre la conversation", - "Forward_to_department": "Transmettre au département", + "Forward_to_department": "Transmettre au service", "Forward_to_user": "Transmettre à l'utilisateur", "Frequently_Used": "Fréquemment utilisé", "Friday": "Vendredi", @@ -614,10 +615,10 @@ "Host": "Hôte", "hours": "heures", "Hours": "Heures", - "How_friendly_was_the_chat_agent": "L'assistant du chat était-il amical ?", - "How_knowledgeable_was_the_chat_agent": "L'assistant du chat était-il clair ?", + "How_friendly_was_the_chat_agent": "Votre interlocuteur était-il amical ?", + "How_knowledgeable_was_the_chat_agent": "Votre interlocuteur était-il clair ?", "How_long_to_wait_after_agent_goes_offline": "Délai d'attente après que l'agent soit hors ligne", - "How_responsive_was_the_chat_agent": "L'assistant du chat avait-il des réponses adaptées ?", + "How_responsive_was_the_chat_agent": "Votre interlocuteur avait-il des réponses adaptées ?", "How_satisfied_were_you_with_this_chat": "Étiez-vous satisfait de ce chat?", "How_to_handle_open_sessions_when_agent_goes_offline": "Comment gérer les sessions ouvertes lorsque l'asistant passe hors ligne", "If_this_email_is_registered": "Si cet e-mail est enregistré, les instructions pour réinitialiser votre mot de passe vous serons envoyées. Si vous ne recevez pas d'email rapidement, merci de revenir et d'essayer à nouveau.", @@ -953,12 +954,12 @@ "N_new_messages": "%s nouveaux messages", "Name": "Nom", "Name_cant_be_empty": "Le nom ne peut pas être vide", - "Name_of_agent": "Nom de l'assistant", + "Name_of_agent": "Nom de l'agent", "Name_optional": "Nom (optionnel)", "Navigation_History": "Historique de navigation", "New_Application": "Nouvelle application", "New_Custom_Field": "Nouveau champ personnalisé", - "New_Department": "Nouveau département", + "New_Department": "Nouveau service", "New_integration": "Nouvelle intégration", "New_logs": "Nouveaux journaux", "New_Message_Notification": "Notification de nouveau message", @@ -968,7 +969,7 @@ "New_Room_Notification": "Notification de nouveau salon", "New_videocall_request": "Nouvelle demande d'appel vidéo", "New_Trigger": "Nouveau déclencheur", - "No_available_agents_to_transfer": "Aucun assistant disponible pour le transfert", + "No_available_agents_to_transfer": "Aucun agent disponible pour le transfert", "No_channel_with_name_%s_was_found": "Aucun canal nommé \"%s\" n'a été trouvé !", "No_channels_yet": "Vous ne faites partie d’aucun canal pour le moment.", "No_direct_messages_yet": "Vous n'avez pris part à aucune discussion pour le moment.", @@ -1029,6 +1030,7 @@ "optional": "facultatif", "or": "ou", "Order": "Ordre", + "Or_talk_as_anonymous": "Ou discutez de manière anonyme", "OS_Arch": "Architecture", "OS_Cpus": "Nombre de CPU", "OS_Freemem": "Mémoire disponible", @@ -1216,7 +1218,7 @@ "seconds": "secondes", "Secret_token": "Jeton secret", "Security": "Sécurité", - "Select_a_department": "Sélectionner un département", + "Select_a_department": "Sélectionner un service", "Select_a_user": "Sélectionner un utilisateur", "Select_an_avatar": "Choisissez un avatar", "Select_file": "Sélectionnez le fichier", @@ -1261,6 +1263,7 @@ "Showing_archived_results": "

Affichage de %s résultats archivés

", "Showing_online_users": "__total_showing__ utilisateur(s) affichés sur un total de __total__", "Showing_results": "

%s résultat(s)

", + "Sign_in_to_start_talking": "Connectez vous pour commencer à discuter", "since_creation": "depuis %s", "Site_Name": "Nom du site", "Site_Url": "URL du site", @@ -1550,7 +1553,7 @@ "Yes_leave_it": "Oui, je veux partir !", "Yes_mute_user": "Oui, rend muet l'utilisateur !", "Yes_remove_user": "Oui, éjecte l'utilisateur !", - "You": "Toi", + "You": "Vous", "you_are_in_preview_mode_of": "Aperçu du salon #__room_name__ ", "You_are_logged_in_as": "Vous êtes connecté en tant que", "You_are_not_authorized_to_view_this_page": "Vous n'avez pas l'autorisation de voir cette page.", diff --git a/packages/rocketchat-i18n/i18n/he.i18n.json b/packages/rocketchat-i18n/i18n/he.i18n.json index b912f90ea58d43bb8bf104d0a775c82f666ca581..3be918e8545b1e49846e69b665f50d9c0c7da2d7 100644 --- a/packages/rocketchat-i18n/i18n/he.i18n.json +++ b/packages/rocketchat-i18n/i18n/he.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "אפשר למשתמש לשנות את הפרופיל", "Accounts_AvatarResize": "שנה את גודל תמונת הפרופיל", "Accounts_AvatarSize": "גודל תמונת הפרופיל", - "Accounts_AvatarStorePath": "נתיב תמונת הפרופיל", - "Accounts_AvatarStoreType": "סוג אחסון תמונת פרופיל", "Accounts_BlockedDomainsList": "רשימת דומיינים חסומים", "Accounts_BlockedDomainsList_Description": "רשימה מופרדת בפסיקים של דומיינים חסומים", "Accounts_BlockedUsernameList": "רשימת משתמש חסום", diff --git a/packages/rocketchat-i18n/i18n/hr.i18n.json b/packages/rocketchat-i18n/i18n/hr.i18n.json index 83e9f1a937d0ca6823ce174ed3dd834338c26f59..56c2306b8c8749fecbc24de8b1081abed3d49f8f 100644 --- a/packages/rocketchat-i18n/i18n/hr.i18n.json +++ b/packages/rocketchat-i18n/i18n/hr.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "Dopusti Promjenu Profila", "Accounts_AvatarResize": "Promjeni veličinu Avatara", "Accounts_AvatarSize": "Veličina Avatara", - "Accounts_AvatarStorePath": "Putanja do spremišta Avatara ", - "Accounts_AvatarStoreType": "Tip Spremišta Avatara", "Accounts_BlockedDomainsList": "Popis blokiranih domena", "Accounts_BlockedDomainsList_Description": "Popis blokiranih domena odvojenih zarezom", "Accounts_BlockedUsernameList": "Popis blokiranih korisničkih imena", diff --git a/packages/rocketchat-i18n/i18n/hu.i18n.json b/packages/rocketchat-i18n/i18n/hu.i18n.json index 93d556692db428f7ca808c9aeacadd0369d1473c..7a936671ff20a858ea5d6ddc815d15e514da1154 100644 --- a/packages/rocketchat-i18n/i18n/hu.i18n.json +++ b/packages/rocketchat-i18n/i18n/hu.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "Felhasználó módosíthatja profilját", "Accounts_AvatarResize": "Profilképek átméretezése", "Accounts_AvatarSize": "Profilkép mérete", - "Accounts_AvatarStorePath": "Profilkép tárolásának elérési útja", - "Accounts_AvatarStoreType": "Profilkép tárolásának típusa", "Accounts_BlockedDomainsList": "Blokkolt domainek listája", "Accounts_BlockedDomainsList_Description": "Blokkolt domain-ek listája (vesszővel elválasztva)", "Accounts_BlockedUsernameList": "Blokkolt felhasználónevek listája", diff --git a/packages/rocketchat-i18n/i18n/id.i18n.json b/packages/rocketchat-i18n/i18n/id.i18n.json index c41f59d8ca82474050b926e05feea905f019c271..954bc6247aba427998b9d4d264195119885cb156 100644 --- a/packages/rocketchat-i18n/i18n/id.i18n.json +++ b/packages/rocketchat-i18n/i18n/id.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "Memungkinkan Profil Pengguna Ganti", "Accounts_AvatarResize": "Ubah Ukuran Avatar", "Accounts_AvatarSize": "Ukuran Avatar", - "Accounts_AvatarStorePath": "Lokasi Penyimpanan Avatar", - "Accounts_AvatarStoreType": "Tipe Penyimpanan Avatar", "Accounts_BlockedDomainsList": "Daftar Domain diblokir", "Accounts_BlockedDomainsList_Description": "Dipisahkan dengan koma daftar domain diblokir", "Accounts_BlockedUsernameList": "Diblokir Daftar Nama", diff --git a/packages/rocketchat-i18n/i18n/it.i18n.json b/packages/rocketchat-i18n/i18n/it.i18n.json index 06d62a2f4d7fc6289056fd013ef88e3cb6495c85..f7d280bdb3390b4e183a889445c663dc44c6feb5 100644 --- a/packages/rocketchat-i18n/i18n/it.i18n.json +++ b/packages/rocketchat-i18n/i18n/it.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "Consenti cambio profilo utente", "Accounts_AvatarResize": "Ridimensiona Avatar", "Accounts_AvatarSize": "Dimensione Avatar", - "Accounts_AvatarStorePath": "Percorso Avatar", - "Accounts_AvatarStoreType": "Tipo di archiviazione per gli Avatar", "Accounts_BlockedDomainsList": "Elenco domini bloccati", "Accounts_BlockedDomainsList_Description": "elenco separato da virgole dei domini bloccati", "Accounts_BlockedUsernameList": "Lista nomi utente bloccati", diff --git a/packages/rocketchat-i18n/i18n/ja.i18n.json b/packages/rocketchat-i18n/i18n/ja.i18n.json index d207cdff4f02c0f98c34490cc3f494e9b4044463..39155bf24657a425a51970daf1679b56ec530137 100644 --- a/packages/rocketchat-i18n/i18n/ja.i18n.json +++ b/packages/rocketchat-i18n/i18n/ja.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "プロフィールの変更を許可する", "Accounts_AvatarResize": "アバターの大きさを変更する", "Accounts_AvatarSize": "アバターの大きさ", - "Accounts_AvatarStorePath": "アバターの保存先", - "Accounts_AvatarStoreType": "アバターの保存先ストレージ種類", "Accounts_BlockedDomainsList": "ブロックされたドメイン一覧", "Accounts_BlockedDomainsList_Description": "カンマ区切りのブロックされたドメイン一覧", "Accounts_BlockedUsernameList": "ブロックされたユーザー名の一覧", diff --git a/packages/rocketchat-i18n/i18n/km.i18n.json b/packages/rocketchat-i18n/i18n/km.i18n.json index 2374ff4d1139cf1e90478811edd9482b0af74c8c..5a6446815f252653703904e735e02d162a2886b2 100644 --- a/packages/rocketchat-i18n/i18n/km.i18n.json +++ b/packages/rocketchat-i18n/i18n/km.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "អនុញ្ញាតិអ្នកប្រើប្រាស់ប្តូរព័ត៌មានផ្ទាល់ខ្លួន", "Accounts_AvatarResize": "ប្តូ​រ​ទំហំ Avatar", "Accounts_AvatarSize": "ទំហំ Avatar", - "Accounts_AvatarStorePath": "ទីតាំងផ្ទុក Avatar", - "Accounts_AvatarStoreType": "ប្រភេទបន្ទុក Avatar", "Accounts_BlockedDomainsList": "បញ្ជីដែន", "Accounts_BlockedDomainsList_Description": "បញ្ជីបំបែកដោយសញ្ញាក្បៀសនៃដែនបានបិទ", "Accounts_BlockedUsernameList": "បញ្ជីឈ្មោះអ្នកប្រើ", diff --git a/packages/rocketchat-i18n/i18n/ko.i18n.json b/packages/rocketchat-i18n/i18n/ko.i18n.json index 02492e8aa80cd41ca749842fdf2d6630aa591094..5d7f599dd39c2c10e83de650a6444e51238e7a3f 100644 --- a/packages/rocketchat-i18n/i18n/ko.i18n.json +++ b/packages/rocketchat-i18n/i18n/ko.i18n.json @@ -6,17 +6,17 @@ "403": "금지됨", "500": "내부 서버 오류", "@username": "@사용자명", - "@username_message": "@username ", - "__username__is_no_longer__role__defined_by__user_by_": "__ 사용자 이름 __는 __user_by__에 의해, __role__ 더 이상 없다", - "__username__was_set__role__by__user_by_": "__ 사용자 이름 __은 __user_by__에 의해 __role__ 설정했다", + "@username_message": "@사용자이름 ", + "__username__is_no_longer__role__defined_by__user_by_": "__사용자는 더이상 __가설정한 __역할이 없습니다", + "__username__was_set__role__by__user_by_": "__ 사용자에게 __사용자가 __역할을__ 설정하였습니다", "Accept": "수락", - "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "온라인 상담원이없는 경우에도 들어오는 실시간 채팅 요청 수락", - "Access_not_authorized": "액세스 권한이 없습니다", + "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "상담원이 온라인 상태가 아닌 경우에도 라이브챗을 수락합니다.", + "Access_not_authorized": "엑세스 권한이 없습니다", "Access_Token_URL": "액세스 토큰 URL", - "Accessing_permissions": "권한 액세스", + "Accessing_permissions": "접속권한", "Account_SID": "계정 SID", "Accounts": "계정", - "Accounts_AllowDeleteOwnAccount": "사용자가 자신의 계정을 삭제할 수 있습니다.", + "Accounts_AllowDeleteOwnAccount": "사용자가 자신의 계정을 삭제할 수 있습니다", "Accounts_AllowedDomainsList": "허용된 도메인 목록", "Accounts_AllowedDomainsList_Description": "허용된 도메인을 쉼표(,)로 구분하기", "Accounts_AllowEmailChange": "이메일 변경을 허용합니다", @@ -26,13 +26,11 @@ "Accounts_AllowUserProfileChange": "사용자 프로필 변경을 허용", "Accounts_AvatarResize": "아바타 크기 조정", "Accounts_AvatarSize": "아바타 크기", - "Accounts_AvatarStorePath": "아바타 저장 경로", - "Accounts_AvatarStoreType": "아바파 저장 타입", "Accounts_BlockedDomainsList": "차단된 도메인 목록", - "Accounts_BlockedDomainsList_Description": "차단 된 도메인의 쉼표로 구분 된 목록", - "Accounts_BlockedUsernameList": "차단 된 사용자 이름 목록", - "Accounts_BlockedUsernameList_Description": "차단 된 사용자 이름의 쉼표로 구분 된 목록 (대소 문자 구분)", - "Accounts_CustomFields_Description": "키는 필드 세팅의 딕셔너리(dictionary) 를 포함하는 필드 이름들이어야 합니다.\n\n예:
{\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} ", + "Accounts_BlockedDomainsList_Description": "쉼표로 구문된 차단 도메인 리스트", + "Accounts_BlockedUsernameList": "차단된 사용자 리스트", + "Accounts_BlockedUsernameList_Description": "쉼표로 구문된 차단 사용자 리스트 (대소 문자 구분)", + "Accounts_CustomFields_Description": "필드 설정에 포함된 필드명을 사용한 올바른 JSON 이여야 합니다.\n\n예:
{\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} ", "Accounts_denyUnverifiedEmail": "확인되지 않은 이메일 거부", "Accounts_EmailVerification": "이메일 확인", "Accounts_EmailVerification_Description": "이 기능을 사용하려면 SMTP설정이 올바르게 되어있는지 확인해주십시오.", @@ -40,7 +38,7 @@ "Accounts_Enrollment_Email_Default": "

에 오신 것을 환영합니다

[Site_Name]

[Site_URL]로 이동하여 오늘날 최고의 오픈 소스 채팅 솔루션을보십시오!

", "Accounts_Enrollment_Email_Description": "당신은 각각 사용자의 전체 이름, 이름 또는 성을 위해 [lname], [name], [fname]을 사용할 수 있습니다.
당신은 사용자의 이메일을 [email]을 사용할 수 있습니다.", "Accounts_Enrollment_Email_Subject_Default": "[Site_Name] 에 오신 것을 환영합니다 ", - "Accounts_ForgetUserSessionOnWindowClose": "윈도루를 닫을 때에 사용자 설정을 삭제 합니다.", + "Accounts_ForgetUserSessionOnWindowClose": "창을 닫을때 사용자 세션을 삭제합니다", "Accounts_Iframe_api_method": "API 메소드", "Accounts_Iframe_api_url": "API URL", "Accounts_iframe_enabled": "사용", @@ -61,8 +59,11 @@ "Accounts_OAuth_Custom_Token_Path": "Token 경로", "Accounts_OAuth_Custom_Token_Sent_Via": "토큰 보낸 비아", "Accounts_OAuth_Custom_Username_Field": "사용자 이름 필드", - "Accounts_OAuth_Drupal": "Drupal Login 이 활성화 됨", - "Accounts_OAuth_Facebook": "Facebook 로그인", + "Accounts_OAuth_Drupal": "듀팔 로그인 이 활성화 되었습니다.", + "Accounts_OAuth_Drupal_callback_url": "듀팔 oAuth2 리다이렉트 URI", + "Accounts_OAuth_Drupal_id": "듀팔 oAuth2 클라이언트 ID", + "Accounts_OAuth_Drupal_secret": "듀팔 oAuth2 클라이언트 비밀번호", + "Accounts_OAuth_Facebook": "페이스북 로그인", "Accounts_OAuth_Facebook_callback_url": "페이스 북 콜백 URL", "Accounts_OAuth_Facebook_id": "Facebook 앱 ID", "Accounts_OAuth_Facebook_secret": "Facebook 암호", @@ -101,6 +102,7 @@ "Accounts_PasswordReset": "암호 재설정", "Accounts_OAuth_Proxy_host": "프록시 서버", "Accounts_OAuth_Proxy_services": "프록시 서비스", + "Accounts_Registration_AuthenticationServices_Default_Roles": "인증서비스용 기본 역할", "Accounts_Registration_AuthenticationServices_Enabled": "인증 서비스에 등록", "Accounts_RegistrationForm": "등록 양식", "Accounts_RegistrationForm_Disabled": "비활성화", @@ -111,43 +113,43 @@ "Accounts_RegistrationForm_SecretURL_Description": "당신은 당신의 등록 URL에 추가됩니다 임의의 문자열을 제공해야합니다. 예 : https://demo.rocket.chat/register/[secret_hash]", "Accounts_RequireNameForSignUp": "회원 가입은 이름 필요", "Accounts_SetDefaultAvatar": "기본 아바타 설정", - "Accounts_ShowFormLogin": "보기 양식 기반 로그인", - "Accounts_UseDefaultBlockedDomainsList": "기본값 사용 차단 된 도메인 목록", - "Accounts_UseDNSDomainCheck": "DNS 도메인 확인을 사용하여", + "Accounts_ShowFormLogin": "폼방식 로그인 보기", + "Accounts_UseDefaultBlockedDomainsList": "기본 차단 도메인리스트 사용", + "Accounts_UseDNSDomainCheck": "DNS 도메인 확인 사용", "Accounts_UserAddedEmail_Default": "

에 오신 것을 환영합니다

[Site_Name]

[Site_URL]로 이동하여 오늘날 최고의 오픈 소스 채팅 솔루션을보십시오!

[email]과 비밀번호 : [password] 당신은 당신의 이메일을 사용하여 로그인 할 수 있습니다. 당신은 처음 로그인 후 변경해야 할 수 있습니다.", "Accounts_UserAddedEmail_Description": "다음과 같은 자리를 사용할 수 있습니다 :

", - "Accounts_UserAddedEmailSubject_Default": "당신이 추가되었습니다 [Site_Name]", + "Accounts_UserAddedEmailSubject_Default": "당신이은 [Site_Name] 에 추가되었습니다", "Activate": "활성화", "Activity": "활동", "Add": "추가", - "Add_agent": "에이전트를 추가", + "Add_agent": "상담사 추가", "Add_custom_oauth": "사용자 정의 OAuth 추가", "Add_Domain": "도메인 추가", "Add_manager": "관리자 추가", "Add_user": "사용자 추가", "Add_User": "사용자 추가", "Add_users": "사용자 추가", - "Adding_OAuth_Services": "OAuth는 서비스 추가", - "Adding_permission": "권한을 추가", - "Adding_user": "추가 사용자", - "Additional_emails": "추가 E - 메일", + "Adding_OAuth_Services": "OAuth 서비스 추가", + "Adding_permission": "권한 추가", + "Adding_user": "사용자 추가", + "Additional_emails": "추가 이메일", "Additional_Feedback": "추가 의견", "Administration": "관리", - "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "OAuth2를 인증 한 후, 사용자는이 URL로 리디렉션됩니다", - "Agent": "에이전트", - "Agent_added": "에이전트는 추가", - "Agent_removed": "에이전트 제거", + "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "Oauth2 인증후 사용자는 이 URL로 이동됩니다", + "Agent": "상담사", + "Agent_added": "상담사가 추가되었습니다", + "Agent_removed": "상담사가 삭제되었습니다", "Alias": "별명", - "Alias_Format": "별칭 형식", + "Alias_Format": "별명 형식", "Alias_Set": "별칭 설정", "All": "모든", "All_channels": "모든 채널", "All_logs": "모든 로그", "All_messages": "모든 메시지", - "Allow_Invalid_SelfSigned_Certs": "잘못된 Self-Signed Certs 허용", - "Allow_Invalid_SelfSigned_Certs_Description": "링크 확인 및 미리보기 무효 및 자체 서명 된 SSL 인증서의 허용.", - "Allow_switching_departments": "방문자가 부서를 변경할 수 있도록 허용함", - "Analytics_features_enabled": "기능 활성화", + "Allow_Invalid_SelfSigned_Certs": "잘못된 자체서명 Certs 를 허용합니다", + "Allow_Invalid_SelfSigned_Certs_Description": "링크확인 과 프리뷰에 잘못된 자체서명 Certs 를 허용합니다.", + "Allow_switching_departments": "방문자가 부서를 변경할 수 있도록 허용합니다", + "Analytics_features_enabled": "기능이 활성화 되었습니다", "Analytics_features_messages_Description": "사용자가 메시지에 대해 수행 행동과 관련된 사용자 정의 이벤트를 추적합니다.", "Analytics_features_rooms_Description": "채널 또는 그룹 (삭제두고 작성)에 대한 작업에 관련된 사용자 정의 이벤트를 추적합니다.", "Analytics_features_users_Description": "사용자 (암호 재설정 시간, 프로필 사진 변경 등)에 관련 작업에 관련된 사용자 정의 이벤트를 추적합니다.", diff --git a/packages/rocketchat-i18n/i18n/ku.i18n.json b/packages/rocketchat-i18n/i18n/ku.i18n.json index ac6d31aede773be005f66283da2d496aff55d2d0..b4fd33eb3a3e1972e56d923c5d350111f67c6fab 100644 --- a/packages/rocketchat-i18n/i18n/ku.i18n.json +++ b/packages/rocketchat-i18n/i18n/ku.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "Destûrê bide User Profile Change", "Accounts_AvatarResize": "resize Avatars", "Accounts_AvatarSize": "Avatar Size", - "Accounts_AvatarStorePath": "Path Avatar Storage", - "Accounts_AvatarStoreType": "Avatar Type Storage", "Accounts_BlockedDomainsList": "Astengkirin Lîsteya Domain", "Accounts_BlockedDomainsList_Description": "lîsteya bêhnok-cuda ji qada astengkirin", "Accounts_BlockedUsernameList": "Astengkirin Lîsteya Username", diff --git a/packages/rocketchat-i18n/i18n/lo.i18n.json b/packages/rocketchat-i18n/i18n/lo.i18n.json index 6c71bb90512df3662e92222303c815f20ad083bc..158e1b799ce20d6ab6ea83b8958caeb1faabbdb7 100644 --- a/packages/rocketchat-i18n/i18n/lo.i18n.json +++ b/packages/rocketchat-i18n/i18n/lo.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "ອະນຸຍາດໃຫ້ການປ່ຽນແປງຂໍ້ມູນຂອງຜູ້ໃຊ້", "Accounts_AvatarResize": "ປັບຂະຫນາດນົດ", "Accounts_AvatarSize": "Avatar ຂະຫນາດ", - "Accounts_AvatarStorePath": "ເສັ້ນທາງກ້າວສູ່ຮູບສ່ວນຕົວການເກັບຮັກສາ", - "Accounts_AvatarStoreType": "Avatar ປະເພດການເກັບຮັກສາ", "Accounts_BlockedDomainsList": "ສະກັດຊີ Domains", "Accounts_BlockedDomainsList_Description": "ບັນຊີລາຍຊື່ຈຸດ, ແຍກຂອງໂດເມນກັດ", "Accounts_BlockedUsernameList": "ບັນຊີ Username ກັດ", diff --git a/packages/rocketchat-i18n/i18n/ms-MY.i18n.json b/packages/rocketchat-i18n/i18n/ms-MY.i18n.json index dfb52d0a5e78631caffe7fa26d69c0f2d155e307..0aa1d7eaf49048f2d4454fe5044fe53d75bd390f 100644 --- a/packages/rocketchat-i18n/i18n/ms-MY.i18n.json +++ b/packages/rocketchat-i18n/i18n/ms-MY.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "Benarkan Profil Pengguna Tukar", "Accounts_AvatarResize": "Saiz semula Avatars", "Accounts_AvatarSize": "avatar Saiz", - "Accounts_AvatarStorePath": "Avatar Storage Path", - "Accounts_AvatarStoreType": "Avatar Storage Jenis", "Accounts_BlockedDomainsList": "Senarai Domain Disekat", "Accounts_BlockedDomainsList_Description": "Senarai diasingkan koma bagi domain disekat", "Accounts_BlockedUsernameList": "Senarai Nama pengguna Disekat", diff --git a/packages/rocketchat-i18n/i18n/nl.i18n.json b/packages/rocketchat-i18n/i18n/nl.i18n.json index a175e83d61644546329dd248ec9dab136479aa53..81f9d3fdeafbe66d60cce1b5890f6ff68f4b1d09 100644 --- a/packages/rocketchat-i18n/i18n/nl.i18n.json +++ b/packages/rocketchat-i18n/i18n/nl.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "Sta wijzen van gebruikers profiel toe", "Accounts_AvatarResize": "Wijzig Avatar grootte", "Accounts_AvatarSize": "Avatar grootte", - "Accounts_AvatarStorePath": "Avatar opslag pad", - "Accounts_AvatarStoreType": "Avatar opslag type", "Accounts_BlockedDomainsList": "Geblokkeerde domeinen List", "Accounts_BlockedDomainsList_Description": "Door komma's gescheiden lijst van geblokkeerde domeinen", "Accounts_BlockedUsernameList": "Geblokkeerde Gebruikersnaam List", diff --git a/packages/rocketchat-i18n/i18n/no.i18n.json b/packages/rocketchat-i18n/i18n/no.i18n.json index d1f6bfb211eccddde2a17c9c3008d8043a9e8833..e71892775f9f6456fcbe31f9c0484a76d624eaa7 100644 --- a/packages/rocketchat-i18n/i18n/no.i18n.json +++ b/packages/rocketchat-i18n/i18n/no.i18n.json @@ -1,3 +1,68 @@ { - "0_Errors_Only": "0 - Kun Feil" + "0_Errors_Only": "0 - Kun Feil", + "1_Errors_and_Information": "1 - Feil og informasjon", + "2_Erros_Information_and_Debug": "2 - Feil, Informasjon og Feilsøking", + "403": "Forbudt", + "500": "Intern server feil", + "Accept": "Aksepter", + "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Aksepter innkommende livechat selv om det ikke er noen online", + "Accept_with_no_online_agents": "Aksepter uten online agenter", + "Access_not_authorized": "Aksepter uautoriserte", + "Accounts_Enrollment_Email_Subject_Default": "Velkommen til [Site_Name]", + "Accounts_PasswordReset": "Reset passord", + "Accounts_RegistrationForm_Disabled": "Deaktivert", + "Accounts_RegistrationForm_Public": "Offentlig", + "Activate": "Aktiver", + "Activity": "Aktivitet", + "Add": "Legg til", + "Add_agent": "Legg til agent", + "Add_Domain": "Legg til domene", + "Add_manager": "Legg til leder", + "Add_user": "Legg til bruker", + "Add_User": "Legg til bruker", + "Add_users": "Legg til brukere", + "Adding_permission": "Legger til rettigheter", + "Adding_user": "Legger til bruker", + "Additional_emails": "Ekstra e-postadresser", + "Additional_Feedback": "Ekstra tilbakemelding", + "Administration": "Administrasjon", + "Agent": "Agent", + "Agent_added": "Lagt til agent", + "Agent_removed": "Fjernet agent", + "All": "Alle", + "All_channels": "Alle kanaler", + "All_logs": "Aller logger", + "All_messages": "Alle meldinger", + "Archive": "Arkiv", + "are_also_typing": "skriver også", + "are_typing": "skriver", + "Are_you_sure": "Er du sikker?", + "Author": "Forfatter", + "Available": "Tilgjengelig", + "Available_agents": "Tilgjengelige agenter", + "away": "borte", + "Away": "Borte", + "away_female": "borte", + "Away_female": "Borte", + "away_male": "borte", + "Away_male": "Borte", + "Back": "Tilbake", + "Back_to_applications": "Tilbake til programmer", + "Back_to_integrations": "Tilbake til integrasjoner", + "Back_to_login": "Tilbake til login", + "Back_to_permissions": "Tilbake til rettigheter", + "Block_User": "Blokker bruker", + "bold": "fet", + "busy": "opptatt", + "Busy": "Opptatt", + "busy_female": "opptatt", + "Busy_female": "Opptatt", + "busy_male": "opptatt", + "Busy_male": "Opptatt", + "by": "av", + "Content": "Innhold", + "Cancel": "Avbryt", + "Cancel_message_input": "Avbryt", + "channel": "kanal", + "Channel": "Kanal" } \ No newline at end of file diff --git a/packages/rocketchat-i18n/i18n/pl.i18n.json b/packages/rocketchat-i18n/i18n/pl.i18n.json index 28492fd71c39f769039508c18eef64a56c00c46a..c93dd52f309ec08b5ac5f207e6423c0892c44e07 100644 --- a/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -24,8 +24,6 @@ "Accounts_AllowUserProfileChange": "Pozwól na zmienianie profilów użytkowników", "Accounts_AvatarResize": "Zmiana rozmiaru avatarów", "Accounts_AvatarSize": "Rozmiar avataru", - "Accounts_AvatarStorePath": "Ścieżka przechowywania avatarów", - "Accounts_AvatarStoreType": "Rodzaj magazynu avatarów", "Accounts_BlockedDomainsList": "Lista zablokowanych domen", "Accounts_BlockedDomainsList_Description": "Oddzielonych przecinkami lista zablokowanych domen", "Accounts_BlockedUsernameList": "Lista zablokowanych użytkowników", diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 6261000c036a70913a87690d310aa7f533317fa2..f34f587d3c202ad6c7b90809056028739e68ffc1 100644 --- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "Permitir que o usuário altere o perfil", "Accounts_AvatarResize": "Redimensionar Avatares", "Accounts_AvatarSize": "Tamanho do Avatar", - "Accounts_AvatarStorePath": "Caminho para armazenar Avatares", - "Accounts_AvatarStoreType": "Tipo de armazenamento de Avatares", "Accounts_BlockedDomainsList": "Lista de Domínios Bloqueados", "Accounts_BlockedDomainsList_Description": "Lista de domínios bloqueados, separados por vírgulas ", "Accounts_BlockedUsernameList": "Lista de nomes de usuário bloqueados", diff --git a/packages/rocketchat-i18n/i18n/pt.i18n.json b/packages/rocketchat-i18n/i18n/pt.i18n.json index 6261000c036a70913a87690d310aa7f533317fa2..bcec56545ecb110b0d651382532da05251a8db71 100644 --- a/packages/rocketchat-i18n/i18n/pt.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt.i18n.json @@ -7,16 +7,18 @@ "500": "Erro Interno do Servidor", "@username": "@username", "@username_message": "@usuario ", - "__username__is_no_longer__role__defined_by__user_by_": "__username__ não pertence mais à __role__, por __user_by__", + "__username__is_no_longer__role__defined_by__user_by_": "__username__ já não pertence a __role__, por __user_by__", "__username__was_set__role__by__user_by_": "__username__ foi definido como __role__ por __user_by__", "Accept": "Aceitar", - "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Aceitar requisições de livechat mesmo se não houverem agentes online", + "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Aceitar pedidos de livechat mesmo se não houverem agentes online", "Accept_with_no_online_agents": "Aceitar sem agentes online", "Access_not_authorized": "Acesso não autorizado", "Access_Token_URL": "URL do Token de Acesso", "Accessing_permissions": "Acessando permissões", "Account_SID": "Account SID", "Accounts": "Contas", + "Accounts_AllowAnonymousRead": "Permitir leitura anónima", + "Accounts_AllowAnonymousWrite": "Permitir escrita anónima", "Accounts_AllowDeleteOwnAccount": "Permitir que usuários apaguem a própria conta", "Accounts_AllowedDomainsList": "Lista de domínios permitidos", "Accounts_AllowedDomainsList_Description": "Lista de domínios permitidos, separados por vírgula", @@ -27,13 +29,12 @@ "Accounts_AllowUserProfileChange": "Permitir que o usuário altere o perfil", "Accounts_AvatarResize": "Redimensionar Avatares", "Accounts_AvatarSize": "Tamanho do Avatar", - "Accounts_AvatarStorePath": "Caminho para armazenar Avatares", - "Accounts_AvatarStoreType": "Tipo de armazenamento de Avatares", "Accounts_BlockedDomainsList": "Lista de Domínios Bloqueados", "Accounts_BlockedDomainsList_Description": "Lista de domínios bloqueados, separados por vírgulas ", "Accounts_BlockedUsernameList": "Lista de nomes de usuário bloqueados", "Accounts_BlockedUsernameList_Description": "Lista de nomes de usuários bloqueados, separada por vírgulas (não diferencia maiúsculas)", "Accounts_CustomFields_Description": "Deve ser um JSON válido onde as chaves são os nomes de campos contendo um dicionário de configuração de campos. Exemplo:
{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"estudante\",\n  \"options\": [\"professor\", \"estudante\"],\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} ", + "Accounts_DefaultUsernamePrefixSuggestion": "Sugestão de prefixo de utilizador por defeito", "Accounts_denyUnverifiedEmail": "Proibir e-mail não verificado", "Accounts_EmailVerification": "Verificação de E-mail", "Accounts_EmailVerification_Description": "Certifique-se de que as configurações de SMTP estão corretas para usar este recurso", @@ -41,6 +42,7 @@ "Accounts_Enrollment_Email_Default": "

Bem-vindo ao

[Site_Name]

Vá para [Site_URL] e tente a melhor solução de bate-papo aberta fonte disponível hoje!

", "Accounts_Enrollment_Email_Description": "Você pode usar [name], [fname], [lname] para o nome completo, primeiro nome ou último nome do usuário, respectivamente.
Você pode usar [email] para o email do usuário.", "Accounts_Enrollment_Email_Subject_Default": "Bem-vindo ao [Site_Name]", + "Accounts_ForgetUserSessionOnWindowClose": "Esquecer sessão de utilizador ao fechar a janela", "Accounts_Iframe_api_method": "Método Api", "Accounts_Iframe_api_url": "URL da API", "Accounts_iframe_enabled": "Habilitado", @@ -55,10 +57,12 @@ "Accounts_OAuth_Custom_id": "Id", "Accounts_OAuth_Custom_Identity_Path": "Identity Path", "Accounts_OAuth_Custom_Login_Style": "Estilo de Login", + "Accounts_OAuth_Custom_Merge_Users": "Migrar utilizadores", "Accounts_OAuth_Custom_Scope": "Escopo", "Accounts_OAuth_Custom_Secret": "Secret", "Accounts_OAuth_Custom_Token_Path": "Token Path", "Accounts_OAuth_Custom_Token_Sent_Via": "Token Enviado Por", + "Accounts_OAuth_Custom_Username_Field": "Campo de Utilizador", "Accounts_OAuth_Facebook": "Login do Facebook", "Accounts_OAuth_Facebook_callback_url": "URL de Callback do Facebook", "Accounts_OAuth_Facebook_id": "Facebook App Id", @@ -96,6 +100,8 @@ "Accounts_OAuth_Wordpress_id": "WordPress ID", "Accounts_OAuth_Wordpress_secret": "WordPress Secret", "Accounts_PasswordReset": "Resetar senha", + "Accounts_OAuth_Proxy_host": "Host de Proxy", + "Accounts_OAuth_Proxy_services": "Serviços de Proxy", "Accounts_Registration_AuthenticationServices_Enabled": "Registro com os Serviços de Autenticação", "Accounts_RegistrationForm": "Formulário de Registro", "Accounts_RegistrationForm_Disabled": "Desativado", @@ -117,6 +123,7 @@ "Add": "Adicionar", "Add_agent": "Adicionar agente", "Add_custom_oauth": "Adicionar oauth customizado", + "Add_Domain": "Adicionar Domínio\n", "Add_manager": "Adicionar gerente", "Add_user": "Adicionar usuário", "Add_User": "Adicionar Usuário", @@ -141,15 +148,22 @@ "All_messages": "Todas as mensagens", "Allow_Invalid_SelfSigned_Certs": "Permitir certificados inválidos e auto-assinados para validação de links e previews", "Allow_Invalid_SelfSigned_Certs_Description": "Permitir certificado SSL inválidos e auto-assinados para validação de link e previews.", + "Allow_switching_departments": "Permitir visitante para mudar de departamentos", + "Always_open_in_new_window": "Abrir sempre em nova janela", "Analytics_features_enabled": "Funcionalidades habilitadas", "Analytics_features_messages_Description": "Rastreia eventos personalizados relacionados a ações que um usuário faz em mensagens.", "Analytics_features_rooms_Description": "Rastreia eventos personalizados relacionados com ações em um canal ou grupo (criar, sair, apague).", "Analytics_features_users_Description": "Rastreia eventos personalizados relacionados às ações relacionadas aos usuários (tempos de redefinição de senha, mudança imagem de perfil, etc).", + "Analytics_Google": "Google Analytics", + "Analytics_Google_id": "ID de Tracking", "and": "e", "And_more": "E mais __length__", "Animals_and_Nature": "Animais e Natureza", + "Announcement": "Anúncios", "API": "API", "API_Analytics": "Analytics", + "API_Drupal_URL": "URL de Servidor Drupal", + "API_Drupal_URL_Description": "Exemplo: https://dominio.com (sem parêntesis)", "API_Embed": "Embed", "API_EmbedDisabledFor": "Desabilitar incorporação para usuários", "API_EmbedDisabledFor_Description": "Lista de nomes de usuário separados por vírgula para desabilitar a pré-visualização de links embutidos", @@ -193,6 +207,9 @@ "AutoLinker_Urls_TLD": "Auto-linkar URLs de TLD", "AutoLinker_Urls_www": "Auto-linkar URLs com 'www'", "AutoLinker_UrlsRegExp": "Expressão Regular para URL", + "Auto_Translate": "Tradução-Automática", + "AutoTranslate_Enabled": "Activar Tradução-Automática", + "AutoTranslate_GoogleAPIKey": "Google API Key", "Available": "Disponível", "Available_agents": "Agentes disponíveis", "Avatar": "Avatar", @@ -211,6 +228,7 @@ "Back_to_login": "Voltar para o login", "Back_to_permissions": "Voltar para permissões", "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "Funcionalidade Beta! Depende que Vídeo Conferência esteja habilitado", + "Block_User": "Bloquear Utilizador", "Body": "Corpo", "bold": "negrito", "bot_request": "Requisição de Bot", @@ -223,6 +241,8 @@ "busy_male": "ocupado", "Busy_male": "Ocupado", "by": "por", + "Content": "Conteúdo", + "cache_cleared": "Cache limpa", "Cancel": "Cancelar", "Cancel_message_input": "Cancelar", "Cannot_invite_users_to_direct_rooms": "Não é possível convidar pessoas para salas diretas", @@ -289,7 +309,9 @@ "Create_new": "Criar um novo", "Created_at": "Data criação", "Created_at_s_by_s": "Criado em %s por %s", + "CRM_Integration": "Integração de CRM", "Current_Chats": "Bate-papos atuais", + "Current_Status": "Estado Actual", "Custom": "Personalizado", "Custom_Emoji": "Emoji ", "Custom_Emoji_Add": "Adicionar Novo Emoji", @@ -303,8 +325,13 @@ "Custom_Fields": "Campos Personalizados", "Custom_oauth_helper": "Ao configurar o seu Provedor de OAuth, você terá que informar uma URL de retorno de chamada. Use
%s
.", "Custom_oauth_unique_name": "Nome exclusivo para oauth customizado", + "Custom_Scripts": "Scripts Customizados", "Custom_Script_Logged_In": "Script Personalizado para usuários logados", "Custom_Script_Logged_Out": "Script Personalizado para usuários não logados", + "Custom_Sounds": "Sons Customizados", + "Custom_Sound_Add": "Adicionar Som Customizado", + "Custom_Sound_Error_Invalid_Sound": "Som inválido", + "Custom_Translations": "Traduções Customizadas", "Dashboard": "Dashboard", "Date": "Data", "Date_From": "De", @@ -335,11 +362,16 @@ "Desktop_Notifications_Enabled": "Notificações Desktop estão Habilitadas", "Direct_message_someone": "Enviar mensagem direta para alguém", "Direct_Messages": "Mensagens Diretas", + "Disable_Notifications": "Desactivar Notificações", + "Disable_two-factor_authentication": "Desactivar autenticação de dois passos", "Display_offline_form": "Exibir formulário quando offline", "Displays_action_text": "Exibe texto da ação", "Do_you_want_to_change_to_s_question": "Você quer mudar para %s?", "Domain": "Domínio", + "Domain_added": "Domínio Adicionado", + "Domain_removed": "Domínio Removido", "Domains": "Domínios", + "Download_Snippet": "Download", "Drop_to_upload_file": "Largue para enviar arquivos", "Dry_run": "Simulação", "Dry_run_description": "Enviará apenas um e-mail, para o mesmo endereço definido em 'De'. O e-mail deve pertencer a um usuário válido.", @@ -374,13 +406,17 @@ "Empty_title": "Título vazio", "Enable": "Habilitar", "Enable_Desktop_Notifications": "Habilitar Notificações Desktop", + "Enable_two-factor_authentication": "Activar autenticação de dois passos", "Enabled": "Ativado", + "Enable_Svg_Favicon": "Activar favicon SVG", "Encrypted_message": "Mensagem criptografada", "End_OTR": "Finalizar OTR", + "Enter_authentication_code": "Introduzir código de autenticação", "Enter_a_regex": "Introduza um regex", "Enter_a_room_name": "Digite um nome de sala", "Enter_a_username": "Nome de usuário", "Enter_name_here": "Insira o nome aqui", + "Enter_Normal": "Modo normal (enviar com Enter)", "Enter_to": "Enter para", "Error": "Erro", "error-action-not-allowed": "__action__ não é permitido", diff --git a/packages/rocketchat-i18n/i18n/ro.i18n.json b/packages/rocketchat-i18n/i18n/ro.i18n.json index 94f08574b0e6f9ddc176f2af419f3e4d8ff620b3..1b9eba35a7935251d1cf17b0158856c487a824e3 100644 --- a/packages/rocketchat-i18n/i18n/ro.i18n.json +++ b/packages/rocketchat-i18n/i18n/ro.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "Permite schimbarea profilului utilzatorilor", "Accounts_AvatarResize": "Redimensionarea Avatare", "Accounts_AvatarSize": "Dimensiune Avatar", - "Accounts_AvatarStorePath": "Calea de stocare Avatar", - "Accounts_AvatarStoreType": "Tip stocare Avatar", "Accounts_BlockedDomainsList": "Blocate List Domenii", "Accounts_BlockedDomainsList_Description": "Lista de elemente separate prin virgulă de domenii blocate", "Accounts_BlockedUsernameList": "Utilizator blocat Lista", diff --git a/packages/rocketchat-i18n/i18n/ru.i18n.json b/packages/rocketchat-i18n/i18n/ru.i18n.json index 37ecf89f65ff3ec8068f72a66cc4ddef079f1a0f..57b3f42652df40f19c1f43b64a31213d2d2807c1 100644 --- a/packages/rocketchat-i18n/i18n/ru.i18n.json +++ b/packages/rocketchat-i18n/i18n/ru.i18n.json @@ -16,6 +16,8 @@ "Accessing_permissions": "Права доступа", "Account_SID": "SID учетной записи", "Accounts": "Учётные записи", + "Accounts_AllowAnonymousRead": "Разрешить чтение анонимным пользователям", + "Accounts_AllowAnonymousWrite": "Разрешить писать анонимным пользователям", "Accounts_AllowDeleteOwnAccount": "Разрешить пользователям удалять собственный аккаунт", "Accounts_AllowedDomainsList": "Список разрешенных доменов", "Accounts_AllowedDomainsList_Description": "Список разрешенных доменов, разделенный запятыми ", @@ -26,8 +28,6 @@ "Accounts_AllowUserProfileChange": "Разрешить пользователю изменять настройки профиля", "Accounts_AvatarResize": "Изменение размера аватара", "Accounts_AvatarSize": "Размер аватара", - "Accounts_AvatarStorePath": "Путь к хранилищу аватаров", - "Accounts_AvatarStoreType": "Тип хранилища аватаров", "Accounts_BlockedDomainsList": "Список запрещённых доменов", "Accounts_BlockedDomainsList_Description": "Список запрещённых доменов, разделенных запятой", "Accounts_BlockedUsernameList": "Список заблокированных пользователей", @@ -57,7 +57,7 @@ "Accounts_OAuth_Custom_Token_Path": "Token Path", "Accounts_OAuth_Custom_Token_Sent_Via": "Токен посланными через", "Accounts_OAuth_Facebook": "Facebook логин", - "Accounts_OAuth_Facebook_callback_url": "Обратный URL-адрес Facebook", + "Accounts_OAuth_Facebook_callback_url": "Обратный URL-адрес Facebook", "Accounts_OAuth_Facebook_id": "Facebook App Id", "Accounts_OAuth_Facebook_secret": "Facebook Secret", "Accounts_OAuth_Github": "OAuth разрешён", @@ -98,9 +98,9 @@ "Accounts_RegistrationForm_Disabled": "Отключена", "Accounts_RegistrationForm_LinkReplacementText": "Текст замены ссылки регистрационной формы", "Accounts_RegistrationForm_Public": "Доступна публично", - "Accounts_RegistrationForm_Secret_URL": "Тайный URL-адрес", - "Accounts_RegistrationForm_SecretURL": "Тайный URL-адрес регистрационной формы", - "Accounts_RegistrationForm_SecretURL_Description": "Вы должны предоставить случайную строку, которая будет добавлена к вашему регистрационному URL-адресу. Например: https://demo.rocket.chat/register/[secret_hash]", + "Accounts_RegistrationForm_Secret_URL": "Секретный URL-адрес", + "Accounts_RegistrationForm_SecretURL": "Секретный URL-адрес регистрационной формы", + "Accounts_RegistrationForm_SecretURL_Description": "Вы должны предоставить случайную строку, которая будет добавлена к вашему регистрационному URL-адресу. Например: https://demo.rocket.chat/register/[secret_hash]", "Accounts_RequireNameForSignUp": "Требуется имя для регистрации", "Accounts_ShowFormLogin": "Показать логин на основе формы", "Accounts_UseDefaultBlockedDomainsList": "Использовать список запрещённых доменов по умолчанию", @@ -936,6 +936,7 @@ "Search_Private_Groups": "Поиск приватных чатов", "seconds": "секунд(ы)", "Secret_token": "Секретный маркер", + "Security": "Безопасность", "Select_a_department": "Выберите раздел", "Select_an_avatar": "Выберите аватар", "Select_file": "Выберите файл", diff --git a/packages/rocketchat-i18n/i18n/sq.i18n.json b/packages/rocketchat-i18n/i18n/sq.i18n.json index 2789b1173d7f2293b71cfb758cf3bacaa4acc3d2..0da84a902bdd4eb0a4e2e9f80dfa7c7a49bd9e10 100644 --- a/packages/rocketchat-i18n/i18n/sq.i18n.json +++ b/packages/rocketchat-i18n/i18n/sq.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "Lejo përdoruesit Profilin Change", "Accounts_AvatarResize": "Ndrysho madhësin e Avatarit", "Accounts_AvatarSize": "Madhësia e Avatarit", - "Accounts_AvatarStorePath": "Avatari Storage Path", - "Accounts_AvatarStoreType": "Avatari Storage Lloji", "Accounts_BlockedDomainsList": "Blocked Lista Domains", "Accounts_BlockedDomainsList_Description": "lista të ndara me presje të fushave të bllokuara", "Accounts_BlockedUsernameList": "Blocked List Emri i përdoruesit", diff --git a/packages/rocketchat-i18n/i18n/sr.i18n.json b/packages/rocketchat-i18n/i18n/sr.i18n.json index 86b6a9570fcb433f27aae3558d0b21b705bfc065..7b658902e6de38df2ed525cbbd7bb523eee0bb07 100644 --- a/packages/rocketchat-i18n/i18n/sr.i18n.json +++ b/packages/rocketchat-i18n/i18n/sr.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "Дозволите Усер Профиле Цханге", "Accounts_AvatarResize": "ресизе Аватари", "Accounts_AvatarSize": "аватар величина", - "Accounts_AvatarStorePath": "Аватар складиштења Пут", - "Accounts_AvatarStoreType": "Аватар Стораге Типе:", "Accounts_BlockedDomainsList": "Блокиран доменов", "Accounts_BlockedDomainsList_Description": "Зарезом одвојена листа блокираних домена", "Accounts_BlockedUsernameList": "Блокиран име Списак", diff --git a/packages/rocketchat-i18n/i18n/sv.i18n.json b/packages/rocketchat-i18n/i18n/sv.i18n.json index b677c0687450cad1c82971067170e86e4d5e398c..90899745d5b98d4084d71382682de3a2398c8d1b 100644 --- a/packages/rocketchat-i18n/i18n/sv.i18n.json +++ b/packages/rocketchat-i18n/i18n/sv.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "Tillåt byte av användarprofil", "Accounts_AvatarResize": "Ändra storlek på avatarer", "Accounts_AvatarSize": "Storlek på avatar", - "Accounts_AvatarStorePath": "Sökväg till avatarförråd", - "Accounts_AvatarStoreType": "Typ av avatarförråd", "Accounts_BlockedDomainsList": "Lista över blockerade domäner", "Accounts_BlockedDomainsList_Description": "Kommaseparerad lista över blockerade domäner", "Accounts_BlockedUsernameList": "Användarlista över blockerade", diff --git a/packages/rocketchat-i18n/i18n/ta-IN.i18n.json b/packages/rocketchat-i18n/i18n/ta-IN.i18n.json index 58ea59cd2b62ad74ed4278c0b26ac508d1a0c786..9d857fdf3254a5b7050da38c7d286a49be29616d 100644 --- a/packages/rocketchat-i18n/i18n/ta-IN.i18n.json +++ b/packages/rocketchat-i18n/i18n/ta-IN.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "பயனர் விவரம் மாற்றம் அனுமதி", "Accounts_AvatarResize": "அவதாரங்களை அளவை", "Accounts_AvatarSize": "avatar அளவு", - "Accounts_AvatarStorePath": "அவதார் சேமிப்பு பாதை", - "Accounts_AvatarStoreType": "அவதார் சேமிப்பு வகை", "Accounts_BlockedDomainsList": "தடுக்கப்பட்ட களங்கள் பட்டியல்", "Accounts_BlockedDomainsList_Description": "தடுக்கப்பட்டது களங்களின் கமாவால் பிரிக்கப்பட்ட பட்டியல்", "Accounts_BlockedUsernameList": "தடுக்கப்பட்ட பயனர் பெயர் பட்டியல்", diff --git a/packages/rocketchat-i18n/i18n/tr.i18n.json b/packages/rocketchat-i18n/i18n/tr.i18n.json index 1f577efdd77da3fceac876fe54e0e9a45126dec0..c70b29e1ba59575d740a08f5fd85bec42286cbe4 100644 --- a/packages/rocketchat-i18n/i18n/tr.i18n.json +++ b/packages/rocketchat-i18n/i18n/tr.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "Kullanıcının Profilini Değiştirmesine izin ver", "Accounts_AvatarResize": "Profil Resimlerini yeniden boyutlandır", "Accounts_AvatarSize": "Profil Resmi Boyutu", - "Accounts_AvatarStorePath": "Profil Resmi Depo Yolu", - "Accounts_AvatarStoreType": "Profil Resmi Depolama Türü", "Accounts_BlockedDomainsList": "Engellenen Alanlar Listesi", "Accounts_BlockedDomainsList_Description": "Engellenen alanların virgülle ayrılmış listesi", "Accounts_BlockedUsernameList": "Engellenen Kullanıcı Adı Listesi", diff --git a/packages/rocketchat-i18n/i18n/ug.i18n.json b/packages/rocketchat-i18n/i18n/ug.i18n.json index d3a1dc7c7cfadcc4cc4fd52166b46a0a840d5c8f..ba53f4000c386c4cf0aee1cb4454e886d9bca4a6 100644 --- a/packages/rocketchat-i18n/i18n/ug.i18n.json +++ b/packages/rocketchat-i18n/i18n/ug.i18n.json @@ -21,8 +21,6 @@ "Accounts_AllowUserProfileChange": "ئاكونت ئەزانىڭ ماتېرىيالىنى ئۆزگەرتىشكە رۇخسەت قىلدى", "Accounts_AvatarResize": "ئاكونت باش سۈرئەتنىڭ چوڭ كىچىكلىكىنى تەڭشەش", "Accounts_AvatarSize": "ئاكونت باش سۈرئەتنىڭ چوڭ كىچىكلىكى", - "Accounts_AvatarStorePath": "ئاكونت باش سۈرئەت ساقلاش ئادرېسى", - "Accounts_AvatarStoreType": "ئاكونت باش سۈرئەت ساقلاش تىپى", "Accounts_BlockedDomainsList": "ئاكونت تور بەت نامى تىزىملىكىنى توسۇۋېتىلدى", "Accounts_BlockedDomainsList_Description": "چىكىتلىك پەش ئارقىلىق توسۇۋېتىلگەن توربەت نامى تىزىملىكى", "Accounts_BlockedUsernameList": "ئاكونت مەنئىي قىلغان ئەزا تىزىملىكى", diff --git a/packages/rocketchat-i18n/i18n/uk.i18n.json b/packages/rocketchat-i18n/i18n/uk.i18n.json index ff1c6365af7e87fe6049d91e72c879147cb21a98..9cdb267deb27c9086945bad5614b3de14f67c92d 100644 --- a/packages/rocketchat-i18n/i18n/uk.i18n.json +++ b/packages/rocketchat-i18n/i18n/uk.i18n.json @@ -25,8 +25,6 @@ "Accounts_AllowUserProfileChange": "Дозволити користувачеві змінювати налаштування профілю", "Accounts_AvatarResize": "Змінити розмір аватару", "Accounts_AvatarSize": "Розмір аватару", - "Accounts_AvatarStorePath": "Шлях до сховища з аватарами", - "Accounts_AvatarStoreType": "Тип сховища с аватарами", "Accounts_BlockedDomainsList": "Перелік заблокованих доменів", "Accounts_BlockedDomainsList_Description": "Перелік заблокованих доменів, розділених комою", "Accounts_BlockedUsernameList": "Перелік заблокованих користувачів", diff --git a/packages/rocketchat-i18n/i18n/zh-HK.i18n.json b/packages/rocketchat-i18n/i18n/zh-HK.i18n.json index 549746827755b67aaf77796ab862a61a678b2dae..e3365a6452c6093991fd7ca6bba328c3e951e0da 100644 --- a/packages/rocketchat-i18n/i18n/zh-HK.i18n.json +++ b/packages/rocketchat-i18n/i18n/zh-HK.i18n.json @@ -20,7 +20,6 @@ "Accounts_AllowUserProfileChange": "允許使用者變更個人檔案", "Accounts_AvatarResize": "調整頭像大\b小", "Accounts_AvatarSize": "頭像大\b小", - "Accounts_AvatarStorePath": "頭像儲存路徑", "Accounts_BlockedDomainsList": "黑名單網域列表", "Accounts_denyUnverifiedEmail": "拒绝未经验证的电子邮件", "Accounts_EmailVerification": "邮件验证", diff --git a/packages/rocketchat-i18n/i18n/zh-TW.i18n.json b/packages/rocketchat-i18n/i18n/zh-TW.i18n.json index d06f9788e46eff704e20f3df3c045f62904ba618..53c8db1dc5458c668b2f604565b43439cd90133e 100644 --- a/packages/rocketchat-i18n/i18n/zh-TW.i18n.json +++ b/packages/rocketchat-i18n/i18n/zh-TW.i18n.json @@ -25,8 +25,6 @@ "Accounts_AllowUserProfileChange": "允許變更使用者個人檔案", "Accounts_AvatarResize": "調整大頭貼尺寸", "Accounts_AvatarSize": "大頭貼尺寸", - "Accounts_AvatarStorePath": "大頭貼儲存路徑", - "Accounts_AvatarStoreType": "大頭貼儲存類型", "Accounts_BlockedDomainsList": "黑名單網域列表", "Accounts_BlockedDomainsList_Description": "以逗號分隔的網域黑名單", "Accounts_BlockedUsernameList": "使用者黑名單", diff --git a/packages/rocketchat-i18n/i18n/zh.i18n.json b/packages/rocketchat-i18n/i18n/zh.i18n.json index 00b01cda799a2d11ed22421715aaecef70efc298..4f1c6f29eb570892821f64800dabc2e4bd87368f 100644 --- a/packages/rocketchat-i18n/i18n/zh.i18n.json +++ b/packages/rocketchat-i18n/i18n/zh.i18n.json @@ -27,8 +27,6 @@ "Accounts_AllowUserProfileChange": "允许修改个人资料", "Accounts_AvatarResize": "调整头像大小", "Accounts_AvatarSize": "头像大小", - "Accounts_AvatarStorePath": "头像存储路径", - "Accounts_AvatarStoreType": "头像存储类型", "Accounts_BlockedDomainsList": "已屏蔽域名列表", "Accounts_BlockedDomainsList_Description": "以逗号分隔的屏蔽的域名列表", "Accounts_BlockedUsernameList": "已屏蔽的用户名列表", diff --git a/packages/rocketchat-importer-hipchat/main.coffee b/packages/rocketchat-importer-hipchat/main.coffee deleted file mode 100644 index 1198383d75c6a801856f1fd8f6b44f93e3731d0e..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer-hipchat/main.coffee +++ /dev/null @@ -1,3 +0,0 @@ -Importer.addImporter 'hipchat', Importer.HipChat, - name: 'HipChat' - mimeType: 'application/zip' diff --git a/packages/rocketchat-importer-hipchat/main.js b/packages/rocketchat-importer-hipchat/main.js new file mode 100644 index 0000000000000000000000000000000000000000..895b7b875965d16df96c640cd483913fd0a057f6 --- /dev/null +++ b/packages/rocketchat-importer-hipchat/main.js @@ -0,0 +1,6 @@ +/* globals Importer */ + +Importer.addImporter('hipchat', Importer.HipChat, { + name: 'HipChat', + mimeType: 'application/zip' +}); diff --git a/packages/rocketchat-importer-hipchat/package.js b/packages/rocketchat-importer-hipchat/package.js index 7461d067475c54ab884207655389e22b5047f769..499d337065e40169747ad2df13c6e52943831bac 100644 --- a/packages/rocketchat-importer-hipchat/package.js +++ b/packages/rocketchat-importer-hipchat/package.js @@ -8,11 +8,10 @@ Package.describe({ Package.onUse(function(api) { api.use([ 'ecmascript', - 'coffeescript', 'rocketchat:lib', 'rocketchat:importer' ]); api.use('rocketchat:logger', 'server'); - api.addFiles('server.coffee', 'server'); - api.addFiles('main.coffee', ['client', 'server']); + api.addFiles('server.js', 'server'); + api.addFiles('main.js', ['client', 'server']); }); diff --git a/packages/rocketchat-importer-hipchat/server.coffee b/packages/rocketchat-importer-hipchat/server.coffee deleted file mode 100644 index 19a4cbced95162d8df1807e8975416357240af9d..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer-hipchat/server.coffee +++ /dev/null @@ -1,241 +0,0 @@ -import moment from 'moment' -import 'moment-timezone' - -Importer.HipChat = class Importer.HipChat extends Importer.Base - @RoomPrefix = 'hipchat_export/rooms/' - @UsersPrefix = 'hipchat_export/users/' - - constructor: (name, descriptionI18N, mimeType) -> - super(name, descriptionI18N, mimeType) - @logger.debug('Constructed a new Slack Importer.') - @userTags = [] - - prepare: (dataURI, sentContentType, fileName) => - super(dataURI, sentContentType, fileName) - - {image, contentType} = RocketChatFile.dataURIParse dataURI - zip = new @AdmZip(new Buffer(image, 'base64')) - zipEntries = zip.getEntries() - - tempRooms = [] - tempUsers = [] - tempMessages = {} - for entry in zipEntries - do (entry) => - if entry.entryName.indexOf('__MACOSX') > -1 - #ignore all of the files inside of __MACOSX - @logger.debug("Ignoring the file: #{entry.entryName}") - if not entry.isDirectory - if entry.entryName.indexOf(Importer.HipChat.RoomPrefix) > -1 - roomName = entry.entryName.split(Importer.HipChat.RoomPrefix)[1] - if roomName is 'list.json' - @updateProgress Importer.ProgressStep.PREPARING_CHANNELS - tempRooms = JSON.parse(entry.getData().toString()).rooms - for room in tempRooms - room.name = _.slugify room.name - else if roomName.indexOf('/') > -1 - item = roomName.split('/') - roomName = _.slugify item[0] #random - msgGroupData = item[1].split('.')[0] #2015-10-04 - if not tempMessages[roomName] - tempMessages[roomName] = {} - # For some reason some of the json files in the HipChat export aren't valid JSON - # files, so we need to catch those and just ignore them (sadly). - try - tempMessages[roomName][msgGroupData] = JSON.parse entry.getData().toString() - catch - @logger.warn "#{entry.entryName} is not a valid JSON file! Unable to import it." - else if entry.entryName.indexOf(Importer.HipChat.UsersPrefix) > -1 - usersName = entry.entryName.split(Importer.HipChat.UsersPrefix)[1] - if usersName is 'list.json' - @updateProgress Importer.ProgressStep.PREPARING_USERS - tempUsers = JSON.parse(entry.getData().toString()).users - else - @logger.warn "Unexpected file in the #{@name} import: #{entry.entryName}" - - # Insert the users record, eventually this might have to be split into several ones as well - # if someone tries to import a several thousands users instance - usersId = @collection.insert { 'import': @importRecord._id, 'importer': @name, 'type': 'users', 'users': tempUsers } - @users = @collection.findOne usersId - @updateRecord { 'count.users': tempUsers.length } - @addCountToTotal tempUsers.length - - # Insert the rooms records. - channelsId = @collection.insert { 'import': @importRecord._id, 'importer': @name, 'type': 'channels', 'channels': tempRooms } - @channels = @collection.findOne channelsId - @updateRecord { 'count.channels': tempRooms.length } - @addCountToTotal tempRooms.length - - # Insert the messages records - @updateProgress Importer.ProgressStep.PREPARING_MESSAGES - messagesCount = 0 - for channel, messagesObj of tempMessages - do (channel, messagesObj) => - if not @messages[channel] - @messages[channel] = {} - for date, msgs of messagesObj - messagesCount += msgs.length - @updateRecord { 'messagesstatus': "#{channel}/#{date}" } - - if Importer.Base.getBSONSize(msgs) > Importer.Base.MaxBSONSize - for splitMsg, i in Importer.Base.getBSONSafeArraysFromAnArray(msgs) - messagesId = @collection.insert { 'import': @importRecord._id, 'importer': @name, 'type': 'messages', 'name': "#{channel}/#{date}.#{i}", 'messages': splitMsg } - @messages[channel]["#{date}.#{i}"] = @collection.findOne messagesId - else - messagesId = @collection.insert { 'import': @importRecord._id, 'importer': @name, 'type': 'messages', 'name': "#{channel}/#{date}", 'messages': msgs } - @messages[channel][date] = @collection.findOne messagesId - - @updateRecord { 'count.messages': messagesCount, 'messagesstatus': null } - @addCountToTotal messagesCount - - if tempUsers.length is 0 or tempRooms.length is 0 or messagesCount is 0 - @logger.warn "The loaded users count #{tempUsers.length}, the loaded channels #{tempRooms.length}, and the loaded messages #{messagesCount}" - @updateProgress Importer.ProgressStep.ERROR - return @getProgress() - - selectionUsers = tempUsers.map (user) -> - #HipChat's export doesn't contain bot users, from the data I've seen - return new Importer.SelectionUser user.user_id, user.name, user.email, user.is_deleted, false, !user.is_bot - selectionChannels = tempRooms.map (room) -> - return new Importer.SelectionChannel room.room_id, room.name, room.is_archived, true, false - - @updateProgress Importer.ProgressStep.USER_SELECTION - return new Importer.Selection @name, selectionUsers, selectionChannels - - startImport: (importSelection) => - super(importSelection) - start = Date.now() - - for user in importSelection.users - for u in @users.users when u.user_id is user.user_id - u.do_import = user.do_import - @collection.update { _id: @users._id }, { $set: { 'users': @users.users }} - - for channel in importSelection.channels - for c in @channels.channels when c.room_id is channel.channel_id - c.do_import = channel.do_import - @collection.update { _id: @channels._id }, { $set: { 'channels': @channels.channels }} - - startedByUserId = Meteor.userId() - Meteor.defer => - @updateProgress Importer.ProgressStep.IMPORTING_USERS - for user in @users.users when user.do_import - do (user) => - Meteor.runAsUser startedByUserId, () => - existantUser = RocketChat.models.Users.findOneByEmailAddress user.email - if existantUser - user.rocketId = existantUser._id - @userTags.push - hipchat: "@#{user.mention_name}" - rocket: "@#{existantUser.username}" - else - userId = Accounts.createUser { email: user.email, password: Date.now() + user.name + user.email.toUpperCase() } - user.rocketId = userId - @userTags.push - hipchat: "@#{user.mention_name}" - rocket: "@#{user.mention_name}" - Meteor.runAsUser userId, () => - Meteor.call 'setUsername', user.mention_name, {joinDefaultChannelsSilenced: true} - Meteor.call 'setAvatarFromService', user.photo_url, undefined, 'url' - Meteor.call 'userSetUtcOffset', parseInt moment().tz(user.timezone).format('Z').toString().split(':')[0] - - if user.name? - RocketChat.models.Users.setName userId, user.name - - #Deleted users are 'inactive' users in Rocket.Chat - if user.is_deleted - Meteor.call 'setUserActiveStatus', userId, false - @addCountCompleted 1 - @collection.update { _id: @users._id }, { $set: { 'users': @users.users }} - - @updateProgress Importer.ProgressStep.IMPORTING_CHANNELS - for channel in @channels.channels when channel.do_import - do (channel) => - Meteor.runAsUser startedByUserId, () => - channel.name = channel.name.replace(/ /g, '') - existantRoom = RocketChat.models.Rooms.findOneByName channel.name - if existantRoom - channel.rocketId = existantRoom._id - else - userId = '' - for user in @users.users when user.user_id is channel.owner_user_id - userId = user.rocketId - - if userId is '' - @logger.warn "Failed to find the channel creator for #{channel.name}, setting it to the current running user." - userId = startedByUserId - - Meteor.runAsUser userId, () => - returned = Meteor.call 'createChannel', channel.name, [] - channel.rocketId = returned.rid - RocketChat.models.Rooms.update { _id: channel.rocketId }, { $set: { 'ts': new Date(channel.created * 1000) }} - @addCountCompleted 1 - @collection.update { _id: @channels._id }, { $set: { 'channels': @channels.channels }} - - @updateProgress Importer.ProgressStep.IMPORTING_MESSAGES - nousers = {}; - for channel, messagesObj of @messages - do (channel, messagesObj) => - Meteor.runAsUser startedByUserId, () => - hipchatChannel = @getHipChatChannelFromName channel - if hipchatChannel?.do_import - room = RocketChat.models.Rooms.findOneById hipchatChannel.rocketId, { fields: { usernames: 1, t: 1, name: 1 } } - for date, msgs of messagesObj - @updateRecord { 'messagesstatus': "#{channel}/#{date}.#{msgs.messages.length}" } - for message in msgs.messages - if message.from? - user = @getRocketUser(message.from.user_id) - if user? - msgObj = - msg: @convertHipChatMessageToRocketChat(message.message) - ts: new Date(message.date) - u: - _id: user._id - username: user.username - - RocketChat.sendMessage user, msgObj, room, true - else - if not nousers[message.from.user_id] - nousers[message.from.user_id] = message.from - else - if not _.isArray message - console.warn 'Please report the following:', message - @addCountCompleted 1 - @logger.warn 'The following did not have users:', nousers - - @updateProgress Importer.ProgressStep.FINISHING - for channel in @channels.channels when channel.do_import and channel.is_archived - do (channel) => - Meteor.runAsUser startedByUserId, () => - Meteor.call 'archiveRoom', channel.rocketId - - @updateProgress Importer.ProgressStep.DONE - timeTook = Date.now() - start - @logger.log "Import took #{timeTook} milliseconds." - - return @getProgress() - - getHipChatChannelFromName: (channelName) => - for channel in @channels.channels when channel.name is channelName - return channel - - getRocketUser: (hipchatId) => - for user in @users.users when user.user_id is hipchatId - return RocketChat.models.Users.findOneById user.rocketId, { fields: { username: 1, name: 1 }} - - convertHipChatMessageToRocketChat: (message) => - if message? - for userReplace in @userTags - message = message.replace userReplace.hipchat, userReplace.rocket - else - message = '' - return message - - getSelection: () => - selectionUsers = @users.users.map (user) -> - #HipChat's export doesn't contain bot users, from the data I've seen - return new Importer.SelectionUser user.user_id, user.name, user.email, user.is_deleted, false, !user.is_bot - selectionChannels = @channels.channels.map (room) -> - return new Importer.SelectionChannel room.room_id, room.name, room.is_archived, true, false - - return new Importer.Selection @name, selectionUsers, selectionChannels diff --git a/packages/rocketchat-importer-hipchat/server.js b/packages/rocketchat-importer-hipchat/server.js new file mode 100644 index 0000000000000000000000000000000000000000..89713ce84c11945e5c8d5fa1adfedc07a7ef94e7 --- /dev/null +++ b/packages/rocketchat-importer-hipchat/server.js @@ -0,0 +1,345 @@ +/* globals Importer */ +import moment from 'moment'; + +import 'moment-timezone'; + +Importer.HipChat = Importer.HipChat = (function() { + class HipChat extends Importer.Base { + constructor(name, descriptionI18N, mimeType) { + super(name, descriptionI18N, mimeType); + this.logger.debug('Constructed a new Slack Importer.'); + this.userTags = []; + } + + prepare(dataURI, sentContentType, fileName) { + super.prepare(dataURI, sentContentType, fileName); + const image = RocketChatFile.dataURIParse(dataURI).image; + // const contentType = ref.contentType; + const zip = new this.AdmZip(new Buffer(image, 'base64')); + const zipEntries = zip.getEntries(); + const tempRooms = []; + let tempUsers = []; + const tempMessages = {}; + + zipEntries.forEach(entry => { + if (entry.entryName.indexOf('__MACOSX') > -1) { + this.logger.debug(`Ignoring the file: ${ entry.entryName }`); + } + if (entry.isDirectory) { + return; + } + if (entry.entryName.indexOf(Importer.HipChat.RoomPrefix) > -1) { + let roomName = entry.entryName.split(Importer.HipChat.RoomPrefix)[1]; + if (roomName === 'list.json') { + this.updateProgress(Importer.ProgressStep.PREPARING_CHANNELS); + const tempRooms = JSON.parse(entry.getData().toString()).rooms; + tempRooms.forEach(room => { + room.name = _.slugify(room.name); + }); + } else if (roomName.indexOf('/') > -1) { + const item = roomName.split('/'); + roomName = _.slugify(item[0]); + const msgGroupData = item[1].split('.')[0]; + if (!tempMessages[roomName]) { + tempMessages[roomName] = {}; + } + try { + return tempMessages[roomName][msgGroupData] = JSON.parse(entry.getData().toString()); + } catch (error) { + return this.logger.warn(`${ entry.entryName } is not a valid JSON file! Unable to import it.`); + } + } + } else if (entry.entryName.indexOf(Importer.HipChat.UsersPrefix) > -1) { + const usersName = entry.entryName.split(Importer.HipChat.UsersPrefix)[1]; + if (usersName === 'list.json') { + this.updateProgress(Importer.ProgressStep.PREPARING_USERS); + return tempUsers = JSON.parse(entry.getData().toString()).users; + } else { + return this.logger.warn(`Unexpected file in the ${ this.name } import: ${ entry.entryName }`); + } + } + }); + const usersId = this.collection.insert({ + 'import': this.importRecord._id, + 'importer': this.name, + 'type': 'users', + 'users': tempUsers + }); + this.users = this.collection.findOne(usersId); + this.updateRecord({ + 'count.users': tempUsers.length + }); + this.addCountToTotal(tempUsers.length); + const channelsId = this.collection.insert({ + 'import': this.importRecord._id, + 'importer': this.name, + 'type': 'channels', + 'channels': tempRooms + }); + this.channels = this.collection.findOne(channelsId); + this.updateRecord({ + 'count.channels': tempRooms.length + }); + this.addCountToTotal(tempRooms.length); + this.updateProgress(Importer.ProgressStep.PREPARING_MESSAGES); + let messagesCount = 0; + Object.keys(tempMessages).forEach(channel => { + const messagesObj = tempMessages[channel]; + this.messages[channel] = this.messages[channel] || {}; + Object.keys(messagesObj).forEach(date => { + const msgs = messagesObj[date]; + messagesCount += msgs.length; + this.updateRecord({ + 'messagesstatus': `${ channel }/${ date }` + }); + if (Importer.Base.getBSONSize(msgs) > Importer.Base.MaxBSONSize) { + Importer.Base.getBSONSafeArraysFromAnArray(msgs).forEach((splitMsg, i) => { + const messagesId = this.collection.insert({ + 'import': this.importRecord._id, + 'importer': this.name, + 'type': 'messages', + 'name': `${ channel }/${ date }.${ i }`, + 'messages': splitMsg + }); + this.messages[channel][`${ date }.${ i }`] = this.collection.findOne(messagesId); + }); + } else { + const messagesId = this.collection.insert({ + 'import': this.importRecord._id, + 'importer': this.name, + 'type': 'messages', + 'name': `${ channel }/${ date }`, + 'messages': msgs + }); + this.messages[channel][date] = this.collection.findOne(messagesId); + } + }); + }); + this.updateRecord({ + 'count.messages': messagesCount, + 'messagesstatus': null + }); + this.addCountToTotal(messagesCount); + if (tempUsers.length === 0 || tempRooms.length === 0 || messagesCount === 0) { + this.logger.warn(`The loaded users count ${ tempUsers.length }, the loaded channels ${ tempRooms.length }, and the loaded messages ${ messagesCount }`); + this.updateProgress(Importer.ProgressStep.ERROR); + return this.getProgress(); + } + const selectionUsers = tempUsers.map(function(user) { + return new Importer.SelectionUser(user.user_id, user.name, user.email, user.is_deleted, false, !user.is_bot); + }); + const selectionChannels = tempRooms.map(function(room) { + return new Importer.SelectionChannel(room.room_id, room.name, room.is_archived, true, false); + }); + this.updateProgress(Importer.ProgressStep.USER_SELECTION); + return new Importer.Selection(this.name, selectionUsers, selectionChannels); + } + + startImport(importSelection) { + super.startImport(importSelection); + const start = Date.now(); + importSelection.users.forEach(user => { + this.users.users.forEach(u => { + if (u.user_id === user.user_id) { + u.do_import = user.do_import; + } + }); + }); + this.collection.update({_id: this.users._id}, { $set: { 'users': this.users.users } }); + importSelection.channels.forEach(channel => + this.channels.channels.forEach(c => c.room_id === channel.channel_id && (c.do_import = channel.do_import)) + ); + this.collection.update({ _id: this.channels._id }, { $set: { 'channels': this.channels.channels }}); + const startedByUserId = Meteor.userId(); + Meteor.defer(() => { + this.updateProgress(Importer.ProgressStep.IMPORTING_USERS); + this.users.users.forEach(user => { + if (!user.do_import) { + return; + } + Meteor.runAsUser(startedByUserId, () => { + const existantUser = RocketChat.models.Users.findOneByEmailAddress(user.email); + if (existantUser) { + user.rocketId = existantUser._id; + this.userTags.push({ + hipchat: `@${ user.mention_name }`, + rocket: `@${ existantUser.username }` + }); + } else { + const userId = Accounts.createUser({ + email: user.email, + password: Date.now() + user.name + user.email.toUpperCase() + }); + user.rocketId = userId; + this.userTags.push({ + hipchat: `@${ user.mention_name }`, + rocket: `@${ user.mention_name }` + }); + Meteor.runAsUser(userId, () => { + Meteor.call('setUsername', user.mention_name, { + joinDefaultChannelsSilenced: true + }); + Meteor.call('setAvatarFromService', user.photo_url, undefined, 'url'); + return Meteor.call('userSetUtcOffset', parseInt(moment().tz(user.timezone).format('Z').toString().split(':')[0])); + }); + if (user.name != null) { + RocketChat.models.Users.setName(userId, user.name); + } + if (user.is_deleted) { + Meteor.call('setUserActiveStatus', userId, false); + } + } + return this.addCountCompleted(1); + }); + }); + this.collection.update({ _id: this.users._id }, { $set: { 'users': this.users.users }}); + this.updateProgress(Importer.ProgressStep.IMPORTING_CHANNELS); + this.channels.channels.forEach(channel => { + if (!channel.do_import) { + return; + } + Meteor.runAsUser(startedByUserId, () => { + channel.name = channel.name.replace(/ /g, ''); + const existantRoom = RocketChat.models.Rooms.findOneByName(channel.name); + if (existantRoom) { + channel.rocketId = existantRoom._id; + } else { + let userId = ''; + this.users.users.forEach(user => { + if (user.user_id === channel.owner_user_id) { + userId = user.rocketId; + } + }); + if (userId === '') { + this.logger.warn(`Failed to find the channel creator for ${ channel.name }, setting it to the current running user.`); + userId = startedByUserId; + } + Meteor.runAsUser(userId, () => { + const returned = Meteor.call('createChannel', channel.name, []); + return channel.rocketId = returned.rid; + }); + RocketChat.models.Rooms.update({ + _id: channel.rocketId + }, { + $set: { + 'ts': new Date(channel.created * 1000) + } + }); + } + return this.addCountCompleted(1); + }); + }); + this.collection.update({ + _id: this.channels._id + }, { + $set: { + 'channels': this.channels.channels + } + }); + this.updateProgress(Importer.ProgressStep.IMPORTING_MESSAGES); + const nousers = {}; + + Object.keys(this.messages).forEach(channel => { + const messagesObj = this.messages[channel]; + Meteor.runAsUser(startedByUserId, () => { + const hipchatChannel = this.getHipChatChannelFromName(channel); + if (hipchatChannel != null ? hipchatChannel.do_import : undefined) { + const room = RocketChat.models.Rooms.findOneById(hipchatChannel.rocketId, { + fields: { + usernames: 1, + t: 1, + name: 1 + } + }); + + Object.keys(messagesObj).forEach(date => { + const msgs = messagesObj[date]; + this.updateRecord({ + 'messagesstatus': `${ channel }/${ date }.${ msgs.messages.length }` + }); + + msgs.messages.forEach(message => { + if (message.from != null) { + const user = this.getRocketUser(message.from.user_id); + if (user != null) { + const msgObj = { + msg: this.convertHipChatMessageToRocketChat(message.message), + ts: new Date(message.date), + u: { + _id: user._id, + username: user.username + } + }; + RocketChat.sendMessage(user, msgObj, room, true); + } else if (!nousers[message.from.user_id]) { + nousers[message.from.user_id] = message.from; + } + } else if (!_.isArray(message)) { + console.warn('Please report the following:', message); + } + this.addCountCompleted(1); + }); + }); + } + }); + }); + this.logger.warn('The following did not have users:', nousers); + this.updateProgress(Importer.ProgressStep.FINISHING); + this.channels.channels.forEach(channel => { + if (channel.do_import && channel.is_archived) { + Meteor.runAsUser(startedByUserId, () => { + return Meteor.call('archiveRoom', channel.rocketId); + }); + } + }); + this.updateProgress(Importer.ProgressStep.DONE); + const timeTook = Date.now() - start; + return this.logger.log(`Import took ${ timeTook } milliseconds.`); + }); + return this.getProgress(); + } + + getHipChatChannelFromName(channelName) { + return this.channels.channels.find(channel => channel.name === channelName); + } + + getRocketUser(hipchatId) { + const user = this.users.users.find(user => user.user_id === hipchatId); + return user ? RocketChat.models.Users.findOneById(user.rocketId, { + fields: { + username: 1, + name: 1 + } + }) : undefined; + } + + convertHipChatMessageToRocketChat(message) { + if (message != null) { + this.userTags.forEach(userReplace => { + message = message.replace(userReplace.hipchat, userReplace.rocket); + }); + } else { + message = ''; + } + return message; + } + + getSelection() { + const selectionUsers = this.users.users.map(function(user) { + return new Importer.SelectionUser(user.user_id, user.name, user.email, user.is_deleted, false, !user.is_bot); + }); + const selectionChannels = this.channels.channels.map(function(room) { + return new Importer.SelectionChannel(room.room_id, room.name, room.is_archived, true, false); + }); + return new Importer.Selection(this.name, selectionUsers, selectionChannels); + } + + } + + HipChat.RoomPrefix = 'hipchat_export/rooms/'; + + HipChat.UsersPrefix = 'hipchat_export/users/'; + + return HipChat; + +}()); diff --git a/packages/rocketchat-importer-slack/main.coffee b/packages/rocketchat-importer-slack/main.coffee deleted file mode 100644 index da0518e538746aec7087d8489e95646a4ad79a8a..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer-slack/main.coffee +++ /dev/null @@ -1,3 +0,0 @@ -Importer.addImporter 'slack', Importer.Slack, - name: 'Slack' - mimeType: 'application/zip' diff --git a/packages/rocketchat-importer-slack/main.js b/packages/rocketchat-importer-slack/main.js new file mode 100644 index 0000000000000000000000000000000000000000..d7298a2700e171abca5073c04849820a2731345c --- /dev/null +++ b/packages/rocketchat-importer-slack/main.js @@ -0,0 +1,5 @@ +/* globals Importer */ +Importer.addImporter('slack', Importer.Slack, { + name: 'Slack', + mimeType: 'application/zip' +}); diff --git a/packages/rocketchat-importer-slack/package.js b/packages/rocketchat-importer-slack/package.js index e10bc401cca15d69565ad74e099498bc66ad307f..04eb0571ac4a183dd21f6cdbcef7e9be020caa1c 100644 --- a/packages/rocketchat-importer-slack/package.js +++ b/packages/rocketchat-importer-slack/package.js @@ -8,11 +8,10 @@ Package.describe({ Package.onUse(function(api) { api.use([ 'ecmascript', - 'coffeescript', 'rocketchat:lib', 'rocketchat:importer' ]); api.use('rocketchat:logger', 'server'); - api.addFiles('server.coffee', 'server'); - api.addFiles('main.coffee', ['client', 'server']); + api.addFiles('server.js', 'server'); + api.addFiles('main.js', ['client', 'server']); }); diff --git a/packages/rocketchat-importer-slack/server.coffee b/packages/rocketchat-importer-slack/server.coffee deleted file mode 100644 index bbfa2a0504748d762e3d59f9955806d320fc49ad..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer-slack/server.coffee +++ /dev/null @@ -1,373 +0,0 @@ -Importer.Slack = class Importer.Slack extends Importer.Base - constructor: (name, descriptionI18N, mimeType) -> - super(name, descriptionI18N, mimeType) - @userTags = [] - @bots = {} - @logger.debug('Constructed a new Slack Importer.') - - prepare: (dataURI, sentContentType, fileName) => - super(dataURI, sentContentType, fileName) - - {image, contentType} = RocketChatFile.dataURIParse dataURI - zip = new @AdmZip(new Buffer(image, 'base64')) - zipEntries = zip.getEntries() - - tempChannels = [] - tempUsers = [] - tempMessages = {} - for entry in zipEntries - do (entry) => - if entry.entryName.indexOf('__MACOSX') > -1 - #ignore all of the files inside of __MACOSX - @logger.debug("Ignoring the file: #{entry.entryName}") - else if entry.entryName == 'channels.json' - @updateProgress Importer.ProgressStep.PREPARING_CHANNELS - tempChannels = JSON.parse entry.getData().toString() - tempChannels = tempChannels.filter (channel) -> channel.creator? - else if entry.entryName == 'users.json' - @updateProgress Importer.ProgressStep.PREPARING_USERS - tempUsers = JSON.parse entry.getData().toString() - - for user in tempUsers when user.is_bot - @bots[user.profile.bot_id] = user - - else if not entry.isDirectory and entry.entryName.indexOf('/') > -1 - item = entry.entryName.split('/') #random/2015-10-04.json - channelName = item[0] #random - msgGroupData = item[1].split('.')[0] #2015-10-04 - if not tempMessages[channelName] - tempMessages[channelName] = {} - # Catch files which aren't valid JSON files, ignore them - try - tempMessages[channelName][msgGroupData] = JSON.parse entry.getData().toString() - catch - @logger.warn "#{entry.entryName} is not a valid JSON file! Unable to import it." - - # Insert the users record, eventually this might have to be split into several ones as well - # if someone tries to import a several thousands users instance - usersId = @collection.insert { 'import': @importRecord._id, 'importer': @name, 'type': 'users', 'users': tempUsers } - @users = @collection.findOne usersId - @updateRecord { 'count.users': tempUsers.length } - @addCountToTotal tempUsers.length - - # Insert the channels records. - channelsId = @collection.insert { 'import': @importRecord._id, 'importer': @name, 'type': 'channels', 'channels': tempChannels } - @channels = @collection.findOne channelsId - @updateRecord { 'count.channels': tempChannels.length } - @addCountToTotal tempChannels.length - - # Insert the messages records - @updateProgress Importer.ProgressStep.PREPARING_MESSAGES - messagesCount = 0 - for channel, messagesObj of tempMessages - do (channel, messagesObj) => - if not @messages[channel] - @messages[channel] = {} - for date, msgs of messagesObj - messagesCount += msgs.length - @updateRecord { 'messagesstatus': "#{channel}/#{date}" } - - if Importer.Base.getBSONSize(msgs) > Importer.Base.MaxBSONSize - for splitMsg, i in Importer.Base.getBSONSafeArraysFromAnArray(msgs) - messagesId = @collection.insert { 'import': @importRecord._id, 'importer': @name, 'type': 'messages', 'name': "#{channel}/#{date}.#{i}", 'messages': splitMsg } - @messages[channel]["#{date}.#{i}"] = @collection.findOne messagesId - else - messagesId = @collection.insert { 'import': @importRecord._id, 'importer': @name, 'type': 'messages', 'name': "#{channel}/#{date}", 'messages': msgs } - @messages[channel][date] = @collection.findOne messagesId - - @updateRecord { 'count.messages': messagesCount, 'messagesstatus': null } - @addCountToTotal messagesCount - - if tempUsers.length is 0 or tempChannels.length is 0 or messagesCount is 0 - @logger.warn "The loaded users count #{tempUsers.length}, the loaded channels #{tempChannels.length}, and the loaded messages #{messagesCount}" - @updateProgress Importer.ProgressStep.ERROR - return @getProgress() - - selectionUsers = tempUsers.map (user) -> - return new Importer.SelectionUser user.id, user.name, user.profile.email, user.deleted, user.is_bot, !user.is_bot - selectionChannels = tempChannels.map (channel) -> - return new Importer.SelectionChannel channel.id, channel.name, channel.is_archived, true, false - - @updateProgress Importer.ProgressStep.USER_SELECTION - return new Importer.Selection @name, selectionUsers, selectionChannels - - startImport: (importSelection) => - super(importSelection) - start = Date.now() - - for user in importSelection.users - for u in @users.users when u.id is user.user_id - u.do_import = user.do_import - @collection.update { _id: @users._id }, { $set: { 'users': @users.users }} - - for channel in importSelection.channels - for c in @channels.channels when c.id is channel.channel_id - c.do_import = channel.do_import - @collection.update { _id: @channels._id }, { $set: { 'channels': @channels.channels }} - - startedByUserId = Meteor.userId() - Meteor.defer => - @updateProgress Importer.ProgressStep.IMPORTING_USERS - for user in @users.users when user.do_import - do (user) => - Meteor.runAsUser startedByUserId, () => - existantUser = RocketChat.models.Users.findOneByEmailAddress user.profile.email - if not existantUser - existantUser = RocketChat.models.Users.findOneByUsername user.name - - if existantUser - user.rocketId = existantUser._id - RocketChat.models.Users.update { _id: user.rocketId }, { $addToSet: { importIds: user.id } } - @userTags.push - slack: "<@#{user.id}>" - slackLong: "<@#{user.id}|#{user.name}>" - rocket: "@#{existantUser.username}" - else - if user.profile.email - userId = Accounts.createUser { email: user.profile.email, password: Date.now() + user.name + user.profile.email.toUpperCase() } - else - userId = Accounts.createUser { username: user.name, password: Date.now() + user.name, joinDefaultChannelsSilenced: true } - Meteor.runAsUser userId, () => - Meteor.call 'setUsername', user.name, {joinDefaultChannelsSilenced: true} - url = null - if user.profile.image_original - url = user.profile.image_original - else if user.profile.image_512 - url = user.profile.image_512 - - try - Meteor.call 'setAvatarFromService', url, undefined, 'url' - catch error - this.logger.warn "Failed to set #{user.name}'s avatar from url #{url}" - - # Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600 - if user.tz_offset - Meteor.call 'userSetUtcOffset', user.tz_offset / 3600 - - RocketChat.models.Users.update { _id: userId }, { $addToSet: { importIds: user.id } } - - if user.profile.real_name - RocketChat.models.Users.setName userId, user.profile.real_name - #Deleted users are 'inactive' users in Rocket.Chat - if user.deleted - Meteor.call 'setUserActiveStatus', userId, false - #TODO: Maybe send emails? - user.rocketId = userId - @userTags.push - slack: "<@#{user.id}>" - slackLong: "<@#{user.id}|#{user.name}>" - rocket: "@#{user.name}" - @addCountCompleted 1 - @collection.update { _id: @users._id }, { $set: { 'users': @users.users }} - - @updateProgress Importer.ProgressStep.IMPORTING_CHANNELS - for channel in @channels.channels when channel.do_import - do (channel) => - Meteor.runAsUser startedByUserId, () => - existantRoom = RocketChat.models.Rooms.findOneByName channel.name - if existantRoom or channel.is_general - if channel.is_general and channel.name isnt existantRoom?.name - Meteor.call 'saveRoomSettings', 'GENERAL', 'roomName', channel.name - channel.rocketId = if channel.is_general then 'GENERAL' else existantRoom._id - RocketChat.models.Rooms.update { _id: channel.rocketId }, { $addToSet: { importIds: channel.id } } - else - users = [] - for member in channel.members when member isnt channel.creator - user = @getRocketUser member - if user? - users.push user.username - - userId = startedByUserId - for user in @users.users when user.id is channel.creator and user.do_import - userId = user.rocketId - - Meteor.runAsUser userId, () => - returned = Meteor.call 'createChannel', channel.name, users - channel.rocketId = returned.rid - - # @TODO implement model specific function - roomUpdate = - ts: new Date(channel.created * 1000) - - if not _.isEmpty channel.topic?.value - roomUpdate.topic = channel.topic.value - - if not _.isEmpty(channel.purpose?.value) - roomUpdate.description = channel.purpose.value - - RocketChat.models.Rooms.update { _id: channel.rocketId }, { $set: roomUpdate, $addToSet: { importIds: channel.id } } - - @addCountCompleted 1 - @collection.update { _id: @channels._id }, { $set: { 'channels': @channels.channels }} - - missedTypes = {} - ignoreTypes = { 'bot_add': true, 'file_comment': true, 'file_mention': true } - @updateProgress Importer.ProgressStep.IMPORTING_MESSAGES - for channel, messagesObj of @messages - do (channel, messagesObj) => - Meteor.runAsUser startedByUserId, () => - slackChannel = @getSlackChannelFromName channel - if slackChannel?.do_import - room = RocketChat.models.Rooms.findOneById slackChannel.rocketId, { fields: { usernames: 1, t: 1, name: 1 } } - for date, msgs of messagesObj - @updateRecord { 'messagesstatus': "#{channel}/#{date}.#{msgs.messages.length}" } - for message in msgs.messages - msgDataDefaults = - _id: "slack-#{slackChannel.id}-#{message.ts.replace(/\./g, '-')}" - ts: new Date(parseInt(message.ts.split('.')[0]) * 1000) - - if message.type is 'message' - if message.subtype? - if message.subtype is 'channel_join' - if @getRocketUser(message.user)? - RocketChat.models.Messages.createUserJoinWithRoomIdAndUser room._id, @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'channel_leave' - if @getRocketUser(message.user)? - RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser room._id, @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'me_message' - msgObj = - msg: "_#{@convertSlackMessageToRocketChat(message.text)}_" - _.extend msgObj, msgDataDefaults - RocketChat.sendMessage @getRocketUser(message.user), msgObj, room, true - else if message.subtype is 'bot_message' or message.subtype is 'slackbot_response' - botUser = RocketChat.models.Users.findOneById 'rocket.cat', { fields: { username: 1 }} - botUsername = if @bots[message.bot_id] then @bots[message.bot_id]?.name else message.username - msgObj = - msg: @convertSlackMessageToRocketChat(message.text) - rid: room._id - bot: true - attachments: message.attachments - username: if botUsername then botUsername else undefined - - _.extend msgObj, msgDataDefaults - - if message.edited? - msgObj.editedAt = new Date(parseInt(message.edited.ts.split('.')[0]) * 1000) - editedBy = @getRocketUser(message.edited.user) - if editedBy? - msgObj.editedBy = - _id: editedBy._id - username: editedBy.username - - if message.icons? - msgObj.emoji = message.icons.emoji - - RocketChat.sendMessage botUser, msgObj, room, true - else if message.subtype is 'channel_purpose' - if @getRocketUser(message.user)? - RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser 'room_changed_description', room._id, message.purpose, @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'channel_topic' - if @getRocketUser(message.user)? - RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser 'room_changed_topic', room._id, message.topic, @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'channel_name' - if @getRocketUser(message.user)? - RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser room._id, message.name, @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'pinned_item' - if message.attachments - msgObj = - attachments: [ - "text" : @convertSlackMessageToRocketChat message.attachments[0].text - "author_name" : message.attachments[0].author_subname - "author_icon" : getAvatarUrlFromUsername(message.attachments[0].author_subname) - ] - _.extend msgObj, msgDataDefaults - RocketChat.models.Messages.createWithTypeRoomIdMessageAndUser 'message_pinned', room._id, '', @getRocketUser(message.user), msgObj - else - #TODO: make this better - @logger.debug('Pinned item with no attachment, needs work.'); - #RocketChat.models.Messages.createWithTypeRoomIdMessageAndUser 'message_pinned', room._id, '', @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'file_share' - if message.file?.url_private_download isnt undefined - details = - message_id: "slack-#{message.ts.replace(/\./g, '-')}" - name: message.file.name - size: message.file.size - type: message.file.mimetype - rid: room._id - @uploadFile details, message.file.url_private_download, @getRocketUser(message.user), room, new Date(parseInt(message.ts.split('.')[0]) * 1000) - else - if not missedTypes[message.subtype] and not ignoreTypes[message.subtype] - missedTypes[message.subtype] = message - else - user = @getRocketUser(message.user) - if user? - msgObj = - msg: @convertSlackMessageToRocketChat message.text - rid: room._id - u: - _id: user._id - username: user.username - - _.extend msgObj, msgDataDefaults - - if message.edited? - msgObj.editedAt = new Date(parseInt(message.edited.ts.split('.')[0]) * 1000) - editedBy = @getRocketUser(message.edited.user) - if editedBy? - msgObj.editedBy = - _id: editedBy._id - username: editedBy.username - - RocketChat.sendMessage @getRocketUser(message.user), msgObj, room, true - - # Process the reactions - if RocketChat.models.Messages.findOneById(msgDataDefaults._id)? and message.reactions?.length > 0 - for reaction in message.reactions - for u in reaction.users - rcUser = @getRocketUser(u) - if rcUser? - Meteor.runAsUser rcUser._id, () => - Meteor.call 'setReaction', ":#{reaction.name}:", msgDataDefaults._id - - @addCountCompleted 1 - - if not _.isEmpty missedTypes - console.log 'Missed import types:', missedTypes - - @updateProgress Importer.ProgressStep.FINISHING - for channel in @channels.channels when channel.do_import and channel.is_archived - do (channel) => - Meteor.runAsUser startedByUserId, () => - Meteor.call 'archiveRoom', channel.rocketId - - @updateProgress Importer.ProgressStep.DONE - timeTook = Date.now() - start - @logger.log "Import took #{timeTook} milliseconds." - - return @getProgress() - - getSlackChannelFromName: (channelName) => - for channel in @channels.channels when channel.name is channelName - return channel - - getRocketUser: (slackId) => - for user in @users.users when user.id is slackId - return RocketChat.models.Users.findOneById user.rocketId, { fields: { username: 1, name: 1 }} - - convertSlackMessageToRocketChat: (message) => - if message? - message = message.replace //g, '@all' - message = message.replace //g, '@all' - message = message.replace //g, '@here' - message = message.replace />/g, '>' - message = message.replace /</g, '<' - message = message.replace /&/g, '&' - message = message.replace /:simple_smile:/g, ':smile:' - message = message.replace /:memo:/g, ':pencil:' - message = message.replace /:piggy:/g, ':pig:' - message = message.replace /:uk:/g, ':gb:' - message = message.replace /<(http[s]?:[^>]*)>/g, '$1' - for userReplace in @userTags - message = message.replace userReplace.slack, userReplace.rocket - message = message.replace userReplace.slackLong, userReplace.rocket - else - message = '' - return message - - getSelection: () => - selectionUsers = @users.users.map (user) -> - return new Importer.SelectionUser user.id, user.name, user.profile.email, user.deleted, user.is_bot, !user.is_bot - selectionChannels = @channels.channels.map (channel) -> - return new Importer.SelectionChannel channel.id, channel.name, channel.is_archived, true, false - - return new Importer.Selection @name, selectionUsers, selectionChannels diff --git a/packages/rocketchat-importer-slack/server.js b/packages/rocketchat-importer-slack/server.js new file mode 100644 index 0000000000000000000000000000000000000000..ad52655f58a0c6d7b640d5b5bebf0c9a9eaa6dce --- /dev/null +++ b/packages/rocketchat-importer-slack/server.js @@ -0,0 +1,434 @@ +/* globals Importer */ +Importer.Slack = class extends Importer.Base { + constructor(name, descriptionI18N, mimeType) { + super(name, descriptionI18N, mimeType); + this.userTags = []; + this.bots = {}; + this.logger.debug('Constructed a new Slack Importer.'); + } + prepare(dataURI, sentContentType, fileName) { + super.prepare(dataURI, sentContentType, fileName); + const {image/*, contentType*/} = RocketChatFile.dataURIParse(dataURI); + const zip = new this.AdmZip(new Buffer(image, 'base64')); + const zipEntries = zip.getEntries(); + let tempChannels = []; + let tempUsers = []; + const tempMessages = {}; + zipEntries.forEach(entry => { + if (entry.entryName.indexOf('__MACOSX') > -1) { + return this.logger.debug(`Ignoring the file: ${ entry.entryName }`); + } + if (entry.entryName === 'channels.json') { + this.updateProgress(Importer.ProgressStep.PREPARING_CHANNELS); + tempChannels = JSON.parse(entry.getData().toString()).filter(channel => channel.creator != null); + return; + } + if (entry.entryName === 'users.json') { + this.updateProgress(Importer.ProgressStep.PREPARING_USERS); + tempUsers = JSON.parse(entry.getData().toString()); + return tempUsers.forEach(user => { + if (user.is_bot) { + this.bots[user.profile.bot_id] = user; + } + }); + } + if (!entry.isDirectory && entry.entryName.indexOf('/') > -1) { + const item = entry.entryName.split('/'); + const channelName = item[0]; + const msgGroupData = item[1].split('.')[0]; + tempMessages[channelName] = tempMessages[channelName] || {}; + try { + tempMessages[channelName][msgGroupData] = JSON.parse(entry.getData().toString()); + } catch (error) { + this.logger.warn(`${ entry.entryName } is not a valid JSON file! Unable to import it.`); + } + } + }); + + // Insert the users record, eventually this might have to be split into several ones as well + // if someone tries to import a several thousands users instance + const usersId = this.collection.insert({ 'import': this.importRecord._id, 'importer': this.name, 'type': 'users', 'users': tempUsers }); + this.users = this.collection.findOne(usersId); + this.updateRecord({ 'count.users': tempUsers.length }); + this.addCountToTotal(tempUsers.length); + + // Insert the channels records. + const channelsId = this.collection.insert({ 'import': this.importRecord._id, 'importer': this.name, 'type': 'channels', 'channels': tempChannels }); + this.channels = this.collection.findOne(channelsId); + this.updateRecord({ 'count.channels': tempChannels.length }); + this.addCountToTotal(tempChannels.length); + + // Insert the messages records + this.updateProgress(Importer.ProgressStep.PREPARING_MESSAGES); + + let messagesCount = 0; + Object.keys(tempMessages).forEach(channel => { + const messagesObj = tempMessages[channel]; + this.messages[channel] = this.messages[channel] || {}; + Object.keys(messagesObj).forEach(date => { + const msgs = messagesObj[date]; + messagesCount += msgs.length; + this.updateRecord({ 'messagesstatus': '#{channel}/#{date}' }); + if (Importer.Base.getBSONSize(msgs) > Importer.Base.MaxBSONSize) { + const tmp = Importer.Base.getBSONSafeArraysFromAnArray(msgs); + Object.keys(tmp).forEach(i => { + const splitMsg = tmp[i]; + const messagesId = this.collection.insert({ 'import': this.importRecord._id, 'importer': this.name, 'type': 'messages', 'name': `${ channel }/${ date }.${ i }`, 'messages': splitMsg }); + this.messages[channel][`${ date }.${ i }`] = this.collection.findOne(messagesId); + }); + } else { + const messagesId = this.collection.insert({ 'import': this.importRecord._id, 'importer': this.name, 'type': 'messages', 'name': `${ channel }/${ date }`, 'messages': msgs }); + this.messages[channel][date] = this.collection.findOne(messagesId); + } + }); + }); + this.updateRecord({ 'count.messages': messagesCount, 'messagesstatus': null }); + this.addCountToTotal(messagesCount); + if ([tempUsers.length, tempChannels.length, messagesCount].some(e => e === 0)) { + this.logger.warn(`The loaded users count ${ tempUsers.length }, the loaded channels ${ tempChannels.length }, and the loaded messages ${ messagesCount }`); + console.log(`The loaded users count ${ tempUsers.length }, the loaded channels ${ tempChannels.length }, and the loaded messages ${ messagesCount }`); + this.updateProgress(Importer.ProgressStep.ERROR); + return this.getProgress(); + } + const selectionUsers = tempUsers.map(user => new Importer.SelectionUser(user.id, user.name, user.profile.email, user.deleted, user.is_bot, !user.is_bot)); + const selectionChannels = tempChannels.map(channel => new Importer.SelectionChannel(channel.id, channel.name, channel.is_archived, true, false)); + this.updateProgress(Importer.ProgressStep.USER_SELECTION); + return new Importer.Selection(this.name, selectionUsers, selectionChannels); + } + startImport(importSelection) { + super.startImport(importSelection); + const start = Date.now(); + Object.keys(importSelection.users).forEach(key => { + const user = importSelection.users[key]; + Object.keys(this.users.users).forEach(k => { + const u = this.users.users[k]; + if (u.id === user.user_id) { + u.do_import = user.do_import; + } + }); + }); + this.collection.update({ _id: this.users._id }, { $set: { 'users': this.users.users }}); + Object.keys(importSelection.channels).forEach(key => { + const channel = importSelection.channels[key]; + Object.keys(this.channels.channels).forEach(k => { + const c = this.channels.channels[k]; + if (c.id === channel.channel_id) { + c.do_import = channel.do_import; + } + }); + }); + this.collection.update({ _id: this.channels._id }, { $set: { 'channels': this.channels.channels }}); + const startedByUserId = Meteor.userId(); + Meteor.defer(() => { + this.updateProgress(Importer.ProgressStep.IMPORTING_USERS); + this.users.users.forEach(user => { + if (!user.do_import) { + return; + } + Meteor.runAsUser(startedByUserId, () => { + const existantUser = RocketChat.models.Users.findOneByEmailAddress(user.profile.email) || RocketChat.models.Users.findOneByUsername(user.name); + if (existantUser) { + user.rocketId = existantUser._id; + RocketChat.models.Users.update({ _id: user.rocketId }, { $addToSet: { importIds: user.id } }); + this.userTags.push({ + slack: `<@${ user.id }>`, + slackLong: `<@${ user.id }|${ user.name }>`, + rocket: `@${ existantUser.username }` + }); + } else { + const userId = user.profile.email ? Accounts.createUser({ email: user.profile.email, password: Date.now() + user.name + user.profile.email.toUpperCase() }) : Accounts.createUser({ username: user.name, password: Date.now() + user.name, joinDefaultChannelsSilenced: true }); + Meteor.runAsUser(userId, () => { + Meteor.call('setUsername', user.name, {joinDefaultChannelsSilenced: true}); + const url = user.profile.image_original || user.profile.image_512; + try { + Meteor.call('setAvatarFromService', url, undefined, 'url'); + } catch (error) { + this.logger.warn(`Failed to set ${ user.name }'s avatar from url ${ url }`); + console.log(`Failed to set ${ user.name }'s avatar from url ${ url }`); + } + // Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600 + if (user.tz_offset) { + Meteor.call('userSetUtcOffset', user.tz_offset / 3600); + } + }); + + RocketChat.models.Users.update({ _id: userId }, { $addToSet: { importIds: user.id } }); + + if (user.profile.real_name) { + RocketChat.models.Users.setName(userId, user.profile.real_name); + } + //Deleted users are 'inactive' users in Rocket.Chat + if (user.deleted) { + Meteor.call('setUserActiveStatus', userId, false); + } + //TODO: Maybe send emails? + user.rocketId = userId; + this.userTags.push({ + slack: `<@${ user.id }>`, + slackLong: `<@${ user.id }|${ user.name }>`, + rocket: `@${ user.name }` + }); + + } + this.addCountCompleted(1); + }); + }); + this.collection.update({ _id: this.users._id }, { $set: { 'users': this.users.users }}); + this.updateProgress(Importer.ProgressStep.IMPORTING_CHANNELS); + this.channels.channels.forEach(channel => { + if (!channel.do_import) { + return; + } + Meteor.runAsUser (startedByUserId, () => { + const existantRoom = RocketChat.models.Rooms.findOneByName(channel.name); + if (existantRoom || channel.is_general) { + if (channel.is_general && existantRoom && channel.name !== existantRoom.name) { + Meteor.call('saveRoomSettings', 'GENERAL', 'roomName', channel.name); + } + channel.rocketId = channel.is_general ? 'GENERAL' : existantRoom._id; + RocketChat.models.Rooms.update({ _id: channel.rocketId }, { $addToSet: { importIds: channel.id } }); + } else { + const users = channel.members + .reduce((ret, member) => { + if (member !== channel.creator) { + const user = this.getRocketUser(member); + if (user && user.username) { + ret.push(user.username); + } + } + return ret; + }, []); + let userId = startedByUserId; + this.users.users.forEach(user => { + if (user.id === channel.creator && user.do_import) { + userId = user.rocketId; + } + }); + Meteor.runAsUser(userId, () => { + const returned = Meteor.call('createChannel', channel.name, users); + channel.rocketId = returned.rid; + }); + + // @TODO implement model specific function + const roomUpdate = { + ts: new Date(channel.created * 1000) + }; + if (!_.isEmpty(channel.topic && channel.topic.value)) { + roomUpdate.topic = channel.topic.value; + } + if (!_.isEmpty(channel.purpose && channel.purpose.value)) { + roomUpdate.description = channel.purpose.value; + } + RocketChat.models.Rooms.update({ _id: channel.rocketId }, { $set: roomUpdate, $addToSet: { importIds: channel.id } }); + } + this.addCountCompleted(1); + }); + }); + this.collection.update({ _id: this.channels._id }, { $set: { 'channels': this.channels.channels }}); + const missedTypes = {}; + const ignoreTypes = { 'bot_add': true, 'file_comment': true, 'file_mention': true }; + this.updateProgress(Importer.ProgressStep.IMPORTING_MESSAGES); + Object.keys(this.messages).forEach(channel => { + const messagesObj = this.messages[channel]; + + Meteor.runAsUser(startedByUserId, () =>{ + const slackChannel = this.getSlackChannelFromName(channel); + if (!slackChannel || !slackChannel.do_import) { return; } + const room = RocketChat.models.Rooms.findOneById(slackChannel.rocketId, { fields: { usernames: 1, t: 1, name: 1 } }); + Object.keys(messagesObj).forEach(date => { + const msgs = messagesObj[date]; + msgs.messages.forEach(message => { + this.updateRecord({ 'messagesstatus': '#{channel}/#{date}.#{msgs.messages.length}' }); + const msgDataDefaults ={ + _id: `slack-${ slackChannel.id }-${ message.ts.replace(/\./g, '-') }`, + ts: new Date(parseInt(message.ts.split('.')[0]) * 1000) + }; + if (message.type === 'message') { + if (message.subtype) { + if (message.subtype === 'channel_join') { + if (this.getRocketUser(message.user)) { + RocketChat.models.Messages.createUserJoinWithRoomIdAndUser(room._id, this.getRocketUser(message.user), msgDataDefaults); + } + } else if (message.subtype === 'channel_leave') { + if (this.getRocketUser(message.user)) { RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser(room._id, this.getRocketUser(message.user), msgDataDefaults); } + } else if (message.subtype === 'me_message') { + const msgObj = { + ...msgDataDefaults, + msg: `_${ this.convertSlackMessageToRocketChat(message.text) }_` + }; + RocketChat.sendMessage(this.getRocketUser(message.user), msgObj, room, true); + } else if (message.subtype === 'bot_message' || message.subtype === 'slackbot_response') { + const botUser = RocketChat.models.Users.findOneById('rocket.cat', { fields: { username: 1 }}); + const botUsername = this.bots[message.bot_id] ? this.bots[message.bot_id].name : message.username; + const msgObj = { + ...msgDataDefaults, + msg: this.convertSlackMessageToRocketChat(message.text), + rid: room._id, + bot: true, + attachments: message.attachments, + username: botUsername || undefined + }; + + if (message.edited) { + msgObj.editedAt = new Date(parseInt(message.edited.ts.split('.')[0]) * 1000); + const editedBy = this.getRocketUser(message.edited.user); + if (editedBy) { + msgObj.editedBy = { + _id: editedBy._id, + username: editedBy.username + }; + } + } + + if (message.icons) { + msgObj.emoji = message.icons.emoji; + } + RocketChat.sendMessage(botUser, msgObj, room, true); + } else if (message.subtype === 'channel_purpose') { + if (this.getRocketUser(message.user)) { + RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_description', room._id, message.purpose, this.getRocketUser(message.user), msgDataDefaults); + } + } else if (message.subtype === 'channel_topic') { + if (this.getRocketUser(message.user)) { + RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', room._id, message.topic, this.getRocketUser(message.user), msgDataDefaults); + } + } else if (message.subtype === 'channel_name') { + if (this.getRocketUser(message.user)) { + RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser(room._id, message.name, this.getRocketUser(message.user), msgDataDefaults); + } + } else if (message.subtype === 'pinned_item') { + if (message.attachments) { + const msgObj = { + ...msgDataDefaults, + attachments: [{ + 'text': this.convertSlackMessageToRocketChat(message.attachments[0].text), + 'author_name' : message.attachments[0].author_subname, + 'author_icon' : getAvatarUrlFromUsername(message.attachments[0].author_subname) + }] + }; + RocketChat.models.Messages.createWithTypeRoomIdMessageAndUser('message_pinned', room._id, '', this.getRocketUser(message.user), msgObj); + } else { + //TODO: make this better + this.logger.debug('Pinned item with no attachment, needs work.'); + //RocketChat.models.Messages.createWithTypeRoomIdMessageAndUser 'message_pinned', room._id, '', @getRocketUser(message.user), msgDataDefaults + } + } else if (message.subtype === 'file_share') { + if (message.file && message.file.url_private_download !== undefined) { + const details = { + message_id: `slack-${ message.ts.replace(/\./g, '-') }`, + name: message.file.name, + size: message.file.size, + type: message.file.mimetype, + rid: room._id + }; + this.uploadFile(details, message.file.url_private_download, this.getRocketUser(message.user), room, new Date(parseInt(message.ts.split('.')[0]) * 1000)); + } + } else if (!missedTypes[message.subtype] && !ignoreTypes[message.subtype]) { + missedTypes[message.subtype] = message; + } + } else { + const user = this.getRocketUser(message.user); + if (user) { + const msgObj = { + ...msgDataDefaults, + msg: this.convertSlackMessageToRocketChat(message.text), + rid: room._id, + u: { + _id: user._id, + username: user.username + } + }; + + if (message.edited) { + msgObj.editedAt = new Date(parseInt(message.edited.ts.split('.')[0]) * 1000); + const editedBy = this.getRocketUser(message.edited.user); + if (editedBy) { + msgObj.editedBy = { + _id: editedBy._id, + username: editedBy.username + }; + } + } + + RocketChat.sendMessage(this.getRocketUser(message.user), msgObj, room, true); + } + } + } + + + // Process the reactions + if (RocketChat.models.Messages.findOneById(msgDataDefaults._id) && message.reactions && message.reactions.length > 0) { + message.reactions.forEach(reaction => { + reaction.users.forEach(u => { + const rcUser = this.getRocketUser(u); + if (!rcUser) { return; } + Meteor.runAsUser(rcUser._id, () => Meteor.call('setReaction', `:${ reaction.name }:`, msgDataDefaults._id)); + }); + }); + } + this.addCountCompleted(1); + }); + }); + }); + }); + + + if (!_.isEmpty(missedTypes)) { + console.log('Missed import types:', missedTypes); + } + + this.updateProgress(Importer.ProgressStep.FINISHING); + + this.channels.channels.forEach(channel => { + if (channel.do_import && channel.is_archived) { + Meteor.runAsUser(startedByUserId, function() { + Meteor.call('archiveRoom', channel.rocketId); + }); + } + }); + this.updateProgress(Importer.ProgressStep.DONE); + + const timeTook = Date.now() - start; + + this.logger.log(`Import took ${ timeTook } milliseconds.`); + + }); + return this.getProgress(); + } + getSlackChannelFromName(channelName) { + return this.channels.channels.find(channel => channel.name === channelName); + } + getRocketUser(slackId) { + const user = this.users.users.find(user => user.id === slackId); + if (user) { + return RocketChat.models.Users.findOneById(user.rocketId, { fields: { username: 1, name: 1 }}); + } + } + convertSlackMessageToRocketChat(message) { + if (message != null) { + message = message.replace(//g, '@all'); + message = message.replace(//g, '@all'); + message = message.replace(//g, '@here'); + message = message.replace(/>/g, '>'); + message = message.replace(/</g, '<'); + message = message.replace(/&/g, '&'); + message = message.replace(/:simple_smile:/g, ':smile:'); + message = message.replace(/:memo:/g, ':pencil:'); + message = message.replace(/:piggy:/g, ':pig:'); + message = message.replace(/:uk:/g, ':gb:'); + message = message.replace(/<(http[s]?:[^>]*)>/g, '$1'); + for (const userReplace of Array.from(this.userTags)) { + message = message.replace(userReplace.slack, userReplace.rocket); + message = message.replace(userReplace.slackLong, userReplace.rocket); + } + } else { + message = ''; + } + return message; + } + getSelection() { + const selectionUsers = this.users.users.map(user => new Importer.SelectionUser(user.id, user.name, user.profile.email, user.deleted, user.is_bot, !user.is_bot)); + const selectionChannels = this.channels.channels.map(channel => new Importer.SelectionChannel(channel.id, channel.name, channel.is_archived, true, false)); + return new Importer.Selection(this.name, selectionUsers, selectionChannels); + } +}; diff --git a/packages/rocketchat-importer/client/admin/adminImport.coffee b/packages/rocketchat-importer/client/admin/adminImport.coffee deleted file mode 100644 index c4a8506248e460521c9e2bc5469e707c2151bdf6..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/client/admin/adminImport.coffee +++ /dev/null @@ -1,24 +0,0 @@ -Template.adminImport.helpers - isAdmin: -> - return RocketChat.authz.hasRole(Meteor.userId(), 'admin') - isImporters: -> - return Object.keys(Importer.Importers).length > 0 - getDescription: (importer) -> - return TAPi18n.__('Importer_From_Description', { from: importer.name }) - importers: -> - importers = [] - _.each Importer.Importers, (importer, key) -> - importer.key = key - importers.push importer - return importers - -Template.adminImport.events - 'click .start-import': (event) -> - importer = @ - - Meteor.call 'setupImporter', importer.key, (error, data) -> - if error - console.log t('importer_setup_error'), importer.key, error - return handleError(error) - else - FlowRouter.go '/admin/import/prepare/' + importer.key diff --git a/packages/rocketchat-importer/client/admin/adminImport.js b/packages/rocketchat-importer/client/admin/adminImport.js new file mode 100644 index 0000000000000000000000000000000000000000..9bfd1e34a4c74ab336f6cab953a9f4284ff4a0df --- /dev/null +++ b/packages/rocketchat-importer/client/admin/adminImport.js @@ -0,0 +1,33 @@ +/* globals Importer */ +Template.adminImport.helpers({ + isAdmin() { + return RocketChat.authz.hasRole(Meteor.userId(), 'admin'); + }, + isImporters() { + return Object.keys(Importer.Importers).length > 0; + }, + getDescription(importer) { + return TAPi18n.__('Importer_From_Description', { from: importer.name }); + }, + importers() { + const importers = []; + _.each(Importer.Importers, function(importer, key) { + importer.key = key; + return importers.push(importer); + }); + return importers; + } +}); + +Template.adminImport.events({ + 'click .start-import'() { + const importer = this; + return Meteor.call('setupImporter', importer.key, function(error) { + if (error) { + console.log(t('importer_setup_error'), importer.key, error); + return handleError(error); + } + return FlowRouter.go(`/admin/import/prepare/${ importer.key }`); + }); + } +}); diff --git a/packages/rocketchat-importer/client/admin/adminImportPrepare.coffee b/packages/rocketchat-importer/client/admin/adminImportPrepare.coffee deleted file mode 100644 index 823418efed047b62b312260eda1f49f4610f2274..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/client/admin/adminImportPrepare.coffee +++ /dev/null @@ -1,152 +0,0 @@ -import toastr from 'toastr' -Template.adminImportPrepare.helpers - isAdmin: -> - return RocketChat.authz.hasRole(Meteor.userId(), 'admin') - importer: -> - importerKey = FlowRouter.getParam('importer') - importer = undefined - _.each Importer.Importers, (i, key) -> - i.key = key - if key == importerKey - importer = i - - return importer - isLoaded: -> - return Template.instance().loaded.get() - isPreparing: -> - return Template.instance().preparing.get() - users: -> - return Template.instance().users.get() - channels: -> - return Template.instance().channels.get() - -Template.adminImportPrepare.events - 'change .import-file-input': (event, template) -> - importer = @ - return if not importer.key - - e = event.originalEvent or event - files = e.target.files - if not files or files.length is 0 - files = e.dataTransfer?.files or [] - - for blob in files - template.preparing.set true - - reader = new FileReader() - reader.readAsDataURL(blob) - reader.onloadend = -> - Meteor.call 'prepareImport', importer.key, reader.result, blob.type, blob.name, (error, data) -> - if error - toastr.error t('Invalid_Import_File_Type') - template.preparing.set false - return - - if !data - console.warn 'The importer ' + importer.key + ' is not set up correctly, as it did not return any data.' - toastr.error t('Importer_not_setup') - template.preparing.set false - return - - if data.step - console.warn 'Invalid file, contains `data.step`.', data - toastr.error t('Invalid_Export_File', importer.key) - template.preparing.set false - return - - template.users.set data.users - template.channels.set data.channels - template.loaded.set true - template.preparing.set false - - 'click .button.start': (event, template) -> - btn = this - $(btn).prop "disabled", true - importer = @ - for user in template.users.get() - user.do_import = $("[name=#{user.user_id}]").is(':checked') - - for channel in template.channels.get() - channel.do_import = $("[name=#{channel.channel_id}]").is(':checked') - - Meteor.call 'startImport', FlowRouter.getParam('importer'), { users: template.users.get(), channels: template.channels.get() }, (error, data) -> - if error - console.warn 'Error on starting the import:', error - return handleError(error) - else - FlowRouter.go '/admin/import/progress/' + FlowRouter.getParam('importer') - - 'click .button.restart': (event, template) -> - Meteor.call 'restartImport', FlowRouter.getParam('importer'), (error, data) -> - if error - console.warn 'Error while restarting the import:', error - handleError(error) - return - - template.users.set [] - template.channels.set [] - template.loaded.set false - - 'click .button.uncheck-deleted-users': (event, template) -> - for user in template.users.get() when user.is_deleted - $("[name=#{user.user_id}]").attr('checked', false); - - 'click .button.uncheck-archived-channels': (event, template) -> - for channel in template.channels.get() when channel.is_archived - $("[name=#{channel.channel_id}]").attr('checked', false); - - -Template.adminImportPrepare.onCreated -> - instance = @ - @preparing = new ReactiveVar true - @loaded = new ReactiveVar false - @users = new ReactiveVar [] - @channels = new ReactiveVar [] - - loadSelection = (progress) -> - if progress?.step - switch progress.step - #When the import is running, take the user to the progress page - when 'importer_importing_started', 'importer_importing_users', 'importer_importing_channels', 'importer_importing_messages', 'importer_finishing' - FlowRouter.go '/admin/import/progress/' + FlowRouter.getParam('importer') - # when the import is done, restart it (new instance) - when 'importer_user_selection' - Meteor.call 'getSelectionData', FlowRouter.getParam('importer'), (error, data) -> - if error - handleError error - instance.users.set data.users - instance.channels.set data.channels - instance.loaded.set true - instance.preparing.set false - when 'importer_new' - instance.preparing.set false - else - Meteor.call 'restartImport', FlowRouter.getParam('importer'), (error, progress) -> - if error - handleError(error) - loadSelection(progress) - else - console.warn 'Invalid progress information.', progress - - # Load the initial progress to determine what we need to do - if FlowRouter.getParam('importer') - Meteor.call 'getImportProgress', FlowRouter.getParam('importer'), (error, progress) -> - if error - console.warn 'Error while getting the import progress:', error - handleError error - return - - # if the progress isnt defined, that means there currently isn't an instance - # of the importer, so we need to create it - if progress is undefined - Meteor.call 'setupImporter', FlowRouter.getParam('importer'), (err, data) -> - if err - handleError(err) - instance.preparing.set false - loadSelection(data) - else - # Otherwise, we might need to do something based upon the current step - # of the import - loadSelection(progress) - else - FlowRouter.go '/admin/import' diff --git a/packages/rocketchat-importer/client/admin/adminImportPrepare.js b/packages/rocketchat-importer/client/admin/adminImportPrepare.js new file mode 100644 index 0000000000000000000000000000000000000000..fe7e00ee045fae999e087f6657bed1b77ed259ad --- /dev/null +++ b/packages/rocketchat-importer/client/admin/adminImportPrepare.js @@ -0,0 +1,195 @@ +/* globals Importer */ +import toastr from 'toastr'; +Template.adminImportPrepare.helpers({ + isAdmin() { + return RocketChat.authz.hasRole(Meteor.userId(), 'admin'); + }, + importer() { + const importerKey = FlowRouter.getParam('importer'); + let importer = undefined; + _.each(Importer.Importers, function(i, key) { + i.key = key; + if (key === importerKey) { + return importer = i; + } + }); + + return importer; + }, + isLoaded() { + return Template.instance().loaded.get(); + }, + isPreparing() { + return Template.instance().preparing.get(); + }, + users() { + return Template.instance().users.get(); + }, + channels() { + return Template.instance().channels.get(); + } +}); + +Template.adminImportPrepare.events({ + 'change .import-file-input'(event, template) { + const importer = this; + if (!importer.key) { return; } + + const e = event.originalEvent || event; + let { files } = e.target; + if (!files || (files.length === 0)) { + files = (e.dataTransfer != null ? e.dataTransfer.files : undefined) || []; + } + + return Array.from(files).map((blob) => { + template.preparing.set(true); + + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = () => { + Meteor.call('prepareImport', importer.key, reader.result, blob.type, blob.name, function(error, data) { + if (error) { + toastr.error(t('Invalid_Import_File_Type')); + template.preparing.set(false); + return; + } + + if (!data) { + console.warn(`The importer ${ importer.key } is not set up correctly, as it did not return any data.`); + toastr.error(t('Importer_not_setup')); + template.preparing.set(false); + return; + } + + if (data.step) { + console.warn('Invalid file, contains `data.step`.', data); + toastr.error(t('Invalid_Export_File', importer.key)); + template.preparing.set(false); + return; + } + + template.users.set(data.users); + template.channels.set(data.channels); + template.loaded.set(true); + template.preparing.set(false); + }); + }; + }); + }, + + 'click .button.start'(event, template) { + const btn = this; + $(btn).prop('disabled', true); + // const importer = this; + for (const user of Array.from(template.users.get())) { + user.do_import = $(`[name=${ user.user_id }]`).is(':checked'); + } + + for (const channel of Array.from(template.channels.get())) { + channel.do_import = $(`[name=${ channel.channel_id }]`).is(':checked'); + } + + return Meteor.call('startImport', FlowRouter.getParam('importer'), { users: template.users.get(), channels: template.channels.get() }, function(error) { + if (error) { + console.warn('Error on starting the import:', error); + return handleError(error); + } else { + return FlowRouter.go(`/admin/import/progress/${ FlowRouter.getParam('importer') }`); + } + }); + }, + + 'click .button.restart'(event, template) { + Meteor.call('restartImport', FlowRouter.getParam('importer'), function(error) { + if (error) { + console.warn('Error while restarting the import:', error); + handleError(error); + return; + } + + template.users.set([]); + template.channels.set([]); + template.loaded.set(false); + }); + }, + + 'click .button.uncheck-deleted-users'(event, template) { + Array.from(template.users.get()).filter((user) => user.is_deleted).map((user) => + $(`[name=${ user.user_id }]`).attr('checked', false)); + }, + + 'click .button.uncheck-archived-channels'(event, template) { + Array.from(template.channels.get()).filter((channel) => channel.is_archived).map((channel) => + $(`[name=${ channel.channel_id }]`).attr('checked', false)); + } +}); + + +Template.adminImportPrepare.onCreated(function() { + const instance = this; + this.preparing = new ReactiveVar(true); + this.loaded = new ReactiveVar(false); + this.users = new ReactiveVar([]); + this.channels = new ReactiveVar([]); + + function loadSelection(progress) { + if ((progress != null ? progress.step : undefined)) { + switch (progress.step) { + //When the import is running, take the user to the progress page + case 'importer_importing_started': case 'importer_importing_users': case 'importer_importing_channels': case 'importer_importing_messages': case 'importer_finishing': + return FlowRouter.go(`/admin/import/progress/${ FlowRouter.getParam('importer') }`); + // when the import is done, restart it (new instance) + case 'importer_user_selection': + return Meteor.call('getSelectionData', FlowRouter.getParam('importer'), function(error, data) { + if (error) { + handleError(error); + } + instance.users.set(data.users); + instance.channels.set(data.channels); + instance.loaded.set(true); + return instance.preparing.set(false); + }); + case 'importer_new': + return instance.preparing.set(false); + default: + return Meteor.call('restartImport', FlowRouter.getParam('importer'), function(error, progress) { + if (error) { + handleError(error); + } + return loadSelection(progress); + }); + } + } else { + return console.warn('Invalid progress information.', progress); + } + } + + // Load the initial progress to determine what we need to do + if (FlowRouter.getParam('importer')) { + return Meteor.call('getImportProgress', FlowRouter.getParam('importer'), function(error, progress) { + if (error) { + console.warn('Error while getting the import progress:', error); + handleError(error); + return; + } + + // if the progress isnt defined, that means there currently isn't an instance + // of the importer, so we need to create it + if (progress === undefined) { + return Meteor.call('setupImporter', FlowRouter.getParam('importer'), function(err, data) { + if (err) { + handleError(err); + } + instance.preparing.set(false); + return loadSelection(data); + }); + } else { + // Otherwise, we might need to do something based upon the current step + // of the import + return loadSelection(progress); + } + }); + } else { + return FlowRouter.go('/admin/import'); + } +}); diff --git a/packages/rocketchat-importer/client/admin/adminImportProgress.coffee b/packages/rocketchat-importer/client/admin/adminImportProgress.coffee deleted file mode 100644 index d7e9fe7511f409a717e8706bc8ec965a71bd16cc..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/client/admin/adminImportProgress.coffee +++ /dev/null @@ -1,41 +0,0 @@ -import toastr from 'toastr' -Template.adminImportProgress.helpers - step: -> - return Template.instance().step.get() - completed: -> - return Template.instance().completed.get() - total: -> - return Template.instance().total.get() - -Template.adminImportProgress.onCreated -> - instance = @ - @step = new ReactiveVar t('Loading...') - @completed = new ReactiveVar 0 - @total = new ReactiveVar 0 - @updateProgress = -> - if FlowRouter.getParam('importer') isnt '' - Meteor.call 'getImportProgress', FlowRouter.getParam('importer'), (error, progress) -> - if error - console.warn 'Error on getting the import progress:', error - handleError error - return - - if progress - if progress.step is 'importer_done' - toastr.success t(progress.step[0].toUpperCase() + progress.step.slice(1)) - FlowRouter.go '/admin/import' - else if progress.step is 'importer_import_failed' - toastr.error t(progress.step[0].toUpperCase() + progress.step.slice(1)) - FlowRouter.go '/admin/import/prepare/' + FlowRouter.getParam('importer') - else - instance.step.set t(progress.step[0].toUpperCase() + progress.step.slice(1)) - instance.completed.set progress.count.completed - instance.total.set progress.count.total - setTimeout(() -> - instance.updateProgress() - , 100) - else - toastr.warning t('Importer_not_in_progress') - FlowRouter.go '/admin/import/prepare/' + FlowRouter.getParam('importer') - - instance.updateProgress() diff --git a/packages/rocketchat-importer/client/admin/adminImportProgress.js b/packages/rocketchat-importer/client/admin/adminImportProgress.js new file mode 100644 index 0000000000000000000000000000000000000000..4d22c3a46a72d21a20d156039b7fb1619810647f --- /dev/null +++ b/packages/rocketchat-importer/client/admin/adminImportProgress.js @@ -0,0 +1,51 @@ +import toastr from 'toastr'; +Template.adminImportProgress.helpers({ + step() { + return Template.instance().step.get(); + }, + completed() { + return Template.instance().completed.get(); + }, + total() { + return Template.instance().total.get(); + } +}); + +Template.adminImportProgress.onCreated(function() { + const instance = this; + this.step = new ReactiveVar(t('Loading...')); + this.completed = new ReactiveVar(0); + this.total = new ReactiveVar(0); + this.updateProgress = function() { + if (FlowRouter.getParam('importer') !== '') { + return Meteor.call('getImportProgress', FlowRouter.getParam('importer'), function(error, progress) { + if (error) { + console.warn('Error on getting the import progress:', error); + handleError(error); + return; + } + + if (progress) { + if (progress.step === 'importer_done') { + toastr.success(t(progress.step[0].toUpperCase() + progress.step.slice(1))); + return FlowRouter.go('/admin/import'); + } else if (progress.step === 'importer_import_failed') { + toastr.error(t(progress.step[0].toUpperCase() + progress.step.slice(1))); + return FlowRouter.go(`/admin/import/prepare/${ FlowRouter.getParam('importer') }`); + } else { + instance.step.set(t(progress.step[0].toUpperCase() + progress.step.slice(1))); + instance.completed.set(progress.count.completed); + instance.total.set(progress.count.total); + return setTimeout(() => instance.updateProgress() + , 100); + } + } else { + toastr.warning(t('Importer_not_in_progress')); + return FlowRouter.go(`/admin/import/prepare/${ FlowRouter.getParam('importer') }`); + } + }); + } + }; + + return instance.updateProgress(); +}); diff --git a/packages/rocketchat-importer/lib/_importer.coffee b/packages/rocketchat-importer/lib/_importer.coffee deleted file mode 100644 index 625541b6160410314083edf270b21f6294f4dceb..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/lib/_importer.coffee +++ /dev/null @@ -1 +0,0 @@ -Importer = {} diff --git a/packages/rocketchat-importer/lib/_importer.js b/packages/rocketchat-importer/lib/_importer.js new file mode 100644 index 0000000000000000000000000000000000000000..8e7c3f9971cfa19c8dddf060263c4f1c08e41dc2 --- /dev/null +++ b/packages/rocketchat-importer/lib/_importer.js @@ -0,0 +1,3 @@ +/* globals Importer */ +Importer = {}; +export default Importer; diff --git a/packages/rocketchat-importer/lib/importTool.coffee b/packages/rocketchat-importer/lib/importTool.coffee deleted file mode 100644 index 99514e045e9a059092fd7e887da717a296eb65e8..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/lib/importTool.coffee +++ /dev/null @@ -1,9 +0,0 @@ -Importer.Importers = {} - -Importer.addImporter = (name, importer, options) -> - if not Importer.Importers[name]? - Importer.Importers[name] = - name: options.name - importer: importer - mimeType: options.mimeType - warnings: options.warnings diff --git a/packages/rocketchat-importer/lib/importTool.js b/packages/rocketchat-importer/lib/importTool.js new file mode 100644 index 0000000000000000000000000000000000000000..94315ab36bd5430c00be8704a2e90ba07810b35c --- /dev/null +++ b/packages/rocketchat-importer/lib/importTool.js @@ -0,0 +1,13 @@ +/* globals Importer */ +Importer.Importers = {}; + +Importer.addImporter = function(name, importer, options) { + if (Importer.Importers[name] == null) { + return Importer.Importers[name] = { + name: options.name, + importer, + mimeType: options.mimeType, + warnings: options.warnings + }; + } +}; diff --git a/packages/rocketchat-importer/package.js b/packages/rocketchat-importer/package.js index 5820b21e2ac7b13a34e21357e2a19518f824cb23..0f9dffc9a96b1af0313dbb136e62b6947d2a5cb9 100644 --- a/packages/rocketchat-importer/package.js +++ b/packages/rocketchat-importer/package.js @@ -9,7 +9,6 @@ Package.onUse(function(api) { api.use([ 'ecmascript', 'templating', - 'coffeescript', 'check', 'rocketchat:lib' ]); @@ -18,37 +17,37 @@ Package.onUse(function(api) { api.use('templating', 'client'); //Import Framework - api.addFiles('lib/_importer.coffee'); - api.addFiles('lib/importTool.coffee'); - api.addFiles('server/classes/ImporterBase.coffee', 'server'); - api.addFiles('server/classes/ImporterProgress.coffee', 'server'); - api.addFiles('server/classes/ImporterProgressStep.coffee', 'server'); - api.addFiles('server/classes/ImporterSelection.coffee', 'server'); - api.addFiles('server/classes/ImporterSelectionChannel.coffee', 'server'); - api.addFiles('server/classes/ImporterSelectionUser.coffee', 'server'); + api.addFiles('lib/_importer.js'); + api.addFiles('lib/importTool.js'); + api.addFiles('server/classes/ImporterBase.js', 'server'); + api.addFiles('server/classes/ImporterProgress.js', 'server'); + api.addFiles('server/classes/ImporterProgressStep.js', 'server'); + api.addFiles('server/classes/ImporterSelection.js', 'server'); + api.addFiles('server/classes/ImporterSelectionChannel.js', 'server'); + api.addFiles('server/classes/ImporterSelectionUser.js', 'server'); //Database models - api.addFiles('server/models/Imports.coffee', 'server'); - api.addFiles('server/models/RawImports.coffee', 'server'); + api.addFiles('server/models/Imports.js', 'server'); + api.addFiles('server/models/RawImports.js', 'server'); //Server methods - api.addFiles('server/methods/getImportProgress.coffee', 'server'); - api.addFiles('server/methods/getSelectionData.coffee', 'server'); + api.addFiles('server/methods/getImportProgress.js', 'server'); + api.addFiles('server/methods/getSelectionData.js', 'server'); api.addFiles('server/methods/prepareImport.js', 'server'); - api.addFiles('server/methods/restartImport.coffee', 'server'); - api.addFiles('server/methods/setupImporter.coffee', 'server'); - api.addFiles('server/methods/startImport.coffee', 'server'); + api.addFiles('server/methods/restartImport.js', 'server'); + api.addFiles('server/methods/setupImporter.js', 'server'); + api.addFiles('server/methods/startImport.js', 'server'); //Client api.addFiles('client/admin/adminImport.html', 'client'); - api.addFiles('client/admin/adminImport.coffee', 'client'); + api.addFiles('client/admin/adminImport.js', 'client'); api.addFiles('client/admin/adminImportPrepare.html', 'client'); - api.addFiles('client/admin/adminImportPrepare.coffee', 'client'); + api.addFiles('client/admin/adminImportPrepare.js', 'client'); api.addFiles('client/admin/adminImportProgress.html', 'client'); - api.addFiles('client/admin/adminImportProgress.coffee', 'client'); + api.addFiles('client/admin/adminImportProgress.js', 'client'); //Imports database records cleanup, mark all as not valid. - api.addFiles('server/startup/setImportsToInvalid.coffee', 'server'); + api.addFiles('server/startup/setImportsToInvalid.js', 'server'); api.export('Importer'); }); diff --git a/packages/rocketchat-importer/server/classes/ImporterBase.coffee b/packages/rocketchat-importer/server/classes/ImporterBase.coffee deleted file mode 100644 index 197e76d48ee86a6b76bdd5a7c22ba17bb3b1bf26..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/server/classes/ImporterBase.coffee +++ /dev/null @@ -1,207 +0,0 @@ -# Base class for all Importers. -# -# @example How to subclass an importer -# class ExampleImporter extends RocketChat.importTool._baseImporter -# constructor: -> -# super('Name of Importer', 'Description of the importer, use i18n string.', new RegExp('application\/.*?zip')) -# prepare: (uploadedFileData, uploadedFileContentType, uploadedFileName) => -# super -# startImport: (selectedUsersAndChannels) => -# super -# getProgress: => -# #return the progress report, tbd what is expected -# @version 1.0.0 -Importer.Base = class Importer.Base - @MaxBSONSize = 8000000 - @http = Npm.require 'http' - @https = Npm.require 'https' - - @getBSONSize: (object) -> - # The max BSON object size we can store in MongoDB is 16777216 bytes - # but for some reason the mongo instanace which comes with meteor - # errors out for anything close to that size. So, we are rounding it - # down to 8000000 bytes. - BSON = require('bson').native().BSON - bson = new BSON() - bson.calculateObjectSize object - - @getBSONSafeArraysFromAnArray: (theArray) -> - BSONSize = Importer.Base.getBSONSize theArray - maxSize = Math.floor(theArray.length / (Math.ceil(BSONSize / Importer.Base.MaxBSONSize))) - safeArrays = [] - i = 0 - while i < theArray.length - safeArrays.push(theArray.slice(i, i += maxSize)) - return safeArrays - - # Constructs a new importer, adding an empty collection, AdmZip property, and empty users & channels - # - # @param [String] name the name of the Importer - # @param [String] description the i18n string which describes the importer - # @param [String] mimeType the of the expected file type - # - constructor: (@name, @description, @mimeType) -> - @logger = new Logger("#{@name} Importer", {}); - @progress = new Importer.Progress @name - @collection = Importer.RawImports - @AdmZip = Npm.require 'adm-zip' - @getFileType = Npm.require 'file-type' - importId = Importer.Imports.insert { 'type': @name, 'ts': Date.now(), 'status': @progress.step, 'valid': true, 'user': Meteor.user()._id } - @importRecord = Importer.Imports.findOne importId - @users = {} - @channels = {} - @messages = {} - - # Takes the uploaded file and extracts the users, channels, and messages from it. - # - # @param [String] dataURI a base64 string of the uploaded file - # @param [String] sentContentType the file type - # @param [String] fileName the name of the uploaded file - # - # @return [Importer.Selection] Contains two properties which are arrays of objects, `channels` and `users`. - # - prepare: (dataURI, sentContentType, fileName) => - fileType = @getFileType(new Buffer(dataURI.split(',')[1], 'base64')) - @logger.debug 'Uploaded file information is:', fileType - @logger.debug 'Expected file type is:', @mimeType - - if not fileType or fileType.mime isnt @mimeType - @logger.warn "Invalid file uploaded for the #{@name} importer." - throw new Meteor.Error('error-invalid-file-uploaded', "Invalid file uploaded to import #{@name} data from.", { step: 'prepare' }) - - @updateProgress Importer.ProgressStep.PREPARING_STARTED - @updateRecord { 'file': fileName } - - # Starts the import process. The implementing method should defer as soon as the selection is set, so the user who started the process - # doesn't end up with a "locked" ui while meteor waits for a response. The returned object should be the progress. - # - # @param [Importer.Selection] selectedUsersAndChannels an object with `channels` and `users` which contains information about which users and channels to import - # - # @return [Importer.Progress] the progress of the import - # - startImport: (importSelection) => - if importSelection is undefined - throw new Error "No selected users and channel data provided to the #{@name} importer." #TODO: Make translatable - else if importSelection.users is undefined - throw new Error "Users in the selected data wasn't found, it must but at least an empty array for the #{@name} importer." #TODO: Make translatable - else if importSelection.channels is undefined - throw new Error "Channels in the selected data wasn't found, it must but at least an empty array for the #{@name} importer." #TODO: Make translatable - - @updateProgress Importer.ProgressStep.IMPORTING_STARTED - - # Gets the Importer.Selection object for the import. - # - # @return [Importer.Selection] the users and channels selection - getSelection: () => - throw new Error "Invalid 'getSelection' called on #{@name}, it must be overridden and super can not be called." - - # Gets the progress of this importer. - # - # @return [Importer.Progress] the progress of the import - # - getProgress: => - return @progress - - # Updates the progress step of this importer. - # - # @return [Importer.Progress] the progress of the import - # - updateProgress: (step) => - @progress.step = step - - @logger.debug "#{@name} is now at #{step}." - @updateRecord { 'status': @progress.step } - - return @progress - - # Adds the passed in value to the total amount of items needed to complete. - # - # @return [Importer.Progress] the progress of the import - # - addCountToTotal: (count) => - @progress.count.total = @progress.count.total + count - @updateRecord { 'count.total': @progress.count.total } - - return @progress - - # Adds the passed in value to the total amount of items completed. - # - # @return [Importer.Progress] the progress of the import - # - addCountCompleted: (count) => - @progress.count.completed = @progress.count.completed + count - - #Only update the database every 500 records - #Or the completed is greater than or equal to the total amount - if (@progress.count.completed % 500 == 0) or @progress.count.completed >= @progress.count.total - @updateRecord { 'count.completed': @progress.count.completed } - - return @progress - - # Updates the import record with the given fields being `set` - # - # @return [Importer.Imports] the import record object - # - updateRecord: (fields) => - Importer.Imports.update { _id: @importRecord._id }, { $set: fields } - @importRecord = Importer.Imports.findOne @importRecord._id - - return @importRecord - - # Uploads the file to the storage. - # - # @param [Object] details an object with details about the upload. name, size, type, and rid - # @param [String] fileUrl url of the file to download/import - # @param [Object] user the Rocket.Chat user - # @param [Object] room the Rocket.Chat room - # @param [Date] timeStamp the timestamp the file was uploaded - # - uploadFile: (details, fileUrl, user, room, timeStamp) => - @logger.debug "Uploading the file #{details.name} from #{fileUrl}." - requestModule = if /https/i.test(fileUrl) then Importer.Base.https else Importer.Base.http - - requestModule.get fileUrl, Meteor.bindEnvironment((stream) -> - fileId = Meteor.fileStore.create details - if fileId - Meteor.fileStore.write stream, fileId, (err, file) -> - if err - throw new Error(err) - else - url = file.url.replace(Meteor.absoluteUrl(), '/') - - attachment = - title: "File Uploaded: #{file.name}" - title_link: url - - if /^image\/.+/.test file.type - attachment.image_url = url - attachment.image_type = file.type - attachment.image_size = file.size - attachment.image_dimensions = file.identify?.size - - if /^audio\/.+/.test file.type - attachment.audio_url = url - attachment.audio_type = file.type - attachment.audio_size = file.size - - if /^video\/.+/.test file.type - attachment.video_url = url - attachment.video_type = file.type - attachment.video_size = file.size - - msg = - rid: details.rid - ts: timeStamp - msg: '' - file: - _id: file._id - groupable: false - attachments: [attachment] - - if details.message_id? and (typeof details.message_id is 'string') - msg['_id'] = details.message_id - - RocketChat.sendMessage user, msg, room, true - else - @logger.error "Failed to create the store for #{fileUrl}!!!" - ) diff --git a/packages/rocketchat-importer/server/classes/ImporterBase.js b/packages/rocketchat-importer/server/classes/ImporterBase.js new file mode 100644 index 0000000000000000000000000000000000000000..a966c786f05f2480ba8c5515878da24bd623322b --- /dev/null +++ b/packages/rocketchat-importer/server/classes/ImporterBase.js @@ -0,0 +1,244 @@ +/* globals Importer */ +// Base class for all Importers. +// +// @example How to subclass an importer +// class ExampleImporter extends RocketChat.importTool._baseImporter +// constructor: -> +// super('Name of Importer', 'Description of the importer, use i18n string.', new RegExp('application\/.*?zip')) +// prepare: (uploadedFileData, uploadedFileContentType, uploadedFileName) => +// super +// startImport: (selectedUsersAndChannels) => +// super +// getProgress: => +// #return the progress report, tbd what is expected +// @version 1.0.0 +Importer.Base = class Base { + static getBSONSize(object) { + // The max BSON object size we can store in MongoDB is 16777216 bytes + // but for some reason the mongo instanace which comes with meteor + // errors out for anything close to that size. So, we are rounding it + // down to 8000000 bytes. + const { BSON } = require('bson').native(); + const bson = new BSON(); + return bson.calculateObjectSize(object); + } + + static getBSONSafeArraysFromAnArray(theArray) { + const BSONSize = Importer.Base.getBSONSize(theArray); + const maxSize = Math.floor(theArray.length / (Math.ceil(BSONSize / Importer.Base.MaxBSONSize))); + const safeArrays = []; + let i = 0; + while (i < theArray.length) { + safeArrays.push(theArray.slice(i, (i += maxSize))); + } + return safeArrays; + } + + // Constructs a new importer, adding an empty collection, AdmZip property, and empty users & channels + // + // @param [String] name the name of the Importer + // @param [String] description the i18n string which describes the importer + // @param [String] mimeType the of the expected file type + // + constructor(name, description, mimeType) { + this.MaxBSONSize = 8000000; + this.http = Npm.require('http'); + this.https = Npm.require('https'); + + + this.prepare = this.prepare.bind(this); + this.startImport = this.startImport.bind(this); + this.getSelection = this.getSelection.bind(this); + this.getProgress = this.getProgress.bind(this); + this.updateProgress = this.updateProgress.bind(this); + this.addCountToTotal = this.addCountToTotal.bind(this); + this.addCountCompleted = this.addCountCompleted.bind(this); + this.updateRecord = this.updateRecord.bind(this); + this.uploadFile = this.uploadFile.bind(this); + this.name = name; + this.description = description; + this.mimeType = mimeType; + this.logger = new Logger(`${ this.name } Importer`, {}); + this.progress = new Importer.Progress(this.name); + this.collection = Importer.RawImports; + this.AdmZip = Npm.require('adm-zip'); + this.getFileType = Npm.require('file-type'); + const importId = Importer.Imports.insert({ 'type': this.name, 'ts': Date.now(), 'status': this.progress.step, 'valid': true, 'user': Meteor.user()._id }); + this.importRecord = Importer.Imports.findOne(importId); + this.users = {}; + this.channels = {}; + this.messages = {}; + } + + // Takes the uploaded file and extracts the users, channels, and messages from it. + // + // @param [String] dataURI a base64 string of the uploaded file + // @param [String] sentContentType the file type + // @param [String] fileName the name of the uploaded file + // + // @return [Importer.Selection] Contains two properties which are arrays of objects, `channels` and `users`. + // + prepare(dataURI, sentContentType, fileName) { + const fileType = this.getFileType(new Buffer(dataURI.split(',')[1], 'base64')); + this.logger.debug('Uploaded file information is:', fileType); + this.logger.debug('Expected file type is:', this.mimeType); + + if (!fileType || (fileType.mime !== this.mimeType)) { + this.logger.warn(`Invalid file uploaded for the ${ this.name } importer.`); + throw new Meteor.Error('error-invalid-file-uploaded', `Invalid file uploaded to import ${ this.name } data from.`, { step: 'prepare' }); + } + + this.updateProgress(Importer.ProgressStep.PREPARING_STARTED); + return this.updateRecord({ 'file': fileName }); + } + + // Starts the import process. The implementing method should defer as soon as the selection is set, so the user who started the process + // doesn't end up with a "locked" ui while meteor waits for a response. The returned object should be the progress. + // + // @param [Importer.Selection] selectedUsersAndChannels an object with `channels` and `users` which contains information about which users and channels to import + // + // @return [Importer.Progress] the progress of the import + // + startImport(importSelection) { + if (importSelection === undefined) { + throw new Error(`No selected users and channel data provided to the ${ this.name } importer.`); //TODO: Make translatable + } else if (importSelection.users === undefined) { + throw new Error(`Users in the selected data wasn't found, it must but at least an empty array for the ${ this.name } importer.`); //TODO: Make translatable + } else if (importSelection.channels === undefined) { + throw new Error(`Channels in the selected data wasn't found, it must but at least an empty array for the ${ this.name } importer.`); //TODO: Make translatable + } + + return this.updateProgress(Importer.ProgressStep.IMPORTING_STARTED); + } + + // Gets the Importer.Selection object for the import. + // + // @return [Importer.Selection] the users and channels selection + getSelection() { + throw new Error(`Invalid 'getSelection' called on ${ this.name }, it must be overridden and super can not be called.`); + } + + // Gets the progress of this importer. + // + // @return [Importer.Progress] the progress of the import + // + getProgress() { + return this.progress; + } + + // Updates the progress step of this importer. + // + // @return [Importer.Progress] the progress of the import + // + updateProgress(step) { + this.progress.step = step; + + this.logger.debug(`${ this.name } is now at ${ step }.`); + this.updateRecord({ 'status': this.progress.step }); + + return this.progress; + } + + // Adds the passed in value to the total amount of items needed to complete. + // + // @return [Importer.Progress] the progress of the import + // + addCountToTotal(count) { + this.progress.count.total = this.progress.count.total + count; + this.updateRecord({ 'count.total': this.progress.count.total }); + + return this.progress; + } + + // Adds the passed in value to the total amount of items completed. + // + // @return [Importer.Progress] the progress of the import + // + addCountCompleted(count) { + this.progress.count.completed = this.progress.count.completed + count; + + //Only update the database every 500 records + //Or the completed is greater than or equal to the total amount + if (((this.progress.count.completed % 500) === 0) || (this.progress.count.completed >= this.progress.count.total)) { + this.updateRecord({ 'count.completed': this.progress.count.completed }); + } + + return this.progress; + } + + // Updates the import record with the given fields being `set` + // + // @return [Importer.Imports] the import record object + // + updateRecord(fields) { + Importer.Imports.update({ _id: this.importRecord._id }, { $set: fields }); + this.importRecord = Importer.Imports.findOne(this.importRecord._id); + + return this.importRecord; + } + + // Uploads the file to the storage. + // + // @param [Object] details an object with details about the upload. name, size, type, and rid + // @param [String] fileUrl url of the file to download/import + // @param [Object] user the Rocket.Chat user + // @param [Object] room the Rocket.Chat room + // @param [Date] timeStamp the timestamp the file was uploaded + // + uploadFile(details, fileUrl, user, room, timeStamp) { + this.logger.debug(`Uploading the file ${ details.name } from ${ fileUrl }.`); + const requestModule = /https/i.test(fileUrl) ? Importer.Base.https : Importer.Base.http; + + return requestModule.get(fileUrl, Meteor.bindEnvironment(function(stream) { + const fileStore = FileUpload.getStore('Uploads'); + fileStore.insert(details, stream, function(err, file) { + if (err) { + throw new Error(err); + } else { + const url = file.url.replace(Meteor.absoluteUrl(), '/'); + + const attachment = { + title: `File Uploaded: ${ file.name }`, + title_link: url + }; + + if (/^image\/.+/.test(file.type)) { + attachment.image_url = url; + attachment.image_type = file.type; + attachment.image_size = file.size; + attachment.image_dimensions = file.identify != null ? file.identify.size : undefined; + } + + if (/^audio\/.+/.test(file.type)) { + attachment.audio_url = url; + attachment.audio_type = file.type; + attachment.audio_size = file.size; + } + + if (/^video\/.+/.test(file.type)) { + attachment.video_url = url; + attachment.video_type = file.type; + attachment.video_size = file.size; + } + + const msg = { + rid: details.rid, + ts: timeStamp, + msg: '', + file: { + _id: file._id + }, + groupable: false, + attachments: [attachment] + }; + + if ((details.message_id != null) && (typeof details.message_id === 'string')) { + msg['_id'] = details.message_id; + } + + return RocketChat.sendMessage(user, msg, room, true); + } + }); + })); + } +}; diff --git a/packages/rocketchat-importer/server/classes/ImporterProgress.coffee b/packages/rocketchat-importer/server/classes/ImporterProgress.coffee deleted file mode 100644 index fcc5cd398390b2838700996d07ce1416a0f52403..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/server/classes/ImporterProgress.coffee +++ /dev/null @@ -1,9 +0,0 @@ -# Class for all the progress of the importers to use. -Importer.Progress = class Importer.Progress - # Constructs a new progress object. - # - # @param [String] name the name of the Importer - # - constructor: (@name) -> - @step = Importer.ProgressStep.NEW - @count = { completed: 0, total: 0 } diff --git a/packages/rocketchat-importer/server/classes/ImporterProgress.js b/packages/rocketchat-importer/server/classes/ImporterProgress.js new file mode 100644 index 0000000000000000000000000000000000000000..f1cf9e37067750752015c58cb54d1f7c7a6a2f74 --- /dev/null +++ b/packages/rocketchat-importer/server/classes/ImporterProgress.js @@ -0,0 +1,13 @@ +/* globals Importer */ +// Class for all the progress of the importers to use. +Importer.Progress = (Importer.Progress = class Progress { + // Constructs a new progress object. + // + // @param [String] name the name of the Importer + // + constructor(name) { + this.name = name; + this.step = Importer.ProgressStep.NEW; + this.count = { completed: 0, total: 0 }; + } +}); diff --git a/packages/rocketchat-importer/server/classes/ImporterProgressStep.coffee b/packages/rocketchat-importer/server/classes/ImporterProgressStep.coffee deleted file mode 100644 index e3972dd24db93e41ea535d76b552923abc2a39f2..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/server/classes/ImporterProgressStep.coffee +++ /dev/null @@ -1,16 +0,0 @@ -# "ENUM" of the import step, the value is the translation string -Importer.ProgressStep = Object.freeze - NEW: 'importer_new' - PREPARING_STARTED: 'importer_preparing_started' - PREPARING_USERS: 'importer_preparing_users' - PREPARING_CHANNELS: 'importer_preparing_channels' - PREPARING_MESSAGES: 'importer_preparing_messages' - USER_SELECTION: 'importer_user_selection' - IMPORTING_STARTED: 'importer_importing_started' - IMPORTING_USERS: 'importer_importing_users' - IMPORTING_CHANNELS: 'importer_importing_channels' - IMPORTING_MESSAGES: 'importer_importing_messages' - FINISHING: 'importer_finishing' - DONE: 'importer_done' - ERROR: 'importer_import_failed' - CANCELLED: 'importer_import_cancelled' diff --git a/packages/rocketchat-importer/server/classes/ImporterProgressStep.js b/packages/rocketchat-importer/server/classes/ImporterProgressStep.js new file mode 100644 index 0000000000000000000000000000000000000000..542383ce100c6e60f2103bf7f216533e64300b82 --- /dev/null +++ b/packages/rocketchat-importer/server/classes/ImporterProgressStep.js @@ -0,0 +1,18 @@ +/* globals Importer */ +// "ENUM" of the import step, the value is the translation string +Importer.ProgressStep = Object.freeze({ + NEW: 'importer_new', + PREPARING_STARTED: 'importer_preparing_started', + PREPARING_USERS: 'importer_preparing_users', + PREPARING_CHANNELS: 'importer_preparing_channels', + PREPARING_MESSAGES: 'importer_preparing_messages', + USER_SELECTION: 'importer_user_selection', + IMPORTING_STARTED: 'importer_importing_started', + IMPORTING_USERS: 'importer_importing_users', + IMPORTING_CHANNELS: 'importer_importing_channels', + IMPORTING_MESSAGES: 'importer_importing_messages', + FINISHING: 'importer_finishing', + DONE: 'importer_done', + ERROR: 'importer_import_failed', + CANCELLED: 'importer_import_cancelled' +}); diff --git a/packages/rocketchat-importer/server/classes/ImporterSelection.coffee b/packages/rocketchat-importer/server/classes/ImporterSelection.coffee deleted file mode 100644 index 314a0c44b29d2cd8f84e973cc3a76c286bbd3f7e..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/server/classes/ImporterSelection.coffee +++ /dev/null @@ -1,9 +0,0 @@ -# Class for all the selection of users and channels for the importers -Importer.Selection = class Importer.Selection - # Constructs a new importer selection object. - # - # @param [String] name the name of the Importer - # @param [Array] users the array of users - # @param [Array] channels the array of channels - # - constructor: (@name, @users, @channels) -> diff --git a/packages/rocketchat-importer/server/classes/ImporterSelection.js b/packages/rocketchat-importer/server/classes/ImporterSelection.js new file mode 100644 index 0000000000000000000000000000000000000000..070af9a70be6ddb71b4df2e1f0d6874a7ce50698 --- /dev/null +++ b/packages/rocketchat-importer/server/classes/ImporterSelection.js @@ -0,0 +1,15 @@ +/* globals Importer */ +// Class for all the selection of users and channels for the importers +Importer.Selection = (Importer.Selection = class Selection { + // Constructs a new importer selection object. + // + // @param [String] name the name of the Importer + // @param [Array] users the array of users + // @param [Array] channels the array of channels + // + constructor(name, users, channels) { + this.name = name; + this.users = users; + this.channels = channels; + } +}); diff --git a/packages/rocketchat-importer/server/classes/ImporterSelectionChannel.coffee b/packages/rocketchat-importer/server/classes/ImporterSelectionChannel.coffee deleted file mode 100644 index 0ff26ef39d540625c4c69c3928de47a70663a511..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/server/classes/ImporterSelectionChannel.coffee +++ /dev/null @@ -1,12 +0,0 @@ -# Class for the selection channels for ImporterSelection -Importer.SelectionChannel = class Importer.SelectionChannel - # Constructs a new selection channel. - # - # @param [String] channel_id the unique identifier of the channel - # @param [String] name the name of the channel - # @param [Boolean] is_archived whether the channel was archived or not - # @param [Boolean] do_import whether we will be importing the channel or not - # @param [Boolean] is_private whether the channel is private or public - # - constructor: (@channel_id, @name, @is_archived, @do_import, @is_private) -> - #TODO: Add some verification? diff --git a/packages/rocketchat-importer/server/classes/ImporterSelectionChannel.js b/packages/rocketchat-importer/server/classes/ImporterSelectionChannel.js new file mode 100644 index 0000000000000000000000000000000000000000..60262cb0e99c1eebd2ac830cb5bb0a99da2a4399 --- /dev/null +++ b/packages/rocketchat-importer/server/classes/ImporterSelectionChannel.js @@ -0,0 +1,20 @@ +/* globals Importer */ +// Class for the selection channels for ImporterSelection +Importer.SelectionChannel = (Importer.SelectionChannel = class SelectionChannel { + // Constructs a new selection channel. + // + // @param [String] channel_id the unique identifier of the channel + // @param [String] name the name of the channel + // @param [Boolean] is_archived whether the channel was archived or not + // @param [Boolean] do_import whether we will be importing the channel or not + // @param [Boolean] is_private whether the channel is private or public + // + constructor(channel_id, name, is_archived, do_import, is_private) { + this.channel_id = channel_id; + this.name = name; + this.is_archived = is_archived; + this.do_import = do_import; + this.is_private = is_private; + } +}); + //TODO: Add some verification? diff --git a/packages/rocketchat-importer/server/classes/ImporterSelectionUser.coffee b/packages/rocketchat-importer/server/classes/ImporterSelectionUser.coffee deleted file mode 100644 index 1d46e1df90af786d35d33714da85711452997ec1..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/server/classes/ImporterSelectionUser.coffee +++ /dev/null @@ -1,13 +0,0 @@ -# Class for the selection users for ImporterSelection -Importer.SelectionUser = class Importer.SelectionUser - # Constructs a new selection user. - # - # @param [String] user_id the unique user identifier - # @param [String] username the user's username - # @param [String] email the user's email - # @param [Boolean] is_deleted whether the user was deleted or not - # @param [Boolean] is_bot whether the user is a bot or not - # @param [Boolean] do_import whether we are going to import this user or not - # - constructor: (@user_id, @username, @email, @is_deleted, @is_bot, @do_import) -> - #TODO: Add some verification? diff --git a/packages/rocketchat-importer/server/classes/ImporterSelectionUser.js b/packages/rocketchat-importer/server/classes/ImporterSelectionUser.js new file mode 100644 index 0000000000000000000000000000000000000000..1e1afffda545caea21260669648fce5bf3eb88cc --- /dev/null +++ b/packages/rocketchat-importer/server/classes/ImporterSelectionUser.js @@ -0,0 +1,22 @@ +/* globals Importer */ +// Class for the selection users for ImporterSelection +Importer.SelectionUser = (Importer.SelectionUser = class SelectionUser { + // Constructs a new selection user. + // + // @param [String] user_id the unique user identifier + // @param [String] username the user's username + // @param [String] email the user's email + // @param [Boolean] is_deleted whether the user was deleted or not + // @param [Boolean] is_bot whether the user is a bot or not + // @param [Boolean] do_import whether we are going to import this user or not + // + constructor(user_id, username, email, is_deleted, is_bot, do_import) { + this.user_id = user_id; + this.username = username; + this.email = email; + this.is_deleted = is_deleted; + this.is_bot = is_bot; + this.do_import = do_import; + } +}); + //TODO: Add some verification? diff --git a/packages/rocketchat-importer/server/methods/getImportProgress.coffee b/packages/rocketchat-importer/server/methods/getImportProgress.coffee deleted file mode 100644 index 89ba39919b0ed6694888a514e2e38085d9e175ae..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/server/methods/getImportProgress.coffee +++ /dev/null @@ -1,12 +0,0 @@ -Meteor.methods - getImportProgress: (name) -> - if not Meteor.userId() - throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'getImportProgress' } - - if not RocketChat.authz.hasPermission(Meteor.userId(), 'run-import') - throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter'}); - - if Importer.Importers[name]? - return Importer.Importers[name].importerInstance?.getProgress() - else - throw new Meteor.Error 'error-importer-not-defined', 'The importer was not defined correctly, it is missing the Import class.', { method: 'getImportProgress' } diff --git a/packages/rocketchat-importer/server/methods/getImportProgress.js b/packages/rocketchat-importer/server/methods/getImportProgress.js new file mode 100644 index 0000000000000000000000000000000000000000..f3ca0c27935180db2acc6008c89a33a572119dc1 --- /dev/null +++ b/packages/rocketchat-importer/server/methods/getImportProgress.js @@ -0,0 +1,17 @@ +/* globals Importer */ +Meteor.methods({ + getImportProgress(name) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getImportProgress' }); + } + + if (!RocketChat.authz.hasPermission(Meteor.userId(), 'run-import')) { + throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter'}); + } + + if (Importer.Importers[name] != null) { + return (Importer.Importers[name].importerInstance != null ? Importer.Importers[name].importerInstance.getProgress() : undefined); + } else { + throw new Meteor.Error('error-importer-not-defined', 'The importer was not defined correctly, it is missing the Import class.', { method: 'getImportProgress' }); + } + }}); diff --git a/packages/rocketchat-importer/server/methods/getSelectionData.coffee b/packages/rocketchat-importer/server/methods/getSelectionData.coffee deleted file mode 100644 index 8b4780c894a952d018c88d88c03f41a4e1d0e63f..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/server/methods/getSelectionData.coffee +++ /dev/null @@ -1,17 +0,0 @@ -Meteor.methods - getSelectionData: (name) -> - if not Meteor.userId() - throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'getSelectionData' } - - if not RocketChat.authz.hasPermission(Meteor.userId(), 'run-import') - throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter'}); - - if Importer.Importers[name]?.importerInstance? - progress = Importer.Importers[name].importerInstance.getProgress() - switch progress.step - when Importer.ProgressStep.USER_SELECTION - return Importer.Importers[name].importerInstance.getSelection() - else - return false - else - throw new Meteor.Error 'error-importer-not-defined', 'The importer was not defined correctly, it is missing the Import class.', { method: 'getSelectionData' } diff --git a/packages/rocketchat-importer/server/methods/getSelectionData.js b/packages/rocketchat-importer/server/methods/getSelectionData.js new file mode 100644 index 0000000000000000000000000000000000000000..a8e2a2f57c11df12d34cc131695cfbfa4c3bd19b --- /dev/null +++ b/packages/rocketchat-importer/server/methods/getSelectionData.js @@ -0,0 +1,23 @@ +/* globals Importer */ +Meteor.methods({ + getSelectionData(name) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getSelectionData' }); + } + + if (!RocketChat.authz.hasPermission(Meteor.userId(), 'run-import')) { + throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter'}); + } + + if ((Importer.Importers[name] != null ? Importer.Importers[name].importerInstance : undefined) != null) { + const progress = Importer.Importers[name].importerInstance.getProgress(); + switch (progress.step) { + case Importer.ProgressStep.USER_SELECTION: + return Importer.Importers[name].importerInstance.getSelection(); + default: + return false; + } + } else { + throw new Meteor.Error('error-importer-not-defined', 'The importer was not defined correctly, it is missing the Import class.', { method: 'getSelectionData' }); + } + }}); diff --git a/packages/rocketchat-importer/server/methods/restartImport.coffee b/packages/rocketchat-importer/server/methods/restartImport.coffee deleted file mode 100644 index aff7a019b43c71eaef9194df247bd3c45b72ea8e..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/server/methods/restartImport.coffee +++ /dev/null @@ -1,17 +0,0 @@ -Meteor.methods - restartImport: (name) -> - if not Meteor.userId() - throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'restartImport' } - - if not RocketChat.authz.hasPermission(Meteor.userId(), 'run-import') - throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter'}); - - if Importer.Importers[name]? - importer = Importer.Importers[name] - importer.importerInstance.updateProgress Importer.ProgressStep.CANCELLED - importer.importerInstance.updateRecord { valid: false } - importer.importerInstance = undefined - importer.importerInstance = new importer.importer importer.name, importer.description, importer.mimeType - return importer.importerInstance.getProgress() - else - throw new Meteor.Error 'error-importer-not-defined', 'The importer was not defined correctly, it is missing the Import class.', { method: 'restartImport' } diff --git a/packages/rocketchat-importer/server/methods/restartImport.js b/packages/rocketchat-importer/server/methods/restartImport.js new file mode 100644 index 0000000000000000000000000000000000000000..c4a69df03ddb2387731f002636d71d192947c094 --- /dev/null +++ b/packages/rocketchat-importer/server/methods/restartImport.js @@ -0,0 +1,22 @@ +/* globals Importer*/ +Meteor.methods({ + restartImport(name) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'restartImport' }); + } + + if (!RocketChat.authz.hasPermission(Meteor.userId(), 'run-import')) { + throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter'}); + } + + if (Importer.Importers[name] != null) { + const importer = Importer.Importers[name]; + importer.importerInstance.updateProgress(Importer.ProgressStep.CANCELLED); + importer.importerInstance.updateRecord({ valid: false }); + importer.importerInstance = undefined; + importer.importerInstance = new importer.importer(importer.name, importer.description, importer.mimeType); // eslint-disable-line new-cap + return importer.importerInstance.getProgress(); + } else { + throw new Meteor.Error('error-importer-not-defined', 'The importer was not defined correctly, it is missing the Import class.', { method: 'restartImport' }); + } + }}); diff --git a/packages/rocketchat-importer/server/methods/setupImporter.coffee b/packages/rocketchat-importer/server/methods/setupImporter.coffee deleted file mode 100644 index 5ae12ab836c805dd1be01ee65a7051cf5e642308..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/server/methods/setupImporter.coffee +++ /dev/null @@ -1,19 +0,0 @@ -Meteor.methods - setupImporter: (name) -> - if not Meteor.userId() - throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'setupImporter' } - - if not RocketChat.authz.hasPermission(Meteor.userId(), 'run-import') - throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter'}); - - if Importer.Importers[name]?.importer? - importer = Importer.Importers[name] - # If they currently have progress, get it and return the progress. - if importer.importerInstance - return importer.importerInstance.getProgress() - else - importer.importerInstance = new importer.importer importer.name, importer.description, importer.mimeType - return importer.importerInstance.getProgress() - else - console.warn "Tried to setup #{name} as an importer." - throw new Meteor.Error 'error-importer-not-defined', 'The importer was not defined correctly, it is missing the Import class.', { method: 'setupImporter' } diff --git a/packages/rocketchat-importer/server/methods/setupImporter.js b/packages/rocketchat-importer/server/methods/setupImporter.js new file mode 100644 index 0000000000000000000000000000000000000000..38cd360efa1ff7c7d477516fa038c63e268e99b9 --- /dev/null +++ b/packages/rocketchat-importer/server/methods/setupImporter.js @@ -0,0 +1,25 @@ +/* globals Importer */ +Meteor.methods({ + setupImporter(name) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setupImporter' }); + } + + if (!RocketChat.authz.hasPermission(Meteor.userId(), 'run-import')) { + throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter'}); + } + + if ((Importer.Importers[name] != null ? Importer.Importers[name].importer : undefined) != null) { + const importer = Importer.Importers[name]; + // If they currently have progress, get it and return the progress. + if (importer.importerInstance) { + return importer.importerInstance.getProgress(); + } else { + importer.importerInstance = new importer.importer(importer.name, importer.description, importer.mimeType); //eslint-disable-line new-cap + return importer.importerInstance.getProgress(); + } + } else { + console.warn(`Tried to setup ${ name } as an importer.`); + throw new Meteor.Error('error-importer-not-defined', 'The importer was not defined correctly, it is missing the Import class.', { method: 'setupImporter' }); + } + }}); diff --git a/packages/rocketchat-importer/server/methods/startImport.coffee b/packages/rocketchat-importer/server/methods/startImport.coffee deleted file mode 100644 index b119f6eb8a86c989ef752b4e38464de5c96c2a23..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/server/methods/startImport.coffee +++ /dev/null @@ -1,19 +0,0 @@ -Meteor.methods - startImport: (name, input) -> - # Takes name and object with users / channels selected to import - if not Meteor.userId() - throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'startImport' } - - if not RocketChat.authz.hasPermission(Meteor.userId(), 'run-import') - throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter'}); - - if Importer.Importers[name]?.importerInstance? - usersSelection = input.users.map (user) -> - return new Importer.SelectionUser user.user_id, user.username, user.email, user.is_deleted, user.is_bot, user.do_import - channelsSelection = input.channels.map (channel) -> - return new Importer.SelectionChannel channel.channel_id, channel.name, channel.is_archived, channel.do_import - - selection = new Importer.Selection name, usersSelection, channelsSelection - Importer.Importers[name].importerInstance.startImport selection - else - throw new Meteor.Error 'error-importer-not-defined', 'The importer was not defined correctly, it is missing the Import class.', { method: 'startImport' } diff --git a/packages/rocketchat-importer/server/methods/startImport.js b/packages/rocketchat-importer/server/methods/startImport.js new file mode 100644 index 0000000000000000000000000000000000000000..352193e28a2fb20c2e3afa0049d6e8bb64a5a347 --- /dev/null +++ b/packages/rocketchat-importer/server/methods/startImport.js @@ -0,0 +1,22 @@ +/* globals Importer */ +Meteor.methods({ + startImport(name, input) { + // Takes name and object with users / channels selected to import + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'startImport' }); + } + + if (!RocketChat.authz.hasPermission(Meteor.userId(), 'run-import')) { + throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter'}); + } + + if (Importer.Importers[name] && Importer.Importers[name].importerInstance) { + const usersSelection = input.users.map(user => new Importer.SelectionUser(user.user_id, user.username, user.email, user.is_deleted, user.is_bot, user.do_import)); + const channelsSelection = input.channels.map(channel => new Importer.SelectionChannel(channel.channel_id, channel.name, channel.is_archived, channel.do_import)); + + const selection = new Importer.Selection(name, usersSelection, channelsSelection); + return Importer.Importers[name].importerInstance.startImport(selection); + } else { + throw new Meteor.Error('error-importer-not-defined', 'The importer was not defined correctly, it is missing the Import class.', { method: 'startImport' }); + } + }}); diff --git a/packages/rocketchat-importer/server/models/Imports.coffee b/packages/rocketchat-importer/server/models/Imports.coffee deleted file mode 100644 index 2e966dc83809dc2b418b7bda5b27dacd8bdd5f00..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/server/models/Imports.coffee +++ /dev/null @@ -1,3 +0,0 @@ -Importer.Imports = new class Importer.Imports extends RocketChat.models._Base - constructor: -> - super('import') diff --git a/packages/rocketchat-importer/server/models/Imports.js b/packages/rocketchat-importer/server/models/Imports.js new file mode 100644 index 0000000000000000000000000000000000000000..f68dc3e1d246bc9e8df78a7a9b70d14bd3589581 --- /dev/null +++ b/packages/rocketchat-importer/server/models/Imports.js @@ -0,0 +1,6 @@ +/* globals Importer */ +Importer.Imports = new (Importer.Imports = class Imports extends RocketChat.models._Base { + constructor() { + super('import'); + } +}); diff --git a/packages/rocketchat-importer/server/models/RawImports.coffee b/packages/rocketchat-importer/server/models/RawImports.coffee deleted file mode 100644 index 1495423ea4ac483683e4da2e83033485c0c7f414..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/server/models/RawImports.coffee +++ /dev/null @@ -1,3 +0,0 @@ -Importer.RawImports = new class Importer.RawImports extends RocketChat.models._Base - constructor: -> - super('raw_imports') diff --git a/packages/rocketchat-importer/server/models/RawImports.js b/packages/rocketchat-importer/server/models/RawImports.js new file mode 100644 index 0000000000000000000000000000000000000000..70eae7f76c061187209502341eac76fc6848ae9f --- /dev/null +++ b/packages/rocketchat-importer/server/models/RawImports.js @@ -0,0 +1,6 @@ +/* globals Importer */ +Importer.RawImports = new (Importer.RawImports = class RawImports extends RocketChat.models._Base { + constructor() { + super('raw_imports'); + } +}); diff --git a/packages/rocketchat-importer/server/startup/setImportsToInvalid.coffee b/packages/rocketchat-importer/server/startup/setImportsToInvalid.coffee deleted file mode 100644 index fe4e5056f1eb59a55d41eeccbfe9c4fd12d83ef6..0000000000000000000000000000000000000000 --- a/packages/rocketchat-importer/server/startup/setImportsToInvalid.coffee +++ /dev/null @@ -1,8 +0,0 @@ -Meteor.startup -> - # Make sure all imports are marked as invalid, data clean up since you can't - # restart an import at the moment. - Importer.Imports.update { valid: { $ne: false } }, { $set: { valid: false } }, { multi: true } - - # Clean up all the raw import data, since you can't restart an import at the moment - Importer.Imports.find({ valid: { $ne: true }}).forEach (item) -> - Importer.RawImports.remove { 'import': item._id, 'importer': item.type } diff --git a/packages/rocketchat-importer/server/startup/setImportsToInvalid.js b/packages/rocketchat-importer/server/startup/setImportsToInvalid.js new file mode 100644 index 0000000000000000000000000000000000000000..2424ef40a007e019b12d81e2519323f5a1d546d9 --- /dev/null +++ b/packages/rocketchat-importer/server/startup/setImportsToInvalid.js @@ -0,0 +1,9 @@ +/* globals Importer */ +Meteor.startup(function() { + // Make sure all imports are marked as invalid, data clean up since you can't + // restart an import at the moment. + Importer.Imports.update({ valid: { $ne: false } }, { $set: { valid: false } }, { multi: true }); + + // Clean up all the raw import data, since you can't restart an import at the moment + return Importer.Imports.find({ valid: { $ne: true }}).forEach(item => Importer.RawImports.remove({ 'import': item._id, 'importer': item.type })); +}); diff --git a/packages/rocketchat-integrations/client/views/integrationsIncoming.html b/packages/rocketchat-integrations/client/views/integrationsIncoming.html index 68955e653b5d4bb836e8350e1762d611f79de1eb..1bc5f6e0025ac930c9b5a17adca454eb39fe05d2 100644 --- a/packages/rocketchat-integrations/client/views/integrationsIncoming.html +++ b/packages/rocketchat-integrations/client/views/integrationsIncoming.html @@ -30,11 +30,7 @@
- {{#if data.token}} - - {{else}} - - {{/if}} +
{{_ "Choose_the_username_that_this_integration_will_post_as"}}
{{_ "Should_exists_a_user_with_this_username"}}
diff --git a/packages/rocketchat-integrations/client/views/integrationsIncoming.js b/packages/rocketchat-integrations/client/views/integrationsIncoming.js index f255ea62ac2c2a4df0b0845a774b8a93252c5fe1..38e4ebfcc7a3a180e6606c12f175a797206ab90b 100644 --- a/packages/rocketchat-integrations/client/views/integrationsIncoming.js +++ b/packages/rocketchat-integrations/client/views/integrationsIncoming.js @@ -222,6 +222,7 @@ Template.integrationsIncoming.events({ const integration = { enabled: enabled === '1', channel, + username, alias: alias !== '' ? alias : undefined, emoji: emoji !== '' ? emoji : undefined, avatar: avatar !== '' ? avatar : undefined, @@ -240,8 +241,6 @@ Template.integrationsIncoming.events({ toastr.success(TAPi18n.__('Integration_updated')); }); } else { - integration.username = username; - Meteor.call('addIncomingIntegration', integration, (err, data) => { if (err) { return handleError(err); diff --git a/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.js b/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.js index 9a4c4a878e4a56bb169f32b92accb08686f75535..a888484d05c29e38f0e3319ce9688f47e58bfc25 100644 --- a/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.js +++ b/packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.js @@ -76,6 +76,11 @@ Meteor.methods({ } const user = RocketChat.models.Users.findOne({ username: currentIntegration.username }); + + if (!user || !user._id) { + throw new Meteor.Error('error-invalid-post-as-user', 'Invalid Post As User', { method: 'updateIncomingIntegration' }); + } + RocketChat.models.Roles.addUserRoles(user._id, 'bot'); RocketChat.models.Integrations.update(integrationId, { diff --git a/packages/rocketchat-internal-hubot/hubot.coffee b/packages/rocketchat-internal-hubot/hubot.coffee deleted file mode 100644 index b3c2b657ffbce94f5626c04559dfd8a283a01ac1..0000000000000000000000000000000000000000 --- a/packages/rocketchat-internal-hubot/hubot.coffee +++ /dev/null @@ -1,208 +0,0 @@ -CoffeeScript = Npm.require('coffee-script') -CoffeeScript.register() - -Hubot = Npm.require('hubot') - -fs = Npm.require('fs') -path = Npm.require('path') - -# Start a hubot, connected to our chat room. -# 'use strict' - -# Log messages? -DEBUG = false - -# Monkey-patch Hubot to support private messages -Hubot.Response::priv = (strings...) -> - @robot.adapter.priv @envelope, strings... - -# More monkey-patching -Hubot.Robot::loadAdapter = -> # disable - -# grrrr, Meteor.bindEnvironment doesn't preserve `this` apparently -bind = (f) -> - g = Meteor.bindEnvironment (self, args...) -> f.apply(self, args) - (args...) -> g @, args... - -class Robot extends Hubot.Robot - constructor: (args...) -> - super args... - @hear = bind @hear - @respond = bind @respond - @enter = bind @enter - @leave = bind @leave - @topic = bind @topic - @error = bind @error - @catchAll = bind @catchAll - @user = Meteor.users.findOne {username: @name}, fields: username: 1 - loadAdapter: -> false - hear: (regex, callback) -> super regex, Meteor.bindEnvironment callback - respond: (regex, callback) -> super regex, Meteor.bindEnvironment callback - enter: (callback) -> super Meteor.bindEnvironment(callback) - leave: (callback) -> super Meteor.bindEnvironment(callback) - topic: (callback) -> super Meteor.bindEnvironment(callback) - error: (callback) -> super Meteor.bindEnvironment(callback) - catchAll: (callback) -> super Meteor.bindEnvironment(callback) - -class RocketChatAdapter extends Hubot.Adapter - # Public: Raw method for sending data back to the chat source. Extend this. - # - # envelope - A Object with message, room and user details. - # strings - One or more Strings for each message to send. - # - # Returns nothing. - send: (envelope, strings...) -> - console.log 'ROCKETCHATADAPTER -> send'.blue if DEBUG - # console.log envelope, strings - sendHelper @robot, envelope, strings, (string) => - console.log "send #{envelope.room}: #{string} (#{envelope.user.id})" if DEBUG - RocketChat.sendMessage InternalHubot.user, { msg: string }, { _id: envelope.room } - - # Public: Raw method for sending emote data back to the chat source. - # - # envelope - A Object with message, room and user details. - # strings - One or more Strings for each message to send. - # - # Returns nothing. - emote: (envelope, strings...) -> - console.log 'ROCKETCHATADAPTER -> emote'.blue if DEBUG - sendHelper @robot, envelope, strings, (string) => - console.log "emote #{envelope.rid}: #{string} (#{envelope.u.username})" if DEBUG - return @priv envelope, "*** #{string} ***" if envelope.message.private - Meteor.call "sendMessage", - msg: string - rid: envelope.rid - action: true - - # Priv: our extension -- send a PM to user - priv: (envelope, strings...) -> - console.log 'ROCKETCHATADAPTER -> priv'.blue if DEBUG - sendHelper @robot, envelope, strings, (string) -> - console.log "priv #{envelope.room}: #{string} (#{envelope.user.id})" if DEBUG - Meteor.call "sendMessage", - u: - username: "rocketbot" - to: "#{envelope.user.id}" - msg: string - rid: envelope.room - - # Public: Raw method for building a reply and sending it back to the chat - # source. Extend this. - # - # envelope - A Object with message, room and user details. - # strings - One or more Strings for each reply to send. - # - # Returns nothing. - reply: (envelope, strings...) -> - console.log 'ROCKETCHATADAPTER -> reply'.blue if DEBUG - if envelope.message.private - @priv envelope, strings... - else - @send envelope, strings.map((str) -> "#{envelope.user.name}: #{str}")... - - # Public: Raw method for setting a topic on the chat source. Extend this. - # - # envelope - A Object with message, room and user details. - # strings - One more more Strings to set as the topic. - # - # Returns nothing. - topic: (envelope, strings...) -> - console.log 'ROCKETCHATADAPTER -> topic'.blue if DEBUG - - # Public: Raw method for playing a sound in the chat source. Extend this. - # - # envelope - A Object with message, room and user details. - # strings - One or more strings for each play message to send. - # - # Returns nothing - play: (envelope, strings...) -> - console.log 'ROCKETCHATADAPTER -> play'.blue if DEBUG - - # Public: Raw method for invoking the bot to run. Extend this. - # - # Returns nothing. - run: -> - console.log 'ROCKETCHATADAPTER -> run'.blue if DEBUG - @robot.emit 'connected' - @robot.brain.mergeData {} - # @robot.brain.emit 'loaded' - - # Public: Raw method for shutting the bot down. Extend this. - # - # Returns nothing. - close: -> - console.log 'ROCKETCHATADAPTER -> close'.blue if DEBUG - -class InternalHubotReceiver - constructor: (message) -> - console.log message if DEBUG - if message.u.username isnt InternalHubot.name - room = RocketChat.models.Rooms.findOneById message.rid - - if room.t is 'c' - InternalHubotUser = new Hubot.User(message.u.username, room: message.rid) - InternalHubotTextMessage = new Hubot.TextMessage(InternalHubotUser, message.msg, message._id) - InternalHubot.adapter.receive InternalHubotTextMessage - return message - -class HubotScripts - constructor: (robot) -> - modulesToLoad = [ - 'hubot-help/src/help.coffee' - ] - - for modulePath in modulesToLoad - try - Npm.require(modulePath)(robot) - robot.parseHelp __meteor_bootstrap__.serverDir+'/npm/node_modules/meteor/rocketchat_internal-hubot/node_modules/'+modulePath - console.log "Loaded #{modulePath}".green - catch e - console.log "can't load #{modulePath}".red - console.log e - - scriptsToLoad = RocketChat.settings.get('InternalHubot_ScriptsToLoad').split(',') or [] - - for scriptFile in scriptsToLoad - try - scriptFile = s.trim(scriptFile) - - Npm.require('hubot-scripts/src/scripts/'+scriptFile)(robot) - # robot.loadFile __meteor_bootstrap__.serverDir+'/npm/node_modules/meteor/rocketchat_internal-hubot/node_modules/hubot-scripts/src/scripts', scriptFile - robot.parseHelp __meteor_bootstrap__.serverDir+'/npm/node_modules/meteor/rocketchat_internal-hubot/node_modules/hubot-scripts/src/scripts/'+scriptFile - console.log "Loaded #{scriptFile}".green - catch e - console.log "can't load #{scriptFile}".red - console.log e - -sendHelper = Meteor.bindEnvironment (robot, envelope, strings, map) -> - while strings.length > 0 - string = strings.shift() - if typeof(string) == 'function' - string() - else - try - map(string) - catch err - console.error "Hubot error: #{err}" if DEBUG - robot.logger.error "RocketChat send error: #{err}" - -InternalHubot = {} - -init = _.debounce Meteor.bindEnvironment( => - if RocketChat.settings.get 'InternalHubot_Enabled' - InternalHubot = new Robot null, null, false, RocketChat.settings.get 'InternalHubot_Username' - InternalHubot.alias = 'bot' - InternalHubot.adapter = new RocketChatAdapter InternalHubot - HubotScripts(InternalHubot) - InternalHubot.run() - RocketChat.callbacks.add 'afterSaveMessage', InternalHubotReceiver, RocketChat.callbacks.priority.LOW, 'InternalHubot' - else - InternalHubot = {} - RocketChat.callbacks.remove 'afterSaveMessage', 'InternalHubot' -), 1000 - -Meteor.startup -> - init() - RocketChat.models.Settings.findByIds([ 'InternalHubot_Username', 'InternalHubot_Enabled', 'InternalHubot_ScriptsToLoad']).observe - changed: -> - init() diff --git a/packages/rocketchat-internal-hubot/hubot.js b/packages/rocketchat-internal-hubot/hubot.js new file mode 100644 index 0000000000000000000000000000000000000000..102222b015fc723370d745b50b6a4d364595ef6d --- /dev/null +++ b/packages/rocketchat-internal-hubot/hubot.js @@ -0,0 +1,247 @@ +/* globals __meteor_bootstrap__ */ +const CoffeeScript = Npm.require('coffee-script'); +CoffeeScript.register(); +const Hubot = Npm.require('hubot'); +// Start a hubot, connected to our chat room. +// 'use strict' +// Log messages? +const DEBUG = false; + +let InternalHubot = {}; + +const sendHelper = Meteor.bindEnvironment((robot, envelope, strings, map) =>{ + while (strings.length > 0) { + const string = strings.shift(); + if (typeof(string) === 'function') { + string(); + } else { + try { + map(string); + } catch (err) { + if (DEBUG) { console.error(`Hubot error: ${ err }`); } + robot.logger.error(`RocketChat send error: ${ err }`); + } + } + } +}); + +// Monkey-patch Hubot to support private messages +Hubot.Response.prototype.priv = (...strings) => this.robot.adapter.priv(this.envelope, ...strings); + +// More monkey-patching +Hubot.Robot.prototype.loadAdapter = () => {}; // disable + +// grrrr, Meteor.bindEnvironment doesn't preserve `this` apparently +const bind = function(f) { + const g = Meteor.bindEnvironment((self, ...args) => f.apply(self, args)); + return function(...args) { return g(this, ...Array.from(args)); }; +}; + +class Robot extends Hubot.Robot { + constructor(...args) { + super(...(args || [])); + this.hear = bind(this.hear); + this.respond = bind(this.respond); + this.enter = bind(this.enter); + this.leave = bind(this.leave); + this.topic = bind(this.topic); + this.error = bind(this.error); + this.catchAll = bind(this.catchAll); + this.user = Meteor.users.findOne({username: this.name}, {fields: {username: 1}}); + } + loadAdapter() { return false; } + hear(regex, callback) { return super.hear(regex, Meteor.bindEnvironment(callback)); } + respond(regex, callback) { return super.respond(regex, Meteor.bindEnvironment(callback)); } + enter(callback) { return super.enter(Meteor.bindEnvironment(callback)); } + leave(callback) { return super.leave(Meteor.bindEnvironment(callback)); } + topic(callback) { return super.topic(Meteor.bindEnvironment(callback)); } + error(callback) { return super.error(Meteor.bindEnvironment(callback)); } + catchAll(callback) { return super.catchAll(Meteor.bindEnvironment(callback)); } +} + +class RocketChatAdapter extends Hubot.Adapter { + // Public: Raw method for sending data back to the chat source. Extend this. + // + // envelope - A Object with message, room and user details. + // strings - One or more Strings for each message to send. + // + // Returns nothing. + send(envelope, ...strings) { + if (DEBUG) { console.log('ROCKETCHATADAPTER -> send'.blue); } + // console.log envelope, strings + return sendHelper(this.robot, envelope, strings, string => { + if (DEBUG) { console.log(`send ${ envelope.room }: ${ string } (${ envelope.user.id })`); } + return RocketChat.sendMessage(InternalHubot.user, { msg: string }, { _id: envelope.room }); + }); + } + + // Public: Raw method for sending emote data back to the chat source. + // + // envelope - A Object with message, room and user details. + // strings - One or more Strings for each message to send. + // + // Returns nothing. + emote(envelope, ...strings) { + if (DEBUG) { console.log('ROCKETCHATADAPTER -> emote'.blue); } + return sendHelper(this.robot, envelope, strings, string => { + if (DEBUG) { console.log(`emote ${ envelope.rid }: ${ string } (${ envelope.u.username })`); } + if (envelope.message.private) { return this.priv(envelope, `*** ${ string } ***`); } + return Meteor.call('sendMessage', { + msg: string, + rid: envelope.rid, + action: true + } + ); + }); + } + + // Priv: our extension -- send a PM to user + priv(envelope, ...strings) { + if (DEBUG) { console.log('ROCKETCHATADAPTER -> priv'.blue); } + return sendHelper(this.robot, envelope, strings, function(string) { + if (DEBUG) { console.log(`priv ${ envelope.room }: ${ string } (${ envelope.user.id })`); } + return Meteor.call('sendMessage', { + u: { + username: 'rocketbot' + }, + to: `${ envelope.user.id }`, + msg: string, + rid: envelope.room + }); + }); + } + + // Public: Raw method for building a reply and sending it back to the chat + // source. Extend this. + // + // envelope - A Object with message, room and user details. + // strings - One or more Strings for each reply to send. + // + // Returns nothing. + reply(envelope, ...strings) { + if (DEBUG) { console.log('ROCKETCHATADAPTER -> reply'.blue); } + if (envelope.message.private) { + return this.priv(envelope, ...strings); + } else { + return this.send(envelope, ...strings.map(str => `${ envelope.user.name }: ${ str }`)); + } + } + + // Public: Raw method for setting a topic on the chat source. Extend this. + // + // envelope - A Object with message, room and user details. + // strings - One more more Strings to set as the topic. + // + // Returns nothing. + topic(/*envelope, ...strings*/) { + if (DEBUG) { return console.log('ROCKETCHATADAPTER -> topic'.blue); } + } + + // Public: Raw method for playing a sound in the chat source. Extend this. + // + // envelope - A Object with message, room and user details. + // strings - One or more strings for each play message to send. + // + // Returns nothing + play(/*envelope, ...strings*/) { + if (DEBUG) { return console.log('ROCKETCHATADAPTER -> play'.blue); } + } + + // Public: Raw method for invoking the bot to run. Extend this. + // + // Returns nothing. + run() { + if (DEBUG) { console.log('ROCKETCHATADAPTER -> run'.blue); } + this.robot.emit('connected'); + return this.robot.brain.mergeData({}); + } + // @robot.brain.emit 'loaded' + + // Public: Raw method for shutting the bot down. Extend this. + // + // Returns nothing. + close() { + if (DEBUG) { return console.log('ROCKETCHATADAPTER -> close'.blue); } + } +} + +const InternalHubotReceiver = (message) => { + if (DEBUG) { console.log(message); } + if (message.u.username !== InternalHubot.name) { + const room = RocketChat.models.Rooms.findOneById(message.rid); + + if (room.t === 'c') { + const InternalHubotUser = new Hubot.User(message.u.username, {room: message.rid}); + const InternalHubotTextMessage = new Hubot.TextMessage(InternalHubotUser, message.msg, message._id); + InternalHubot.adapter.receive(InternalHubotTextMessage); + } + } + return message; +}; + +class HubotScripts { + constructor(robot) { + const modulesToLoad = [ + 'hubot-help/src/help.coffee' + ]; + const customPath = RocketChat.settings.get('InternalHubot_PathToLoadCustomScripts'); + HubotScripts.load(`${ __meteor_bootstrap__.serverDir }/npm/node_modules/meteor/rocketchat_internal-hubot/node_modules/`, modulesToLoad, robot); + HubotScripts.load(customPath, RocketChat.settings.get('InternalHubot_ScriptsToLoad').split(',') || [], robot); + } + + static load(path, scriptsToLoad, robot) { + if (!path || !scriptsToLoad) { + return; + } + scriptsToLoad.forEach(scriptFile => { + try { + scriptFile = s.trim(scriptFile); + if (scriptFile === '') { + return; + } + // delete require.cache[require.resolve(path+scriptFile)]; + const fn = Npm.require(path + scriptFile); + if (typeof(fn) === 'function') { + fn(robot); + } else { + fn.default(robot); + } + robot.parseHelp(path + scriptFile); + console.log(`Loaded ${ scriptFile }`.green); + } catch (e) { + console.log(`Can't load ${ scriptFile }`.red); + console.log(e); + } + }); + } +} + +const init = _.debounce(Meteor.bindEnvironment(() => { + if (RocketChat.settings.get('InternalHubot_Enabled')) { + InternalHubot = new Robot(null, null, false, RocketChat.settings.get('InternalHubot_Username')); + InternalHubot.alias = 'bot'; + InternalHubot.adapter = new RocketChatAdapter(InternalHubot); + new HubotScripts(InternalHubot); + InternalHubot.run(); + return RocketChat.callbacks.add('afterSaveMessage', InternalHubotReceiver, RocketChat.callbacks.priority.LOW, 'InternalHubot'); + } else { + InternalHubot = {}; + return RocketChat.callbacks.remove('afterSaveMessage', 'InternalHubot'); + } +}), 1000); + +Meteor.startup(function() { + init(); + RocketChat.models.Settings.findByIds([ 'InternalHubot_Username', 'InternalHubot_Enabled', 'InternalHubot_ScriptsToLoad', 'InternalHubot_PathToLoadCustomScripts']).observe({ + changed() { + return init(); + } + }); + // TODO useful when we have the ability to invalidate `require` cache + // RocketChat.RateLimiter.limitMethod('reloadInternalHubot', 1, 5000, { + // userId(/*userId*/) { return true; } + // }); + // Meteor.methods({ + // reloadInternalHubot: () => init() + // }); +}); diff --git a/packages/rocketchat-internal-hubot/package.js b/packages/rocketchat-internal-hubot/package.js index facdc64f4ba33608409373777aa6bdd044884cad..4d1295ee20720583139d972aff9bd6b88150b567 100644 --- a/packages/rocketchat-internal-hubot/package.js +++ b/packages/rocketchat-internal-hubot/package.js @@ -8,7 +8,6 @@ Package.describe({ Package.onUse(function(api) { api.use([ 'ecmascript', - 'coffeescript', 'underscore', 'tracker', 'rocketchat:lib' @@ -18,8 +17,8 @@ Package.onUse(function(api) { api.use('templating', 'client'); api.addFiles([ - 'hubot.coffee', - 'settings.coffee' + 'hubot.js', + 'settings.js' ], ['server']); api.export('Hubot', ['server']); @@ -33,6 +32,5 @@ Package.onUse(function(api) { Npm.depends({ 'coffee-script': '1.10.0', 'hubot': '2.19.0', - 'hubot-scripts': '2.17.1', 'hubot-help': '0.2.0' }); diff --git a/packages/rocketchat-internal-hubot/scripts/maps.coffee b/packages/rocketchat-internal-hubot/scripts/maps.coffee deleted file mode 100644 index ed22ec1e40d255e483a57d73973ca63799077d15..0000000000000000000000000000000000000000 --- a/packages/rocketchat-internal-hubot/scripts/maps.coffee +++ /dev/null @@ -1,25 +0,0 @@ -# Description: -# Interacts with the Google Maps API. -# -# Commands: -# hubot map me - Returns a map view of the area returned by `query`. - -module.exports = (robot) -> - - robot.respond /(?:(satellite|terrain|hybrid)[- ])?map me (.+)/i, (msg) -> - mapType = msg.match[1] or "roadmap" - location = msg.match[2] - mapUrl = "http://maps.google.com/maps/api/staticmap?markers=" + - escape(location) + - "&size=400x400&maptype=" + - mapType + - "&sensor=false" + - "&format=png" # So campfire knows it's an image - url = "http://maps.google.com/maps?q=" + - escape(location) + - "&hl=en&sll=37.0625,-95.677068&sspn=73.579623,100.371094&vpsrc=0&hnear=" + - escape(location) + - "&t=m&z=11" - - msg.send mapUrl - msg.send url diff --git a/packages/rocketchat-internal-hubot/scripts/pugme.coffee b/packages/rocketchat-internal-hubot/scripts/pugme.coffee deleted file mode 100644 index 1bc4b8f95c92feda0ddc32e8ea276a2ee759ef9b..0000000000000000000000000000000000000000 --- a/packages/rocketchat-internal-hubot/scripts/pugme.coffee +++ /dev/null @@ -1,31 +0,0 @@ -# Description: -# Pugme is the most important thing in life -# -# Dependencies: -# None -# -# Configuration: -# None -# -# Commands: -# hubot pug me - Receive a pug -# hubot pug bomb N - get N pugs - -module.exports = (robot) -> - - robot.respond /pug me/i, (msg) -> - msg.http("http://pugme.herokuapp.com/random") - .get() (err, res, body) -> - msg.send JSON.parse(body).pug - - robot.respond /pug bomb( (\d+))?/i, (msg) -> - count = msg.match[2] || 5 - count = 5 if count > 5 - msg.http("http://pugme.herokuapp.com/bomb?count=" + count) - .get() (err, res, body) -> - msg.send pug for pug in JSON.parse(body).pugs - - robot.respond /how many pugs are there/i, (msg) -> - msg.http("http://pugme.herokuapp.com/count") - .get() (err, res, body) -> - msg.send "There are #{JSON.parse(body).pug_count} pugs." diff --git a/packages/rocketchat-internal-hubot/settings.coffee b/packages/rocketchat-internal-hubot/settings.coffee deleted file mode 100644 index 52249d5dc41824b08bcfecb42e86edb7d3e0fa93..0000000000000000000000000000000000000000 --- a/packages/rocketchat-internal-hubot/settings.coffee +++ /dev/null @@ -1,4 +0,0 @@ -RocketChat.settings.addGroup 'InternalHubot' -RocketChat.settings.add 'InternalHubot_Enabled', false, { type: 'boolean', group: 'InternalHubot', i18nLabel: 'Enabled' } -RocketChat.settings.add 'InternalHubot_Username', 'rocket.cat', { type: 'string', group: 'InternalHubot', i18nLabel: 'Username', i18nDescription: 'InternalHubot_Username_Description' } -RocketChat.settings.add 'InternalHubot_ScriptsToLoad', 'hello.coffee,zen.coffee', { type: 'string', group: 'InternalHubot'} diff --git a/packages/rocketchat-internal-hubot/settings.js b/packages/rocketchat-internal-hubot/settings.js new file mode 100644 index 0000000000000000000000000000000000000000..ae9b334ca0e842f7c7e21e19533a4eb191f83880 --- /dev/null +++ b/packages/rocketchat-internal-hubot/settings.js @@ -0,0 +1,10 @@ +RocketChat.settings.addGroup('InternalHubot', function() { + this.add('InternalHubot_Enabled', false, { type: 'boolean', i18nLabel: 'Enabled' }); + this.add('InternalHubot_Username', 'rocket.cat', { type: 'string', i18nLabel: 'Username', i18nDescription: 'InternalHubot_Username_Description' }); + this.add('InternalHubot_ScriptsToLoad', '', { type: 'string'}); + this.add('InternalHubot_PathToLoadCustomScripts', '', { type: 'string' }); + // this.add('InternalHubot_reload', 'reloadInternalHubot', { + // type: 'action', + // actionText: 'reload' + // }); +}); diff --git a/packages/rocketchat-irc/package.js b/packages/rocketchat-irc/package.js index 311343292f8f1a4dc4b98df2660e177630704990..d1192c74501e88b21e6138a00c5c5c017a72ef78 100644 --- a/packages/rocketchat-irc/package.js +++ b/packages/rocketchat-irc/package.js @@ -13,14 +13,13 @@ Npm.depends({ Package.onUse(function(api) { api.use([ 'ecmascript', - 'coffeescript', 'underscore', 'rocketchat:lib' ]); api.addFiles([ 'server/settings.js', - 'server/server.coffee' + 'server/server.js' ], 'server'); api.export(['Irc'], ['server']); diff --git a/packages/rocketchat-irc/server/server.coffee b/packages/rocketchat-irc/server/server.coffee deleted file mode 100644 index 4f753e1683eb1e101e5192f5d8ca3ce911aa436c..0000000000000000000000000000000000000000 --- a/packages/rocketchat-irc/server/server.coffee +++ /dev/null @@ -1,411 +0,0 @@ -# # # -# Assign values -# - -# Package availability -IRC_AVAILABILITY = RocketChat.settings.get('IRC_Enabled'); - -# Cache prep -net = Npm.require('net') -Lru = Npm.require('lru-cache') -MESSAGE_CACHE_SIZE = RocketChat.settings.get('IRC_Message_Cache_Size'); -ircReceiveMessageCache = Lru MESSAGE_CACHE_SIZE -ircSendMessageCache = Lru MESSAGE_CACHE_SIZE - -# IRC server -IRC_PORT = RocketChat.settings.get('IRC_Port'); -IRC_HOST = RocketChat.settings.get('IRC_Host'); - -ircClientMap = {} - - -# # # -# Core functionality -# - -bind = (f) -> - g = Meteor.bindEnvironment (self, args...) -> f.apply(self, args) - (args...) -> g @, args... - -async = (f, args...) -> - Meteor.wrapAsync(f)(args...) - -class IrcClient - constructor: (@loginReq) -> - @user = @loginReq.user - ircClientMap[@user._id] = this - @ircPort = IRC_PORT - @ircHost = IRC_HOST - @msgBuf = [] - - @isConnected = false - @isDistroyed = false - @socket = new net.Socket - @socket.setNoDelay - @socket.setEncoding 'utf-8' - @socket.setKeepAlive true - @onConnect = bind @onConnect - @onClose = bind @onClose - @onTimeout = bind @onTimeout - @onError = bind @onError - @onReceiveRawMessage = bind @onReceiveRawMessage - @socket.on 'data', @onReceiveRawMessage - @socket.on 'close', @onClose - @socket.on 'timeout', @onTimeout - @socket.on 'error', @onError - - @isJoiningRoom = false - @receiveMemberListBuf = {} - @pendingJoinRoomBuf = [] - - @successLoginMessageRegex = /RocketChat.settings.get('IRC_RegEx_successLogin');/ - @failedLoginMessageRegex = /RocketChat.settings.get('IRC_RegEx_failedLogin');/ - @receiveMessageRegex = /RocketChat.settings.get('IRC_RegEx_receiveMessage');/ - @receiveMemberListRegex = /RocketChat.settings.get('IRC_RegEx_receiveMemberList');/ - @endMemberListRegex = /RocketChat.settings.get('IRC_RegEx_endMemberList');/ - @addMemberToRoomRegex = /RocketChat.settings.get('IRC_RegEx_addMemberToRoom');/ - @removeMemberFromRoomRegex = /RocketChat.settings.get('IRC_RegEx_removeMemberFromRoom');/ - @quitMemberRegex = /RocketChat.settings.get('IRC_RegEx_quitMember');/ - - connect: (@loginCb) => - @socket.connect @ircPort, @ircHost, @onConnect - @initRoomList() - - disconnect: () -> - @isDistroyed = true - @socket.destroy() - - onConnect: () => - console.log '[irc] onConnect -> '.yellow, @user.username, 'connect success.' - @socket.write "NICK #{@user.username}\r\n" - @socket.write "USER #{@user.username} 0 * :#{@user.name}\r\n" - # message order could not make sure here - @isConnected = true - @socket.write msg for msg in @msgBuf - - onClose: (data) => - console.log '[irc] onClose -> '.yellow, @user.username, 'connection close.' - @isConnected = false - if @isDistroyed - delete ircClientMap[@user._id] - else - @connect() - - onTimeout: () => - console.log '[irc] onTimeout -> '.yellow, @user.username, 'connection timeout.', arguments - - onError: () => - console.log '[irc] onError -> '.yellow, @user.username, 'connection error.', arguments - - onReceiveRawMessage: (data) => - data = data.toString().split('\n') - for line in data - line = line.trim() - console.log "[#{@ircHost}:#{@ircPort}]:", line - # Send heartbeat package to irc server - if line.indexOf('PING') == 0 - @socket.write line.replace('PING :', 'PONG ') - continue - - matchResult = @receiveMessageRegex.exec line - if matchResult - @onReceiveMessage matchResult[1], matchResult[2], matchResult[3] - continue - - matchResult = @receiveMemberListRegex.exec line - if matchResult - @onReceiveMemberList matchResult[1], matchResult[2].split ' ' - continue - - matchResult = @endMemberListRegex.exec line - if matchResult - @onEndMemberList matchResult[1] - continue - - matchResult = @addMemberToRoomRegex.exec line - if matchResult - @onAddMemberToRoom matchResult[1], matchResult[2] - continue - - matchResult = @removeMemberFromRoomRegex.exec line - if matchResult - @onRemoveMemberFromRoom matchResult[1], matchResult[2] - continue - - matchResult = @quitMemberRegex.exec line - if matchResult - @onQuitMember matchResult[1] - continue - - matchResult = @successLoginMessageRegex.exec line - if matchResult - @onSuccessLoginMessage() - continue - - matchResult = @failedLoginMessageRegex.exec line - if matchResult - @onFailedLoginMessage() - continue - - onSuccessLoginMessage: () -> - console.log '[irc] onSuccessLoginMessage -> '.yellow - if @loginCb - @loginCb null, @loginReq - - onFailedLoginMessage: () -> - console.log '[irc] onFailedLoginMessage -> '.yellow - @loginReq.allowed = false - @disconnect() - if @loginCb - @loginCb null, @loginReq - - onReceiveMessage: (source, target, content) -> - now = new Date - timestamp = now.getTime() - - cacheKey = [source, target, content].join ',' - console.log '[irc] ircSendMessageCache.get -> '.yellow, 'key:', cacheKey, 'value:', ircSendMessageCache.get(cacheKey), 'ts:', (timestamp - 1000) - if ircSendMessageCache.get(cacheKey) > (timestamp - 1000) - return - else - ircSendMessageCache.set cacheKey, timestamp - - console.log '[irc] onReceiveMessage -> '.yellow, 'source:', source, 'target:', target, 'content:', content - source = @createUserWhenNotExist source - if target[0] == '#' - room = RocketChat.models.Rooms.findOneByName target.substring(1) - else - room = @createDirectRoomWhenNotExist(source, @user) - - message = - msg: content - ts: now - cacheKey = "#{source.username}#{timestamp}" - ircReceiveMessageCache.set cacheKey, true - console.log '[irc] ircReceiveMessageCache.set -> '.yellow, 'key:', cacheKey - RocketChat.sendMessage source, message, room - - onReceiveMemberList: (roomName, members) -> - @receiveMemberListBuf[roomName] = @receiveMemberListBuf[roomName].concat members - - onEndMemberList: (roomName) -> - newMembers = @receiveMemberListBuf[roomName] - console.log '[irc] onEndMemberList -> '.yellow, 'room:', roomName, 'members:', newMembers.join ',' - room = RocketChat.models.Rooms.findOneByNameAndType roomName, 'c' - unless room - return - - oldMembers = room.usernames - appendMembers = _.difference newMembers, oldMembers - removeMembers = _.difference oldMembers, newMembers - - for member in appendMembers - @createUserWhenNotExist member - - RocketChat.models.Rooms.removeUsernamesById room._id, removeMembers - RocketChat.models.Rooms.addUsernamesById room._id, appendMembers - - @isJoiningRoom = false - roomName = @pendingJoinRoomBuf.shift() - if roomName - @joinRoom - t: 'c' - name: roomName - - sendRawMessage: (msg) -> - console.log '[irc] sendRawMessage -> '.yellow, msg.slice(0, -2) - if @isConnected - @socket.write msg - else - @msgBuf.push msg - - sendMessage: (room, message) -> - console.log '[irc] sendMessage -> '.yellow, 'userName:', message.u.username - target = '' - if room.t == 'c' - target = "##{room.name}" - else if room.t == 'd' - for name in room.usernames - if message.u.username != name - target = name - break - - cacheKey = [@user.username, target, message.msg].join ',' - console.log '[irc] ircSendMessageCache.set -> '.yellow, 'key:', cacheKey, 'ts:', message.ts.getTime() - ircSendMessageCache.set cacheKey, message.ts.getTime() - msg = "PRIVMSG #{target} :#{message.msg}\r\n" - @sendRawMessage msg - - initRoomList: -> - roomsCursor = RocketChat.models.Rooms.findByTypeContainingUsername 'c', @user.username, - fields: - name: 1 - t: 1 - - rooms = roomsCursor.fetch() - for room in rooms - @joinRoom(room) - - joinRoom: (room) -> - if room.t isnt 'c' or room.name == 'general' - return - - if @isJoiningRoom - @pendingJoinRoomBuf.push room.name - else - console.log '[irc] joinRoom -> '.yellow, 'roomName:', room.name, 'pendingJoinRoomBuf:', @pendingJoinRoomBuf.join ',' - msg = "JOIN ##{room.name}\r\n" - @receiveMemberListBuf[room.name] = [] - @sendRawMessage msg - @isJoiningRoom = true - - leaveRoom: (room) -> - if room.t isnt 'c' - return - msg = "PART ##{room.name}\r\n" - @sendRawMessage msg - - getMemberList: (room) -> - if room.t isnt 'c' - return - msg = "NAMES ##{room.name}\r\n" - @receiveMemberListBuf[room.name] = [] - @sendRawMessage msg - - onAddMemberToRoom: (member, roomName) -> - if @user.username == member - return - - console.log '[irc] onAddMemberToRoom -> '.yellow, 'roomName:', roomName, 'member:', member - @createUserWhenNotExist member - - RocketChat.models.Rooms.addUsernameByName roomName, member - - onRemoveMemberFromRoom: (member, roomName)-> - console.log '[irc] onRemoveMemberFromRoom -> '.yellow, 'roomName:', roomName, 'member:', member - RocketChat.models.Rooms.removeUsernameByName roomName, member - - onQuitMember: (member) -> - console.log '[irc] onQuitMember ->'.yellow, 'username:', member - RocketChat.models.Rooms.removeUsernameFromAll member - - Meteor.users.update {name: member}, - $set: - status: 'offline' - - createUserWhenNotExist: (name) -> - user = Meteor.users.findOne {name: name} - unless user - console.log '[irc] createNotExistUser ->'.yellow, 'userName:', name - Meteor.call 'registerUser', - email: "#{name}@rocketchat.org" - pass: 'rocketchat' - name: name - Meteor.users.update {name: name}, - $set: - status: 'online' - username: name - user = Meteor.users.findOne {name: name} - return user - - - createDirectRoomWhenNotExist: (source, target) -> - console.log '[irc] createDirectRoomWhenNotExist -> '.yellow, 'source:', source, 'target:', target - rid = [source._id, target._id].sort().join('') - now = new Date() - RocketChat.models.Rooms.upsert - _id: rid - , - $set: - usernames: [source.username, target.username] - $setOnInsert: - t: 'd' - msgs: 0 - ts: now - - RocketChat.models.Subscriptions.upsert - rid: rid - $and: [{'u._id': target._id}] - , - $setOnInsert: - name: source.username - t: 'd' - open: false - alert: false - unread: 0 - u: - _id: target._id - username: target.username - return { - t: 'd' - _id: rid - } - -IrcClient.getByUid = (uid) -> - return ircClientMap[uid] - -IrcClient.create = (login) -> - unless login.user? - return login - unless login.user._id of ircClientMap - ircClient = new IrcClient login - return async ircClient.connect - - return login - - -class IrcLoginer - constructor: (login) -> - console.log '[irc] validateLogin -> '.yellow, login - return IrcClient.create login - - -class IrcSender - constructor: (message) -> - name = message.u.username - timestamp = message.ts.getTime() - cacheKey = "#{name}#{timestamp}" - if ircReceiveMessageCache.get cacheKey - return message - - room = RocketChat.models.Rooms.findOneById message.rid, { fields: { name: 1, usernames: 1, t: 1 } } - ircClient = IrcClient.getByUid message.u._id - ircClient.sendMessage room, message - return message - - -class IrcRoomJoiner - constructor: (user, room) -> - ircClient = IrcClient.getByUid user._id - ircClient.joinRoom room - return room - - -class IrcRoomLeaver - constructor: (user, room) -> - ircClient = IrcClient.getByUid user._id - ircClient.leaveRoom room - return room - - -class IrcLogoutCleanUper - constructor: (user) -> - ircClient = IrcClient.getByUid user._id - ircClient.disconnect() - return user - - -# # # -# Make magic happen -# - -# Only proceed if the package has been enabled -if IRC_AVAILABILITY == true - RocketChat.callbacks.add 'beforeValidateLogin', IrcLoginer, RocketChat.callbacks.priority.LOW, 'irc-loginer' - RocketChat.callbacks.add 'beforeSaveMessage', IrcSender, RocketChat.callbacks.priority.LOW, 'irc-sender' - RocketChat.callbacks.add 'beforeJoinRoom', IrcRoomJoiner, RocketChat.callbacks.priority.LOW, 'irc-room-joiner' - RocketChat.callbacks.add 'beforeCreateChannel', IrcRoomJoiner, RocketChat.callbacks.priority.LOW, 'irc-room-joiner-create-channel' - RocketChat.callbacks.add 'beforeLeaveRoom', IrcRoomLeaver, RocketChat.callbacks.priority.LOW, 'irc-room-leaver' - RocketChat.callbacks.add 'afterLogoutCleanUp', IrcLogoutCleanUper, RocketChat.callbacks.priority.LOW, 'irc-clean-up' -else - return diff --git a/packages/rocketchat-irc/server/server.js b/packages/rocketchat-irc/server/server.js new file mode 100644 index 0000000000000000000000000000000000000000..fd085e2060052c059f7d1ef74723b860616bd546 --- /dev/null +++ b/packages/rocketchat-irc/server/server.js @@ -0,0 +1,433 @@ +import net from 'net'; +import Lru from 'lru-cache'; + +/////// +// Assign values + +//Package availability +const IRC_AVAILABILITY = RocketChat.settings.get('IRC_Enabled'); + +// Cache prep +const MESSAGE_CACHE_SIZE = RocketChat.settings.get('IRC_Message_Cache_Size'); +const ircReceiveMessageCache = Lru(MESSAGE_CACHE_SIZE);//eslint-disable-line +const ircSendMessageCache = Lru(MESSAGE_CACHE_SIZE);//eslint-disable-line + +// IRC server +const IRC_PORT = RocketChat.settings.get('IRC_Port'); +const IRC_HOST = RocketChat.settings.get('IRC_Host'); + +const ircClientMap = {}; + +////// +// Core functionality + +const bind = function(f) { + const g = Meteor.bindEnvironment((self, ...args) => f.apply(self, args)); + return function(...args) { g(this, ...args); }; +}; + +const async = (f, ...args) => Meteor.wrapAsync(f)(...args); + +class IrcClient { + constructor(loginReq) { + this.loginReq = loginReq; + + this.user = this.loginReq.user; + ircClientMap[this.user._id] = this; + this.ircPort = IRC_PORT; + this.ircHost = IRC_HOST; + this.msgBuf = []; + + this.isConnected = false; + this.isDistroyed = false; + this.socket = new net.Socket; + this.socket.setNoDelay; + this.socket.setEncoding('utf-8'); + this.socket.setKeepAlive(true); + this.onConnect = bind(this.onConnect); + this.onClose = bind(this.onClose); + this.onTimeout = bind(this.onTimeout); + this.onError = bind(this.onError); + this.onReceiveRawMessage = bind(this.onReceiveRawMessage); + this.socket.on('data', this.onReceiveRawMessage); + this.socket.on('close', this.onClose); + this.socket.on('timeout', this.onTimeout); + this.socket.on('error', this.onError); + + this.isJoiningRoom = false; + this.receiveMemberListBuf = {}; + this.pendingJoinRoomBuf = []; + + this.successLoginMessageRegex = /RocketChat.settings.get('IRC_RegEx_successLogin');/; + this.failedLoginMessageRegex = /RocketChat.settings.get('IRC_RegEx_failedLogin');/; + this.receiveMessageRegex = /RocketChat.settings.get('IRC_RegEx_receiveMessage');/; + this.receiveMemberListRegex = /RocketChat.settings.get('IRC_RegEx_receiveMemberList');/; + this.endMemberListRegex = /RocketChat.settings.get('IRC_RegEx_endMemberList');/; + this.addMemberToRoomRegex = /RocketChat.settings.get('IRC_RegEx_addMemberToRoom');/; + this.removeMemberFromRoomRegex = /RocketChat.settings.get('IRC_RegEx_removeMemberFromRoom');/; + this.quitMemberRegex = /RocketChat.settings.get('IRC_RegEx_quitMember');/; + } + + connect(loginCb) { + this.loginCb = loginCb; + this.socket.connect(this.ircPort, this.ircHost, this.onConnect); + this.initRoomList(); + } + + disconnect() { + this.isDistroyed = true; + this.socket.destroy(); + } + + onConnect() { + console.log('[irc] onConnect -> '.yellow, this.user.username, 'connect success.'); + this.socket.write(`NICK ${ this.user.username }\r\n`); + this.socket.write(`USER ${ this.user.username } 0 * :${ this.user.name }\r\n`); + // message order could not make sure here + this.isConnected = true; + const messageBuf = this.msgBuf; + messageBuf.forEach(msg => this.socket.write(msg)); + } + + onClose() { + console.log('[irc] onClose -> '.yellow, this.user.username, 'connection close.'); + this.isConnected = false; + if (this.isDistroyed) { + delete ircClientMap[this.user._id]; + } else { + this.connect(); + } + } + + onTimeout() { + console.log('[irc] onTimeout -> '.yellow, this.user.username, 'connection timeout.', arguments); + } + + onError() { + console.log('[irc] onError -> '.yellow, this.user.username, 'connection error.', arguments); + } + + onReceiveRawMessage(data) { + data = data.toString().split('\n'); + + data.forEach(line => { + line = line.trim(); + console.log(`[${ this.ircHost }:${ this.ircPort }]:`, line); + + // Send heartbeat package to irc server + if (line.indexOf('PING') === 0) { + this.socket.write(line.replace('PING :', 'PONG ')); + return; + } + let matchResult = this.receiveMessageRegex.exec(line); + if (matchResult) { + this.onReceiveMessage(matchResult[1], matchResult[2], matchResult[3]); + return; + } + matchResult = this.receiveMemberListRegex.exec(line); + if (matchResult) { + this.onReceiveMemberList(matchResult[1], matchResult[2].split(' ')); + return; + } + matchResult = this.endMemberListRegex.exec(line); + if (matchResult) { + this.onEndMemberList(matchResult[1]); + return; + } + matchResult = this.addMemberToRoomRegex.exec(line); + if (matchResult) { + this.onAddMemberToRoom(matchResult[1], matchResult[2]); + return; + } + matchResult = this.removeMemberFromRoomRegex.exec(line); + if (matchResult) { + this.onRemoveMemberFromRoom(matchResult[1], matchResult[2]); + return; + } + matchResult = this.quitMemberRegex.exec(line); + if (matchResult) { + this.onQuitMember(matchResult[1]); + return; + } + matchResult = this.successLoginMessageRegex.exec(line); + if (matchResult) { + this.onSuccessLoginMessage(); + return; + } + matchResult = this.failedLoginMessageRegex.exec(line); + if (matchResult) { + this.onFailedLoginMessage(); + return; + } + }); + } + + onSuccessLoginMessage() { + console.log('[irc] onSuccessLoginMessage -> '.yellow); + if (this.loginCb) { + this.loginCb(null, this.loginReq); + } + } + + onFailedLoginMessage() { + console.log('[irc] onFailedLoginMessage -> '.yellow); + this.loginReq.allowed = false; + this.disconnect(); + if (this.loginCb) { + this.loginCb(null, this.loginReq); + } + } + + onReceiveMessage(source, target, content) { + const now = new Date; + const timestamp = now.getTime(); + let cacheKey = [source, target, content].join(','); + console.log('[irc] ircSendMessageCache.get -> '.yellow, 'key:', cacheKey, 'value:', ircSendMessageCache.get(cacheKey), 'ts:', timestamp - 1000); + if (ircSendMessageCache.get(cacheKey) > (timestamp - 1000)) { + return; + } else { + ircSendMessageCache.set(cacheKey, timestamp); + } + console.log('[irc] onReceiveMessage -> '.yellow, 'source:', source, 'target:', target, 'content:', content); + source = this.createUserWhenNotExist(source); + let room; + if (target[0] === '#') { + room = RocketChat.models.Rooms.findOneByName(target.substring(1)); + } else { + room = this.createDirectRoomWhenNotExist(source, this.user); + } + const message = { msg: content, ts: now }; + cacheKey = `${ source.username }${ timestamp }`; + ircReceiveMessageCache.set(cacheKey, true); + console.log('[irc] ircReceiveMessageCache.set -> '.yellow, 'key:', cacheKey); + RocketChat.sendMessage(source, message, room); + } + + onReceiveMemberList(roomName, members) { + this.receiveMemberListBuf[roomName] = this.receiveMemberListBuf[roomName].concat(members); + } + + onEndMemberList(roomName) { + const newMembers = this.receiveMemberListBuf[roomName]; + console.log('[irc] onEndMemberList -> '.yellow, 'room:', roomName, 'members:', newMembers.join(',')); + const room = RocketChat.models.Rooms.findOneByNameAndType(roomName, 'c'); + if (!room) { + return; + } + const oldMembers = room.usernames; + const appendMembers = _.difference(newMembers, oldMembers); + const removeMembers = _.difference(oldMembers, newMembers); + appendMembers.forEach(member => this.createUserWhenNotExist(member)); + RocketChat.models.Rooms.removeUsernamesById(room._id, removeMembers); + RocketChat.models.Rooms.addUsernamesById(room._id, appendMembers); + + this.isJoiningRoom = false; + roomName = this.pendingJoinRoomBuf.shift(); + if (roomName) { + this.joinRoom({ + t: 'c', + name: roomName + }); + } + } + + sendRawMessage(msg) { + console.log('[irc] sendRawMessage -> '.yellow, msg.slice(0, -2)); + if (this.isConnected) { + this.socket.write(msg); + } else { + this.msgBuf.push(msg); + } + } + + sendMessage(room, message) { + console.log('[irc] sendMessage -> '.yellow, 'userName:', message.u.username); + let target = ''; + if (room.t === 'c') { + target = `#${ room.name }`; + } else if (room.t === 'd') { + const usernames = room.usernames; + usernames.forEach(name => { + if (message.u.username !== name) { + target = name; + return; + } + }); + } + const cacheKey = [this.user.username, target, message.msg].join(','); + console.log('[irc] ircSendMessageCache.set -> '.yellow, 'key:', cacheKey, 'ts:', message.ts.getTime()); + ircSendMessageCache.set(cacheKey, message.ts.getTime()); + const msg = `PRIVMSG ${ target } :${ message.msg }\r\n`; + this.sendRawMessage(msg); + } + + initRoomList() { + const roomsCursor = RocketChat.models.Rooms.findByTypeContainingUsername('c', this.user.username, { fields: { name: 1, t: 1 }}); + const rooms = roomsCursor.fetch(); + rooms.forEach(room => this.joinRoom(room)); + } + + joinRoom(room) { + if (room.t !== 'c' || room.name === 'general') { + return; + } + if (this.isJoiningRoom) { + return this.pendingJoinRoomBuf.push(room.name); + } + console.log('[irc] joinRoom -> '.yellow, 'roomName:', room.name, 'pendingJoinRoomBuf:', this.pendingJoinRoomBuf.join(',')); + const msg = `JOIN #${ room.name }\r\n`; + this.receiveMemberListBuf[room.name] = []; + this.sendRawMessage(msg); + this.isJoiningRoom = true; + } + + leaveRoom(room) { + if (room.t !== 'c') { + return; + } + const msg = `PART #${ room.name }\r\n`; + this.sendRawMessage(msg); + } + + getMemberList(room) { + if (room.t !== 'c') { + return; + } + const msg = `NAMES #${ room.name }\r\n`; + this.receiveMemberListBuf[room.name] = []; + this.sendRawMessage(msg); + } + + onAddMemberToRoom(member, roomName) { + if (this.user.username === member) { + return; + } + console.log('[irc] onAddMemberToRoom -> '.yellow, 'roomName:', roomName, 'member:', member); + this.createUserWhenNotExist(member); + RocketChat.models.Rooms.addUsernameByName(roomName, member); + } + + onRemoveMemberFromRoom(member, roomName) { + console.log('[irc] onRemoveMemberFromRoom -> '.yellow, 'roomName:', roomName, 'member:', member); + RocketChat.models.Rooms.removeUsernameByName(roomName, member); + } + + onQuitMember(member) { + console.log('[irc] onQuitMember ->'.yellow, 'username:', member); + RocketChat.models.Rooms.removeUsernameFromAll(member); + Meteor.users.update({ name: member }, { $set: { status: 'offline' }}); + } + + createUserWhenNotExist(name) { + const user = Meteor.users.findOne({ name }); + if (user) { + return user; + } + console.log('[irc] createNotExistUser ->'.yellow, 'userName:', name); + Meteor.call('registerUser', { + email: `${ name }@rocketchat.org`, + pass: 'rocketchat', + name + }); + Meteor.users.update({ name }, { + $set: { + status: 'online', + username: name + } + }); + return Meteor.users.findOne({ name }); + } + + createDirectRoomWhenNotExist(source, target) { + console.log('[irc] createDirectRoomWhenNotExist -> '.yellow, 'source:', source, 'target:', target); + const rid = [source._id, target._id].sort().join(''); + const now = new Date(); + RocketChat.models.Rooms.upsert({ _id: rid}, { + $set: { + usernames: [source.username, target.username] + }, + $setOnInsert: { + t: 'd', + msgs: 0, + ts: now + } + }); + RocketChat.models.Subscriptions.upsert({ rid, $and: [{ 'u._id': target._id}]}, { + $setOnInsert: { + name: source.username, + t: 'd', + open: false, + alert: false, + unread: 0, + u: { _id: target._id, username: target.username }} + }); + return { t: 'd', _id: rid }; + } +} + +IrcClient.getByUid = function(uid) { + return ircClientMap[uid]; +}; + +IrcClient.create = function(login) { + if (login.user == null) { + return login; + } + if (!(login.user._id in ircClientMap)) { + const ircClient = new IrcClient(login); + return async(ircClient.connect); + } + return login; +}; + +function IrcLoginer(login) { + console.log('[irc] validateLogin -> '.yellow, login); + return IrcClient.create(login); +} + + +function IrcSender(message) { + const name = message.u.username; + const timestamp = message.ts.getTime(); + const cacheKey = `${ name }${ timestamp }`; + if (ircReceiveMessageCache.get(cacheKey)) { + return message; + } + const room = RocketChat.models.Rooms.findOneById(message.rid, { fields: { name: 1, usernames: 1, t: 1 }}); + const ircClient = IrcClient.getByUid(message.u._id); + ircClient.sendMessage(room, message); + return message; +} + + +function IrcRoomJoiner(user, room) { + const ircClient = IrcClient.getByUid(user._id); + ircClient.joinRoom(room); + return room; +} + + +function IrcRoomLeaver(user, room) { + const ircClient = IrcClient.getByUid(user._id); + ircClient.leaveRoom(room); + return room; +} + +function IrcLogoutCleanUper(user) { + const ircClient = IrcClient.getByUid(user._id); + ircClient.disconnect(); + return user; +} + +////// +// Make magic happen + +// Only proceed if the package has been enabled +if (IRC_AVAILABILITY === true) { + RocketChat.callbacks.add('beforeValidateLogin', IrcLoginer, RocketChat.callbacks.priority.LOW, 'irc-loginer'); + RocketChat.callbacks.add('beforeSaveMessage', IrcSender, RocketChat.callbacks.priority.LOW, 'irc-sender'); + RocketChat.callbacks.add('beforeJoinRoom', IrcRoomJoiner, RocketChat.callbacks.priority.LOW, 'irc-room-joiner'); + RocketChat.callbacks.add('beforeCreateChannel', IrcRoomJoiner, RocketChat.callbacks.priority.LOW, 'irc-room-joiner-create-channel'); + RocketChat.callbacks.add('beforeLeaveRoom', IrcRoomLeaver, RocketChat.callbacks.priority.LOW, 'irc-room-leaver'); + RocketChat.callbacks.add('afterLogoutCleanUp', IrcLogoutCleanUper, RocketChat.callbacks.priority.LOW, 'irc-clean-up'); +} diff --git a/packages/rocketchat-katex/package.js b/packages/rocketchat-katex/package.js index de08f7f50f5de94f95204cc3e3787300d20cc012..c72507a3b1d57f9947445db2443c59821be06c19 100644 --- a/packages/rocketchat-katex/package.js +++ b/packages/rocketchat-katex/package.js @@ -29,12 +29,3 @@ Package.onUse(function(api) { api.addAssets(fontFiles, 'client'); }); - -Package.onTest(function(api) { - api.use('coffeescript'); - api.use('sanjo:jasmine@0.20.2'); - api.use('rocketchat:lib'); - api.use('rocketchat:katex'); - - api.addFiles('tests/jasmine/client/unit/katex.spec.coffee', 'client'); -}); diff --git a/packages/rocketchat-katex/tests/jasmine/client/unit/katex.spec.coffee b/packages/rocketchat-katex/tests/jasmine/client/unit/katex.spec.coffee deleted file mode 100644 index 3130fb6012d0335b0c773d133aee7941334a7026..0000000000000000000000000000000000000000 --- a/packages/rocketchat-katex/tests/jasmine/client/unit/katex.spec.coffee +++ /dev/null @@ -1,4 +0,0 @@ -describe 'rocketchat:katex Client', -> - - it 'should exist', -> - expect(RocketChat.katex).toBeDefined() diff --git a/packages/rocketchat-ldap/server/loginHandler.js b/packages/rocketchat-ldap/server/loginHandler.js index 25e5af9045c2907119b257cc7bedcc61612045fa..d12240a33d77cb444a4b0eb5392322892f260167 100644 --- a/packages/rocketchat-ldap/server/loginHandler.js +++ b/packages/rocketchat-ldap/server/loginHandler.js @@ -125,7 +125,11 @@ Accounts.registerLoginHandler('ldap', function(loginRequest) { }); syncUserData(user, ldapUser); - Accounts.setPassword(user._id, loginRequest.ldapPass, {logout: false}); + + if (RocketChat.settings.get('LDAP_Login_Fallback') === true) { + Accounts.setPassword(user._id, loginRequest.ldapPass, {logout: false}); + } + return { userId: user._id, token: stampedToken.token diff --git a/packages/rocketchat-ldap/server/sync.js b/packages/rocketchat-ldap/server/sync.js index df3ec4bf828bd9b084fafb3d6b1b90224370880f..c8c944a145c1f4256f2b739c339be5ed69ba2e00 100644 --- a/packages/rocketchat-ldap/server/sync.js +++ b/packages/rocketchat-ldap/server/sync.js @@ -65,15 +65,15 @@ getDataToSyncUserData = function getDataToSyncUserData(ldapUser, user) { if (syncUserData && syncUserDataFieldMap) { const fieldMap = JSON.parse(syncUserDataFieldMap); const userData = {}; - const emailList = []; _.map(fieldMap, function(userField, ldapField) { - if (!ldapUser.object.hasOwnProperty(ldapField)) { - return; - } - switch (userField) { case 'email': + if (!ldapUser.object.hasOwnProperty(ldapField)) { + logger.debug(`user does not have attribute: ${ ldapField }`); + return; + } + if (_.isObject(ldapUser.object[ldapField])) { _.map(ldapUser.object[ldapField], function(item) { emailList.push({ address: item, verified: true }); @@ -84,8 +84,37 @@ getDataToSyncUserData = function getDataToSyncUserData(ldapUser, user) { break; case 'name': - if (user.name !== ldapUser.object[ldapField]) { - userData.name = ldapUser.object[ldapField]; + const templateRegex = /#{(\w+)}/gi; + let match = templateRegex.exec(ldapField); + let tmpLdapField = ldapField; + + if (match == null) { + if (!ldapUser.object.hasOwnProperty(ldapField)) { + logger.debug(`user does not have attribute: ${ ldapField }`); + return; + } + tmpLdapField = ldapUser.object[ldapField]; + } else { + logger.debug('template found. replacing values'); + while (match != null) { + const tmplVar = match[0]; + const tmplAttrName = match[1]; + + if (!ldapUser.object.hasOwnProperty(tmplAttrName)) { + logger.debug(`user does not have attribute: ${ tmplAttrName }`); + return; + } + + const attrVal = ldapUser.object[tmplAttrName]; + logger.debug(`replacing template var: ${ tmplVar } with value from ldap: ${ attrVal }`); + tmpLdapField = tmpLdapField.replace(tmplVar, attrVal); + match = templateRegex.exec(ldapField); + } + } + + if (user.name !== tmpLdapField) { + userData.name = tmpLdapField; + logger.debug(`user.name changed to: ${ tmpLdapField }`); } break; } @@ -143,16 +172,22 @@ syncUserData = function syncUserData(user, ldapUser) { const avatar = ldapUser.raw.thumbnailPhoto || ldapUser.raw.jpegPhoto; if (avatar) { logger.info('Syncing user avatar'); + const rs = RocketChatFile.bufferToStream(avatar); - RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${ user.username }.jpg`)); - const ws = RocketChatFileAvatarInstance.createWriteStream(encodeURIComponent(`${ user.username }.jpg`), 'image/jpeg'); - ws.on('end', Meteor.bindEnvironment(function() { + const fileStore = FileUpload.getStore('Avatars'); + fileStore.deleteByName(user.username); + + const file = { + userId: user._id, + type: 'image/jpeg' + }; + + fileStore.insert(file, rs, () => { Meteor.setTimeout(function() { RocketChat.models.Users.setAvatarOrigin(user._id, 'ldap'); RocketChat.Notifications.notifyLogged('updateAvatar', {username: user.username}); }, 500); - })); - rs.pipe(ws); + }); } } }; diff --git a/packages/rocketchat-lib/client/MessageAction.js b/packages/rocketchat-lib/client/MessageAction.js index db2ee342a4941ddc2b4b482876e1ba760aab2049..0521980947231dcf3bfbb9bae1a1f035bf0f6572 100644 --- a/packages/rocketchat-lib/client/MessageAction.js +++ b/packages/rocketchat-lib/client/MessageAction.js @@ -180,14 +180,18 @@ Meteor.startup(function() { if (RocketChat.models.Subscriptions.findOne({rid: message.rid}) == null) { return false; } + const forceDelete = RocketChat.authz.hasAtLeastOnePermission('force-delete-message', message.rid); const hasPermission = RocketChat.authz.hasAtLeastOnePermission('delete-message', message.rid); const isDeleteAllowed = RocketChat.settings.get('Message_AllowDeleting'); const deleteOwn = message.u && message.u._id === Meteor.userId(); - if (!(hasPermission || (isDeleteAllowed && deleteOwn))) { + if (!(hasPermission || (isDeleteAllowed && deleteOwn) || forceDelete)) { return; } const blockDeleteInMinutes = RocketChat.settings.get('Message_AllowDeleting_BlockDeleteInMinutes'); - if ((blockDeleteInMinutes != null) && blockDeleteInMinutes !== 0) { + if (forceDelete) { + return true; + } + if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) { let msgTs; if (message.ts != null) { msgTs = moment(message.ts); @@ -210,7 +214,7 @@ Meteor.startup(function() { i18nLabel: 'Permalink', classes: 'clipboard', context: ['message', 'message-mobile'], - action() { + action(event) { const message = this._arguments[1]; const permalink = RocketChat.MessageAction.getPermaLink(message._id); RocketChat.MessageAction.hideDropDown(); @@ -237,7 +241,7 @@ Meteor.startup(function() { i18nLabel: 'Copy', classes: 'clipboard', context: ['message', 'message-mobile'], - action() { + action(event) { const message = this._arguments[1].msg; RocketChat.MessageAction.hideDropDown(); if (Meteor.isCordova) { diff --git a/packages/rocketchat-lib/client/lib/openRoom.coffee b/packages/rocketchat-lib/client/lib/openRoom.coffee deleted file mode 100644 index 3130cd82a3e0f41ead8b19fd979e576125d94623..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/client/lib/openRoom.coffee +++ /dev/null @@ -1,80 +0,0 @@ -currentTracker = undefined - -@openRoom = (type, name) -> - Session.set 'openedRoom', null - - Meteor.defer -> - currentTracker = Tracker.autorun (c) -> - user = Meteor.user() - if (user? and not user.username?) or (not user? and RocketChat.settings.get('Accounts_AllowAnonymousRead') is false) - BlazeLayout.render 'main' - return - - if RoomManager.open(type + name).ready() isnt true - BlazeLayout.render 'main', { modal: RocketChat.Layout.isEmbedded(), center: 'loading' } - return - - currentTracker = undefined - c.stop() - - room = RocketChat.roomTypes.findRoom(type, name, user) - if not room? - if type is 'd' - Meteor.call 'createDirectMessage', name, (err) -> - if !err - RoomManager.close(type + name) - openRoom('d', name) - else - Session.set 'roomNotFound', {type: type, name: name} - BlazeLayout.render 'main', {center: 'roomNotFound'} - return - else - Meteor.call 'getRoomByTypeAndName', type, name, (err, record) -> - if err? - Session.set 'roomNotFound', {type: type, name: name} - BlazeLayout.render 'main', {center: 'roomNotFound'} - else - delete record.$loki - RocketChat.models.Rooms.upsert({ _id: record._id }, _.omit(record, '_id')) - RoomManager.close(type + name) - openRoom(type, name) - - return - - mainNode = document.querySelector('.main-content') - if mainNode? - for child in mainNode.children - mainNode.removeChild child if child? - roomDom = RoomManager.getDomOfRoom(type + name, room._id) - mainNode.appendChild roomDom - if roomDom.classList.contains('room-container') - roomDom.querySelector('.messages-box > .wrapper').scrollTop = roomDom.oldScrollTop - - Session.set 'openedRoom', room._id - - fireGlobalEvent 'room-opened', _.omit room, 'usernames' - - Session.set 'editRoomTitle', false - RoomManager.updateMentionsMarksOfRoom type + name - Meteor.setTimeout -> - readMessage.readNow() - , 2000 - # KonchatNotification.removeRoomNotification(params._id) - - if Meteor.Device.isDesktop() and window.chatMessages?[room._id]? - setTimeout -> - $('.message-form .input-message').focus() - , 100 - - # update user's room subscription - sub = ChatSubscription.findOne({rid: room._id}) - if sub?.open is false - Meteor.call 'openRoom', room._id, (err) -> - if err - return handleError(err) - - if FlowRouter.getQueryParam('msg') - msg = { _id: FlowRouter.getQueryParam('msg'), rid: room._id } - RoomHistoryManager.getSurroundingMessages(msg); - - RocketChat.callbacks.run 'enter-room', sub diff --git a/packages/rocketchat-lib/client/lib/openRoom.js b/packages/rocketchat-lib/client/lib/openRoom.js index b283b51c08abd988c5f96c17c044bd6052e51f64..02351dcf7cf1640dafc742ac2b167760e223cf4a 100644 --- a/packages/rocketchat-lib/client/lib/openRoom.js +++ b/packages/rocketchat-lib/client/lib/openRoom.js @@ -7,7 +7,7 @@ function openRoom(type, name) { 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) { + if ((user && user.username == null) || user == null && RocketChat.settings.get('Accounts_AllowAnonymousRead') === false) { BlazeLayout.render('main'); return; } diff --git a/packages/rocketchat-lib/client/models/Avatars.js b/packages/rocketchat-lib/client/models/Avatars.js new file mode 100644 index 0000000000000000000000000000000000000000..bd8804e746c830c23b15d41a83a26f2f14719433 --- /dev/null +++ b/packages/rocketchat-lib/client/models/Avatars.js @@ -0,0 +1,6 @@ +RocketChat.models.Avatars = new class extends RocketChat.models._Base { + constructor() { + super(); + this._initModel('avatars'); + } +}; diff --git a/packages/rocketchat-lib/lib/roomTypesCommon.js b/packages/rocketchat-lib/lib/roomTypesCommon.js index 1df20edae0b3785391499f4e32996a5c3501bc5b..c0e669eff40df3324a2c37085493e53be71d26a0 100644 --- a/packages/rocketchat-lib/lib/roomTypesCommon.js +++ b/packages/rocketchat-lib/lib/roomTypesCommon.js @@ -10,11 +10,12 @@ this.roomTypesCommon = class { @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 + label: i18n label route: name: route name action: route action function + identifier: room type identifier */ add(identifier = Random.id(), order, config) { @@ -29,7 +30,7 @@ this.roomTypesCommon = class { identifier, order }); - this.roomTypes[identifier] = config; + this.roomTypes[identifier] = {...config, identifier}; if (config.route && config.route.path && config.route.name && config.route.action) { const routeConfig = { name: config.route.name, diff --git a/packages/rocketchat-lib/package.js b/packages/rocketchat-lib/package.js index 3d7f8402927a4b2bbaa46da191ba8a3d2d5bcb2f..b4e0168c2664bd46e8596c81da54d1d93567df16 100644 --- a/packages/rocketchat-lib/package.js +++ b/packages/rocketchat-lib/package.js @@ -21,7 +21,6 @@ Package.onUse(function(api) { api.use('reactive-var'); api.use('reactive-dict'); api.use('accounts-base'); - api.use('coffeescript'); api.use('ecmascript'); api.use('random'); api.use('check'); @@ -72,7 +71,7 @@ Package.onUse(function(api) { api.addFiles('server/functions/addUserToDefaultChannels.js', 'server'); api.addFiles('server/functions/addUserToRoom.js', 'server'); api.addFiles('server/functions/archiveRoom.js', 'server'); - api.addFiles('server/functions/checkUsernameAvailability.coffee', 'server'); + api.addFiles('server/functions/checkUsernameAvailability.js', 'server'); api.addFiles('server/functions/checkEmailAvailability.js', 'server'); api.addFiles('server/functions/createRoom.js', 'server'); api.addFiles('server/functions/deleteMessage.js', 'server'); @@ -82,15 +81,15 @@ Package.onUse(function(api) { api.addFiles('server/functions/removeUserFromRoom.js', 'server'); api.addFiles('server/functions/saveUser.js', 'server'); api.addFiles('server/functions/saveCustomFields.js', 'server'); - api.addFiles('server/functions/sendMessage.coffee', 'server'); - api.addFiles('server/functions/settings.coffee', 'server'); + api.addFiles('server/functions/sendMessage.js', 'server'); + api.addFiles('server/functions/settings.js', 'server'); api.addFiles('server/functions/setUserAvatar.js', 'server'); - api.addFiles('server/functions/setUsername.coffee', 'server'); + api.addFiles('server/functions/setUsername.js', 'server'); api.addFiles('server/functions/setRealName.js', 'server'); api.addFiles('server/functions/setEmail.js', 'server'); api.addFiles('server/functions/unarchiveRoom.js', 'server'); api.addFiles('server/functions/updateMessage.js', 'server'); - api.addFiles('server/functions/Notifications.coffee', 'server'); + api.addFiles('server/functions/Notifications.js', 'server'); // SERVER LIB api.addFiles('server/lib/configLogger.js', 'server'); @@ -104,13 +103,14 @@ Package.onUse(function(api) { // SERVER MODELS api.addFiles('server/models/_Base.js', 'server'); - api.addFiles('server/models/Messages.coffee', 'server'); + api.addFiles('server/models/Avatars.js', 'server'); + api.addFiles('server/models/Messages.js', 'server'); api.addFiles('server/models/Reports.js', 'server'); - api.addFiles('server/models/Rooms.coffee', 'server'); - api.addFiles('server/models/Settings.coffee', 'server'); - api.addFiles('server/models/Subscriptions.coffee', 'server'); - api.addFiles('server/models/Uploads.coffee', 'server'); - api.addFiles('server/models/Users.coffee', 'server'); + api.addFiles('server/models/Rooms.js', 'server'); + api.addFiles('server/models/Settings.js', 'server'); + api.addFiles('server/models/Subscriptions.js', 'server'); + api.addFiles('server/models/Uploads.js', 'server'); + api.addFiles('server/models/Users.js', 'server'); api.addFiles('server/oauth/oauth.js', 'server'); api.addFiles('server/oauth/google.js', 'server'); @@ -144,6 +144,7 @@ Package.onUse(function(api) { api.addFiles('server/methods/getFullUserData.js', 'server'); api.addFiles('server/methods/getRoomRoles.js', 'server'); api.addFiles('server/methods/getServerInfo.js', 'server'); + api.addFiles('server/methods/getSingleMessage.js', 'server'); api.addFiles('server/methods/getUserRoles.js', 'server'); api.addFiles('server/methods/insertOrUpdateUser.js', 'server'); api.addFiles('server/methods/joinDefaultChannels.js', 'server'); @@ -154,7 +155,7 @@ Package.onUse(function(api) { api.addFiles('server/methods/robotMethods.js', 'server'); api.addFiles('server/methods/saveSetting.js', 'server'); api.addFiles('server/methods/sendInvitationEmail.js', 'server'); - api.addFiles('server/methods/sendMessage.coffee', 'server'); + api.addFiles('server/methods/sendMessage.js', 'server'); api.addFiles('server/methods/sendSMTPTestEmail.js', 'server'); api.addFiles('server/methods/setAdminStatus.js', 'server'); api.addFiles('server/methods/setRealName.js', 'server'); @@ -196,6 +197,7 @@ Package.onUse(function(api) { // CLIENT MODELS api.addFiles('client/models/_Base.js', 'client'); + api.addFiles('client/models/Avatars.js', 'client'); api.addFiles('client/models/Uploads.js', 'client'); // CLIENT VIEWS @@ -213,10 +215,3 @@ Package.onUse(function(api) { api.imply('tap:i18n'); }); - -Package.onTest(function(api) { - api.use('coffeescript'); - api.use('sanjo:jasmine@0.20.2'); - api.use('rocketchat:lib'); - api.addFiles('tests/jasmine/server/unit/models/_Base.spec.coffee', 'server'); -}); diff --git a/packages/rocketchat-lib/rocketchat.info b/packages/rocketchat-lib/rocketchat.info index d9cac47d18766935672c48d8faf86593c96acaee..7b66ec26c8c824e6ff212f1d9453d46a9f66baaa 100644 --- a/packages/rocketchat-lib/rocketchat.info +++ b/packages/rocketchat-lib/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "0.56.0" + "version": "0.57.1" } diff --git a/packages/rocketchat-lib/server/functions/Notifications.coffee b/packages/rocketchat-lib/server/functions/Notifications.coffee deleted file mode 100644 index 9e08663db6cd6ef6311c11554072217486bd1a67..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/functions/Notifications.coffee +++ /dev/null @@ -1,119 +0,0 @@ -RocketChat.Notifications = new class - constructor: -> - self = @ - - @debug = false - - @streamAll = new Meteor.Streamer 'notify-all' - @streamLogged = new Meteor.Streamer 'notify-logged' - @streamRoom = new Meteor.Streamer 'notify-room' - @streamRoomUsers = new Meteor.Streamer 'notify-room-users' - @streamUser = new Meteor.Streamer 'notify-user' - - - @streamAll.allowWrite('none') - @streamLogged.allowWrite('none') - @streamRoom.allowWrite('none') - @streamRoomUsers.allowWrite (eventName, args...) -> - [roomId, e] = eventName.split('/') - - user = Meteor.users.findOne @userId, {fields: {username: 1}} - if RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(roomId, @userId)? - subscriptions = RocketChat.models.Subscriptions.findByRoomIdAndNotUserId(roomId, @userId).fetch() - for subscription in subscriptions - RocketChat.Notifications.notifyUser(subscription.u._id, e, args...) - - return false - - @streamUser.allowWrite('logged') - - @streamAll.allowRead('all') - - @streamLogged.allowRead('logged') - - @streamRoom.allowRead (eventName) -> - if not @userId? then return false - - roomId = eventName.split('/')[0] - - user = Meteor.users.findOne @userId, {fields: {username: 1}} - room = RocketChat.models.Rooms.findOneById(roomId) - if room.t is 'l' and room.v._id is user._id - return true - - return room.usernames.indexOf(user.username) > -1 - - @streamRoomUsers.allowRead('none'); - - @streamUser.allowRead (eventName) -> - userId = eventName.split('/')[0] - return @userId? and @userId is userId - - - notifyAll: (eventName, args...) -> - console.log 'notifyAll', arguments if @debug is true - - args.unshift eventName - @streamAll.emit.apply @streamAll, args - - notifyLogged: (eventName, args...) -> - console.log 'notifyLogged', arguments if @debug is true - - args.unshift eventName - @streamLogged.emit.apply @streamLogged, args - - notifyRoom: (room, eventName, args...) -> - console.log 'notifyRoom', arguments if @debug is true - - args.unshift "#{room}/#{eventName}" - @streamRoom.emit.apply @streamRoom, args - - notifyUser: (userId, eventName, args...) -> - console.log 'notifyUser', arguments if @debug is true - - args.unshift "#{userId}/#{eventName}" - @streamUser.emit.apply @streamUser, args - - - notifyAllInThisInstance: (eventName, args...) -> - console.log 'notifyAll', arguments if @debug is true - - args.unshift eventName - @streamAll.emitWithoutBroadcast.apply @streamAll, args - - notifyLoggedInThisInstance: (eventName, args...) -> - console.log 'notifyLogged', arguments if @debug is true - - args.unshift eventName - @streamLogged.emitWithoutBroadcast.apply @streamLogged, args - - notifyRoomInThisInstance: (room, eventName, args...) -> - console.log 'notifyRoomAndBroadcast', arguments if @debug is true - - args.unshift "#{room}/#{eventName}" - @streamRoom.emitWithoutBroadcast.apply @streamRoom, args - - notifyUserInThisInstance: (userId, eventName, args...) -> - console.log 'notifyUserAndBroadcast', arguments if @debug is true - - args.unshift "#{userId}/#{eventName}" - @streamUser.emitWithoutBroadcast.apply @streamUser, args - - -## Permissions for client - -# Enable emit for event typing for rooms and add username to event data -func = (eventName, username) -> - [room, e] = eventName.split('/') - - if e is 'webrtc' - return true - - if e is 'typing' - user = Meteor.users.findOne(@userId, {fields: {username: 1}}) - if user?.username is username - return true - - return false - -RocketChat.Notifications.streamRoom.allowWrite func diff --git a/packages/rocketchat-lib/server/functions/Notifications.js b/packages/rocketchat-lib/server/functions/Notifications.js new file mode 100644 index 0000000000000000000000000000000000000000..89fec97a27998423a5ab0492c50b6bec0e6e3d98 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/Notifications.js @@ -0,0 +1,132 @@ +RocketChat.Notifications = new class { + constructor() { + this.debug = false; + this.streamAll = new Meteor.Streamer('notify-all'); + this.streamLogged = new Meteor.Streamer('notify-logged'); + this.streamRoom = new Meteor.Streamer('notify-room'); + this.streamRoomUsers = new Meteor.Streamer('notify-room-users'); + this.streamUser = new Meteor.Streamer('notify-user'); + this.streamAll.allowWrite('none'); + this.streamLogged.allowWrite('none'); + this.streamRoom.allowWrite('none'); + this.streamRoomUsers.allowWrite(function(eventName, ...args) { + const [roomId, e] = eventName.split('/'); + // const user = Meteor.users.findOne(this.userId, { + // fields: { + // username: 1 + // } + // }); + if (RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId) != null) { + const subscriptions = RocketChat.models.Subscriptions.findByRoomIdAndNotUserId(roomId, this.userId).fetch(); + subscriptions.forEach(subscription => RocketChat.Notifications.notifyUser(subscription.u._id, e, ...args)); + } + return false; + }); + this.streamUser.allowWrite('logged'); + this.streamAll.allowRead('all'); + this.streamLogged.allowRead('logged'); + this.streamRoom.allowRead(function(eventName) { + if (this.userId == null) { + return false; + } + const [roomId] = eventName.split('/'); + const user = Meteor.users.findOne(this.userId, { + fields: { + username: 1 + } + }); + const room = RocketChat.models.Rooms.findOneById(roomId); + if (room.t === 'l' && room.v._id === user._id) { + return true; + } + return room.usernames.indexOf(user.username) > -1; + }); + this.streamRoomUsers.allowRead('none'); + this.streamUser.allowRead(function(eventName) { + const [userId] = eventName.split('/'); + return (this.userId != null) && this.userId === userId; + }); + } + + notifyAll(eventName, ...args) { + if (this.debug === true) { + console.log('notifyAll', arguments); + } + args.unshift(eventName); + return this.streamAll.emit.apply(this.streamAll, args); + } + + notifyLogged(eventName, ...args) { + if (this.debug === true) { + console.log('notifyLogged', arguments); + } + args.unshift(eventName); + return this.streamLogged.emit.apply(this.streamLogged, args); + } + + notifyRoom(room, eventName, ...args) { + if (this.debug === true) { + console.log('notifyRoom', arguments); + } + args.unshift(`${ room }/${ eventName }`); + return this.streamRoom.emit.apply(this.streamRoom, args); + } + + notifyUser(userId, eventName, ...args) { + if (this.debug === true) { + console.log('notifyUser', arguments); + } + args.unshift(`${ userId }/${ eventName }`); + return this.streamUser.emit.apply(this.streamUser, args); + } + + notifyAllInThisInstance(eventName, ...args) { + if (this.debug === true) { + console.log('notifyAll', arguments); + } + args.unshift(eventName); + return this.streamAll.emitWithoutBroadcast.apply(this.streamAll, args); + } + + notifyLoggedInThisInstance(eventName, ...args) { + if (this.debug === true) { + console.log('notifyLogged', arguments); + } + args.unshift(eventName); + return this.streamLogged.emitWithoutBroadcast.apply(this.streamLogged, args); + } + + notifyRoomInThisInstance(room, eventName, ...args) { + if (this.debug === true) { + console.log('notifyRoomAndBroadcast', arguments); + } + args.unshift(`${ room }/${ eventName }`); + return this.streamRoom.emitWithoutBroadcast.apply(this.streamRoom, args); + } + + notifyUserInThisInstance(userId, eventName, ...args) { + if (this.debug === true) { + console.log('notifyUserAndBroadcast', arguments); + } + args.unshift(`${ userId }/${ eventName }`); + return this.streamUser.emitWithoutBroadcast.apply(this.streamUser, args); + } +}; + +RocketChat.Notifications.streamRoom.allowWrite(function(eventName, username) { + const [, e] = eventName.split('/'); + if (e === 'webrtc') { + return true; + } + if (e === 'typing') { + const user = Meteor.users.findOne(this.userId, { + fields: { + username: 1 + } + }); + if (user != null && user.username === username) { + return true; + } + } + return false; +}); diff --git a/packages/rocketchat-lib/server/functions/checkUsernameAvailability.coffee b/packages/rocketchat-lib/server/functions/checkUsernameAvailability.coffee deleted file mode 100644 index 987b8e2af2b2ffa06c191e2672b815831a58980e..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/functions/checkUsernameAvailability.coffee +++ /dev/null @@ -1,10 +0,0 @@ -RocketChat.checkUsernameAvailability = (username) -> - usernameBlackList = [] - RocketChat.settings.get('Accounts_BlockedUsernameList', (key, value) => - usernameBlackList = _.map(value.split(','), (username) => username.trim()) - if usernameBlackList.length isnt 0 - for restrictedUsername in usernameBlackList - regex = new RegExp('^' + s.escapeRegExp(restrictedUsername) + '$', 'i') - return false if regex.test(s.trim(s.escapeRegExp(username))) - return not Meteor.users.findOne({ username: { $regex : new RegExp("^" + s.trim(s.escapeRegExp(username)) + "$", "i") } }); - ) diff --git a/packages/rocketchat-lib/server/functions/checkUsernameAvailability.js b/packages/rocketchat-lib/server/functions/checkUsernameAvailability.js new file mode 100644 index 0000000000000000000000000000000000000000..01e8f8c1b1069264fa686c963ea5e59fb19e355f --- /dev/null +++ b/packages/rocketchat-lib/server/functions/checkUsernameAvailability.js @@ -0,0 +1,20 @@ +RocketChat.checkUsernameAvailability = function(username) { + return RocketChat.settings.get('Accounts_BlockedUsernameList', function(key, value) { + const usernameBlackList = _.map(value.split(','), function(username) { + return username.trim(); + }); + if (usernameBlackList.length !== 0) { + if (usernameBlackList.every(restrictedUsername => { + const regex = new RegExp(`^${ s.escapeRegExp(restrictedUsername) }$`, 'i'); + return !regex.test(s.trim(s.escapeRegExp(username))); + })) { + return !Meteor.users.findOne({ + username: { + $regex: new RegExp(`^${ s.trim(s.escapeRegExp(username)) }$`, 'i') + } + }); + } + return false; + } + }); +}; diff --git a/packages/rocketchat-lib/server/functions/deleteMessage.js b/packages/rocketchat-lib/server/functions/deleteMessage.js index 7a190964be11e7ff2674fa262c0b49c4c9dbb428..50fd5999864e07723e5fd4962d895cd0921b8513 100644 --- a/packages/rocketchat-lib/server/functions/deleteMessage.js +++ b/packages/rocketchat-lib/server/functions/deleteMessage.js @@ -21,7 +21,7 @@ RocketChat.deleteMessage = function(message, user) { } if (message.file && message.file._id) { - FileUpload.delete(message.file._id); + FileUpload.getStore('Uploads').deleteById(message.file._id); } Meteor.defer(function() { diff --git a/packages/rocketchat-lib/server/functions/deleteUser.js b/packages/rocketchat-lib/server/functions/deleteUser.js index a139389b064d7880c12bde7001460e4e11102d57..ab7ec6719b94469169827945d007d0149adefa21 100644 --- a/packages/rocketchat-lib/server/functions/deleteUser.js +++ b/packages/rocketchat-lib/server/functions/deleteUser.js @@ -1,4 +1,3 @@ -/* globals RocketChat */ RocketChat.deleteUser = function(userId) { const user = RocketChat.models.Users.findOneById(userId); @@ -22,7 +21,7 @@ RocketChat.deleteUser = function(userId) { // removes user's avatar if (user.avatarOrigin === 'upload' || user.avatarOrigin === 'url') { - RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${ user.username }.jpg`)); + FileUpload.getStore('Avatars').deleteByName(user.username); } RocketChat.models.Integrations.disableByUserId(userId); // Disables all the integrations which rely on the user being deleted. diff --git a/packages/rocketchat-lib/server/functions/saveUser.js b/packages/rocketchat-lib/server/functions/saveUser.js index 540479df0afb9e5c2125d3165b7a4d9284c5b969..fade4261aaffad07d5cc41e61e858375e07f589c 100644 --- a/packages/rocketchat-lib/server/functions/saveUser.js +++ b/packages/rocketchat-lib/server/functions/saveUser.js @@ -73,7 +73,7 @@ RocketChat.saveUser = function(userId, userData) { } }; - if (userData.requirePasswordChange) { + if (typeof userData.requirePasswordChange !== 'undefined') { updateUser.$set.requirePasswordChange = userData.requirePasswordChange; } @@ -160,7 +160,7 @@ RocketChat.saveUser = function(userId, userData) { updateUser.$set.roles = userData.roles; } - if (userData.requirePasswordChange) { + if (typeof userData.requirePasswordChange !== 'undefined') { updateUser.$set.requirePasswordChange = userData.requirePasswordChange; } diff --git a/packages/rocketchat-lib/server/functions/sendMessage.coffee b/packages/rocketchat-lib/server/functions/sendMessage.coffee deleted file mode 100644 index bdce87faba4d40f69610db48e1f17fe269834568..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/functions/sendMessage.coffee +++ /dev/null @@ -1,50 +0,0 @@ -RocketChat.sendMessage = (user, message, room, upsert = false) -> - if not user or not message or not room._id - return false - - unless message.ts? - message.ts = new Date() - - message.u = _.pick user, ['_id','username'] - - if not Match.test(message.msg, String) - message.msg = '' - - message.rid = room._id - - if not room.usernames? || room.usernames.length is 0 - updated_room = RocketChat.models.Rooms.findOneById(room._id) - if updated_room? - room = updated_room - else - room.usernames = [] - - if message.parseUrls isnt false - if urls = message.msg.match /([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g - message.urls = urls.map (url) -> url: url - - message = RocketChat.callbacks.run 'beforeSaveMessage', message - - # Avoid saving sandstormSessionId to the database - sandstormSessionId = null - if message.sandstormSessionId - sandstormSessionId = message.sandstormSessionId - delete message.sandstormSessionId - - if message._id? and upsert - _id = message._id - delete message._id - RocketChat.models.Messages.upsert {_id: _id, 'u._id': message.u._id}, message - message._id = _id - else - message._id = RocketChat.models.Messages.insert message - - ### - Defer other updates as their return is not interesting to the user - ### - Meteor.defer -> - # Execute all callbacks - message.sandstormSessionId = sandstormSessionId - RocketChat.callbacks.run 'afterSaveMessage', message, room - - return message diff --git a/packages/rocketchat-lib/server/functions/sendMessage.js b/packages/rocketchat-lib/server/functions/sendMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..ee953ee04a20f614f62cd2ccdadf779d9347e035 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/sendMessage.js @@ -0,0 +1,59 @@ +RocketChat.sendMessage = function(user, message, room, upsert = false) { + if (!user || !message || !room._id) { + return false; + } + if (message.ts == null) { + message.ts = new Date(); + } + message.u = _.pick(user, ['_id', 'username', 'name']); + if (!Match.test(message.msg, String)) { + message.msg = ''; + } + message.rid = room._id; + if (!room.usernames || room.usernames.length === 0) { + const updated_room = RocketChat.models.Rooms.findOneById(room._id); + if (updated_room != null) { + room = updated_room; + } else { + room.usernames = []; + } + } + if (message.parseUrls !== false) { + const urls = message.msg.match(/([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g); + if (urls) { + message.urls = urls.map(function(url) { + return { + url + }; + }); + } + } + message = RocketChat.callbacks.run('beforeSaveMessage', message); + // Avoid saving sandstormSessionId to the database + let sandstormSessionId = null; + if (message.sandstormSessionId) { + sandstormSessionId = message.sandstormSessionId; + delete message.sandstormSessionId; + } + if (message._id && upsert) { + const _id = message._id; + delete message._id; + RocketChat.models.Messages.upsert({ + _id, + 'u._id': message.u._id + }, message); + message._id = _id; + } else { + message._id = RocketChat.models.Messages.insert(message); + } + + /* + Defer other updates as their return is not interesting to the user + */ + Meteor.defer(() => { + // Execute all callbacks + message.sandstormSessionId = sandstormSessionId; + return RocketChat.callbacks.run('afterSaveMessage', message, room); + }); + return message; +}; diff --git a/packages/rocketchat-lib/server/functions/setUserAvatar.js b/packages/rocketchat-lib/server/functions/setUserAvatar.js index e80a8c7cdd70f83523908f8219c23148b9aaf9d8..480688d7904b39868808f30f171a59e2d31ac441 100644 --- a/packages/rocketchat-lib/server/functions/setUserAvatar.js +++ b/packages/rocketchat-lib/server/functions/setUserAvatar.js @@ -10,7 +10,7 @@ RocketChat.setUserAvatar = function(user, dataURI, contentType, service) { try { result = HTTP.get(dataURI, { npmRequestOptions: {encoding: 'binary'} }); } catch (error) { - if (error.response.statusCode !== 404) { + if (!error.response || error.response.statusCode !== 404) { console.log(`Error while handling the setting of the avatar from a url (${ dataURI }) for ${ user.username }:`, error); throw new Meteor.Error('error-avatar-url-handling', `Error while handling avatar setting from a URL (${ dataURI }) for ${ user.username }`, { function: 'RocketChat.setUserAvatar', url: dataURI, username: user.username }); } @@ -39,14 +39,20 @@ RocketChat.setUserAvatar = function(user, dataURI, contentType, service) { contentType = fileData.contentType; } - const rs = RocketChatFile.bufferToStream(new Buffer(image, encoding)); - RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${ user.username }.jpg`)); - const ws = RocketChatFileAvatarInstance.createWriteStream(encodeURIComponent(`${ user.username }.jpg`), contentType); - ws.on('end', Meteor.bindEnvironment(function() { + const buffer = new Buffer(image, encoding); + const fileStore = FileUpload.getStore('Avatars'); + fileStore.deleteByName(user.username); + + const file = { + userId: user._id, + type: contentType, + size: buffer.length + }; + + fileStore.insert(file, buffer, () => { Meteor.setTimeout(function() { RocketChat.models.Users.setAvatarOrigin(user._id, service); RocketChat.Notifications.notifyLogged('updateAvatar', {username: user.username}); }, 500); - })); - rs.pipe(ws); + }); }; diff --git a/packages/rocketchat-lib/server/functions/setUsername.coffee b/packages/rocketchat-lib/server/functions/setUsername.coffee deleted file mode 100644 index a3afa4a49e9e0d89febac37cfee8ce018396d98d..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/functions/setUsername.coffee +++ /dev/null @@ -1,77 +0,0 @@ -RocketChat._setUsername = (userId, username) -> - username = s.trim username - if not userId or not username - return false - - try - nameValidation = new RegExp '^' + RocketChat.settings.get('UTF8_Names_Validation') + '$' - catch - nameValidation = new RegExp '^[0-9a-zA-Z-_.]+$' - - if not nameValidation.test username - return false - - user = RocketChat.models.Users.findOneById userId - - # User already has desired username, return - if user.username is username - return user - - previousUsername = user.username - - # Check username availability or if the user already owns a different casing of the name - if ( !previousUsername or !(username.toLowerCase() == previousUsername.toLowerCase())) - unless RocketChat.checkUsernameAvailability username - return false - - # If first time setting username, send Enrollment Email - try - if not previousUsername and user.emails?.length > 0 and RocketChat.settings.get 'Accounts_Enrollment_Email' - Accounts.sendEnrollmentEmail(user._id) - catch error - - user.username = username - - # If first time setting username, check if should set default avatar - if not previousUsername and RocketChat.settings.get('Accounts_SetDefaultAvatar') is true - avatarSuggestions = getAvatarSuggestionForUser user - for service, avatarData of avatarSuggestions - if service isnt 'gravatar' - RocketChat.setUserAvatar(user, avatarData.blob, avatarData.contentType, service) - gravatar = null - break - else - gravatar = avatarData - if gravatar? - RocketChat.setUserAvatar(user, gravatar.blob, gravatar.contentType, 'gravatar') - - # Username is available; if coming from old username, update all references - if previousUsername - RocketChat.models.Messages.updateAllUsernamesByUserId user._id, username - RocketChat.models.Messages.updateUsernameOfEditByUserId user._id, username - - RocketChat.models.Messages.findByMention(previousUsername).forEach (msg) -> - updatedMsg = msg.msg.replace(new RegExp("@#{previousUsername}", "ig"), "@#{username}") - RocketChat.models.Messages.updateUsernameAndMessageOfMentionByIdAndOldUsername msg._id, previousUsername, username, updatedMsg - - RocketChat.models.Rooms.replaceUsername previousUsername, username - RocketChat.models.Rooms.replaceMutedUsername previousUsername, username - RocketChat.models.Rooms.replaceUsernameOfUserByUserId user._id, username - - RocketChat.models.Subscriptions.setUserUsernameByUserId user._id, username - RocketChat.models.Subscriptions.setNameForDirectRoomsWithOldName previousUsername, username - - rs = RocketChatFileAvatarInstance.getFileWithReadStream(encodeURIComponent("#{previousUsername}.jpg")) - if rs? - RocketChatFileAvatarInstance.deleteFile encodeURIComponent("#{username}.jpg") - ws = RocketChatFileAvatarInstance.createWriteStream encodeURIComponent("#{username}.jpg"), rs.contentType - ws.on 'end', Meteor.bindEnvironment -> - RocketChatFileAvatarInstance.deleteFile encodeURIComponent("#{previousUsername}.jpg") - rs.readStream.pipe(ws) - - # Set new username - RocketChat.models.Users.setUsername user._id, username - return user - -RocketChat.setUsername = RocketChat.RateLimiter.limitFunction RocketChat._setUsername, 1, 60000, - 0: () -> return not Meteor.userId() or not RocketChat.authz.hasPermission(Meteor.userId(), 'edit-other-user-info') # Administrators have permission to change others usernames, so don't limit those diff --git a/packages/rocketchat-lib/server/functions/setUsername.js b/packages/rocketchat-lib/server/functions/setUsername.js new file mode 100644 index 0000000000000000000000000000000000000000..20eeabdccf0fcdac67843379fe5024aba42bb5b4 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/setUsername.js @@ -0,0 +1,84 @@ + +RocketChat._setUsername = function(userId, u) { + const username = s.trim(u); + if (!userId || !username) { + return false; + } + let nameValidation; + try { + nameValidation = new RegExp(`^${ RocketChat.settings.get('UTF8_Names_Validation') }$`); + } catch (error) { + nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); + } + if (!nameValidation.test(username)) { + return false; + } + const user = RocketChat.models.Users.findOneById(userId); + // User already has desired username, return + if (user.username === username) { + return user; + } + const previousUsername = user.username; + // Check username availability or if the user already owns a different casing of the name + if (!previousUsername || !(username.toLowerCase() === previousUsername.toLowerCase())) { + if (!RocketChat.checkUsernameAvailability(username)) { + return false; + } + } + //If first time setting username, send Enrollment Email + try { + if (!previousUsername && user.emails && user.emails.length > 0 && RocketChat.settings.get('Accounts_Enrollment_Email')) { + Accounts.sendEnrollmentEmail(user._id); + } + } catch (e) { + console.error(e); + } + /* globals getAvatarSuggestionForUser */ + user.username = username; + if (!previousUsername && RocketChat.settings.get('Accounts_SetDefaultAvatar') === true) { + const avatarSuggestions = getAvatarSuggestionForUser(user); + let gravatar; + Object.keys(avatarSuggestions).some(service => { + const avatarData = avatarSuggestions[service]; + if (service !== 'gravatar') { + RocketChat.setUserAvatar(user, avatarData.blob, avatarData.contentType, service); + gravatar = null; + return true; + } else { + gravatar = avatarData; + } + }); + if (gravatar != null) { + RocketChat.setUserAvatar(user, gravatar.blob, gravatar.contentType, 'gravatar'); + } + } + // Username is available; if coming from old username, update all references + if (previousUsername) { + RocketChat.models.Messages.updateAllUsernamesByUserId(user._id, username); + RocketChat.models.Messages.updateUsernameOfEditByUserId(user._id, username); + RocketChat.models.Messages.findByMention(previousUsername).forEach(function(msg) { + const updatedMsg = msg.msg.replace(new RegExp(`@${ previousUsername }`, 'ig'), `@${ username }`); + return RocketChat.models.Messages.updateUsernameAndMessageOfMentionByIdAndOldUsername(msg._id, previousUsername, username, updatedMsg); + }); + RocketChat.models.Rooms.replaceUsername(previousUsername, username); + RocketChat.models.Rooms.replaceMutedUsername(previousUsername, username); + RocketChat.models.Rooms.replaceUsernameOfUserByUserId(user._id, username); + RocketChat.models.Subscriptions.setUserUsernameByUserId(user._id, username); + RocketChat.models.Subscriptions.setNameForDirectRoomsWithOldName(previousUsername, username); + + const fileStore = FileUpload.getStore('Avatars'); + const file = fileStore.model.findOneByName(previousUsername); + if (file) { + fileStore.model.updateFileNameById(file._id, username); + } + } + // Set new username* + RocketChat.models.Users.setUsername(user._id, username); + return user; +}; + +RocketChat.setUsername = RocketChat.RateLimiter.limitFunction(RocketChat._setUsername, 1, 60000, { + [0](userId) { + return !userId || !RocketChat.authz.hasPermission(userId, 'edit-other-user-info'); + } +}); diff --git a/packages/rocketchat-lib/server/functions/settings.coffee b/packages/rocketchat-lib/server/functions/settings.coffee deleted file mode 100644 index 1098cd7941da3552922513f053de3f46d9c7ae8b..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/functions/settings.coffee +++ /dev/null @@ -1,248 +0,0 @@ -blockedSettings = {} -process.env.SETTINGS_BLOCKED?.split(',').forEach (settingId) -> - blockedSettings[settingId] = 1 - -hiddenSettings = {} -process.env.SETTINGS_HIDDEN?.split(',').forEach (settingId) -> - hiddenSettings[settingId] = 1 - -RocketChat.settings._sorter = {} - -### -# Add a setting -# @param {String} _id -# @param {Mixed} value -# @param {Object} setting -### -RocketChat.settings.add = (_id, value, options = {}) -> - # console.log '[functions] RocketChat.settings.add -> '.green, 'arguments:', arguments - - if not _id or - not value? and not process?.env?['OVERWRITE_SETTING_' + _id]? - return false - - RocketChat.settings._sorter[options.group] ?= 0 - - options.packageValue = value - options.valueSource = 'packageValue' - options.hidden = false - options.blocked = options.blocked || false - options.sorter ?= RocketChat.settings._sorter[options.group]++ - - if options.enableQuery? - options.enableQuery = JSON.stringify options.enableQuery - - if options.i18nDefaultQuery? - options.i18nDefaultQuery = JSON.stringify options.i18nDefaultQuery - - if process?.env?[_id]? - value = process.env[_id] - if value.toLowerCase() is "true" - value = true - else if value.toLowerCase() is "false" - value = false - options.processEnvValue = value - options.valueSource = 'processEnvValue' - - else if Meteor.settings?[_id]? - value = Meteor.settings[_id] - options.meteorSettingsValue = value - options.valueSource = 'meteorSettingsValue' - - if not options.i18nLabel? - options.i18nLabel = _id - - # Default description i18n key will be the setting name + "_Description" (eg: LDAP_Enable -> LDAP_Enable_Description) - if not options.i18nDescription? - options.i18nDescription = "#{_id}_Description" - - if blockedSettings[_id]? - options.blocked = true - - if hiddenSettings[_id]? - options.hidden = true - - if process?.env?['OVERWRITE_SETTING_' + _id]? - value = process.env['OVERWRITE_SETTING_' + _id] - if value.toLowerCase() is "true" - value = true - else if value.toLowerCase() is "false" - value = false - options.value = value - options.processEnvValue = value - options.valueSource = 'processEnvValue' - - updateOperations = - $set: options - $setOnInsert: - createdAt: new Date - - if options.editor? - updateOperations.$setOnInsert.editor = options.editor - delete options.editor - - if not options.value? - if options.force is true - updateOperations.$set.value = options.packageValue - else - updateOperations.$setOnInsert.value = value - - query = _.extend { _id: _id }, updateOperations.$set - - if not options.section? - updateOperations.$unset = { section: 1 } - query.section = { $exists: false } - - existantSetting = RocketChat.models.Settings.db.findOne(query) - - if existantSetting? - if not existantSetting.editor? and updateOperations.$setOnInsert.editor? - updateOperations.$set.editor = updateOperations.$setOnInsert.editor - delete updateOperations.$setOnInsert.editor - else - updateOperations.$set.ts = new Date - - return RocketChat.models.Settings.upsert { _id: _id }, updateOperations - - - -### -# Add a setting group -# @param {String} _id -### -RocketChat.settings.addGroup = (_id, options = {}, cb) -> - # console.log '[functions] RocketChat.settings.addGroup -> '.green, 'arguments:', arguments - - if not _id - return false - - if _.isFunction(options) - cb = options - options = {} - - if not options.i18nLabel? - options.i18nLabel = _id - - if not options.i18nDescription? - options.i18nDescription = "#{_id}_Description" - - options.ts = new Date - options.blocked = false - options.hidden = false - - if blockedSettings[_id]? - options.blocked = true - - if hiddenSettings[_id]? - options.hidden = true - - RocketChat.models.Settings.upsert { _id: _id }, - $set: options - $setOnInsert: - type: 'group' - createdAt: new Date - - if cb? - cb.call - add: (id, value, options = {}) -> - options.group = _id - RocketChat.settings.add id, value, options - - section: (section, cb) -> - cb.call - add: (id, value, options = {}) -> - options.group = _id - options.section = section - RocketChat.settings.add id, value, options - - return - - -### -# Remove a setting by id -# @param {String} _id -### -RocketChat.settings.removeById = (_id) -> - # console.log '[functions] RocketChat.settings.add -> '.green, 'arguments:', arguments - - if not _id - return false - - return RocketChat.models.Settings.removeById _id - - -### -# Update a setting by id -# @param {String} _id -### -RocketChat.settings.updateById = (_id, value, editor) -> - # console.log '[functions] RocketChat.settings.updateById -> '.green, 'arguments:', arguments - - if not _id or not value? - return false - - if editor? - return RocketChat.models.Settings.updateValueAndEditorById _id, value, editor - - return RocketChat.models.Settings.updateValueById _id, value - - -### -# Update options of a setting by id -# @param {String} _id -### -RocketChat.settings.updateOptionsById = (_id, options) -> - # console.log '[functions] RocketChat.settings.updateOptionsById -> '.green, 'arguments:', arguments - - if not _id or not options? - return false - - return RocketChat.models.Settings.updateOptionsById _id, options - - -### -# Update a setting by id -# @param {String} _id -### -RocketChat.settings.clearById = (_id) -> - # console.log '[functions] RocketChat.settings.clearById -> '.green, 'arguments:', arguments - - if not _id? - return false - - return RocketChat.models.Settings.updateValueById _id, undefined - - -### -# Update a setting by id -### -RocketChat.settings.init = -> - RocketChat.settings.initialLoad = true - RocketChat.models.Settings.find().observe - added: (record) -> - Meteor.settings[record._id] = record.value - if record.env is true - process.env[record._id] = record.value - RocketChat.settings.load record._id, record.value, RocketChat.settings.initialLoad - changed: (record) -> - Meteor.settings[record._id] = record.value - if record.env is true - process.env[record._id] = record.value - RocketChat.settings.load record._id, record.value, RocketChat.settings.initialLoad - removed: (record) -> - delete Meteor.settings[record._id] - if record.env is true - delete process.env[record._id] - RocketChat.settings.load record._id, undefined, RocketChat.settings.initialLoad - RocketChat.settings.initialLoad = false - - for fn in RocketChat.settings.afterInitialLoad - fn(Meteor.settings) - - -RocketChat.settings.afterInitialLoad = [] - -RocketChat.settings.onAfterInitialLoad = (fn) -> - RocketChat.settings.afterInitialLoad.push(fn) - if RocketChat.settings.initialLoad is false - fn(Meteor.settings) diff --git a/packages/rocketchat-lib/server/functions/settings.js b/packages/rocketchat-lib/server/functions/settings.js new file mode 100644 index 0000000000000000000000000000000000000000..4fa9a3f2b40af858700b26cae989fed25493cb51 --- /dev/null +++ b/packages/rocketchat-lib/server/functions/settings.js @@ -0,0 +1,283 @@ +const blockedSettings = {}; + +if (process.env.SETTINGS_BLOCKED) { + process.env.SETTINGS_BLOCKED.split(',').forEach((settingId) => blockedSettings[settingId] = 1); +} + +const hiddenSettings = {}; +if (process.env.SETTINGS_HIDDEN) { + process.env.SETTINGS_HIDDEN.split(',').forEach((settingId) => hiddenSettings[settingId] = 1); +} + +RocketChat.settings._sorter = {}; + + +/* +* Add a setting +* @param {String} _id +* @param {Mixed} value +* @param {Object} setting +*/ + +RocketChat.settings.add = function(_id, value, options = {}) { + if (options == null) { + options = {}; + } + if (!_id || value == null) { + return false; + } + if (RocketChat.settings._sorter[options.group] == null) { + RocketChat.settings._sorter[options.group] = 0; + } + options.packageValue = value; + options.valueSource = 'packageValue'; + options.hidden = false; + options.blocked = options.blocked || false; + if (options.sorter == null) { + options.sorter = RocketChat.settings._sorter[options.group]++; + } + if (options.enableQuery != null) { + options.enableQuery = JSON.stringify(options.enableQuery); + } + if (options.i18nDefaultQuery != null) { + options.i18nDefaultQuery = JSON.stringify(options.i18nDefaultQuery); + } + if (typeof process !== 'undefined' && process.env && process.env._id) { + let value = process.env[_id]; + if (value.toLowerCase() === 'true') { + value = true; + } else if (value.toLowerCase() === 'false') { + value = false; + } + options.processEnvValue = value; + options.valueSource = 'processEnvValue'; + } else if (Meteor.settings && Meteor.settings) { + const value = Meteor.settings[_id]; + options.meteorSettingsValue = value; + options.valueSource = 'meteorSettingsValue'; + } + if (options.i18nLabel == null) { + options.i18nLabel = _id; + } + if (options.i18nDescription == null) { + options.i18nDescription = `${ _id }_Description`; + } + if (blockedSettings[_id] != null) { + options.blocked = true; + } + if (hiddenSettings[_id] != null) { + options.hidden = true; + } + if (typeof process !== 'undefined' && process.env && process.env[`OVERWRITE_SETTING_${ _id }`]) { + let value = process.env[`OVERWRITE_SETTING_${ _id }`]; + if (value.toLowerCase() === 'true') { + value = true; + } else if (value.toLowerCase() === 'false') { + value = false; + } + options.value = value; + options.processEnvValue = value; + options.valueSource = 'processEnvValue'; + } + const updateOperations = { + $set: options, + $setOnInsert: { + createdAt: new Date + } + }; + if (options.editor != null) { + updateOperations.$setOnInsert.editor = options.editor; + delete options.editor; + } + if (options.value == null) { + if (options.force === true) { + updateOperations.$set.value = options.packageValue; + } else { + updateOperations.$setOnInsert.value = value; + } + } + const query = _.extend({ + _id + }, updateOperations.$set); + if (options.section == null) { + updateOperations.$unset = { + section: 1 + }; + query.section = { + $exists: false + }; + } + const existantSetting = RocketChat.models.Settings.db.findOne(query); + if (existantSetting != null) { + if (existantSetting.editor == null && updateOperations.$setOnInsert.editor != null) { + updateOperations.$set.editor = updateOperations.$setOnInsert.editor; + delete updateOperations.$setOnInsert.editor; + } + } else { + updateOperations.$set.ts = new Date; + } + return RocketChat.models.Settings.upsert({ + _id + }, updateOperations); +}; + + +/* +* Add a setting group +* @param {String} _id +*/ + +RocketChat.settings.addGroup = function(_id, options = {}, cb) { + if (!_id) { + return false; + } + if (_.isFunction(options)) { + cb = options; + options = {}; + } + if (options.i18nLabel == null) { + options.i18nLabel = _id; + } + if (options.i18nDescription == null) { + options.i18nDescription = `${ _id }_Description`; + } + options.ts = new Date; + options.blocked = false; + options.hidden = false; + if (blockedSettings[_id] != null) { + options.blocked = true; + } + if (hiddenSettings[_id] != null) { + options.hidden = true; + } + RocketChat.models.Settings.upsert({ + _id + }, { + $set: options, + $setOnInsert: { + type: 'group', + createdAt: new Date + } + }); + if (cb != null) { + cb.call({ + add(id, value, options) { + if (options == null) { + options = {}; + } + options.group = _id; + return RocketChat.settings.add(id, value, options); + }, + section(section, cb) { + return cb.call({ + add(id, value, options) { + if (options == null) { + options = {}; + } + options.group = _id; + options.section = section; + return RocketChat.settings.add(id, value, options); + } + }); + } + }); + } +}; + + +/* +* Remove a setting by id +* @param {String} _id +*/ + +RocketChat.settings.removeById = function(_id) { + if (!_id) { + return false; + } + return RocketChat.models.Settings.removeById(_id); +}; + + +/* +* Update a setting by id +* @param {String} _id +*/ + +RocketChat.settings.updateById = function(_id, value, editor) { + if (!_id || value == null) { + return false; + } + if (editor != null) { + return RocketChat.models.Settings.updateValueAndEditorById(_id, value, editor); + } + return RocketChat.models.Settings.updateValueById(_id, value); +}; + + +/* +* Update options of a setting by id +* @param {String} _id +*/ + +RocketChat.settings.updateOptionsById = function(_id, options) { + if (!_id || options == null) { + return false; + } + return RocketChat.models.Settings.updateOptionsById(_id, options); +}; + + +/* +* Update a setting by id +* @param {String} _id +*/ + +RocketChat.settings.clearById = function(_id) { + if (_id == null) { + return false; + } + return RocketChat.models.Settings.updateValueById(_id, undefined); +}; + + +/* +* Update a setting by id +*/ + +RocketChat.settings.init = function() { + RocketChat.settings.initialLoad = true; + RocketChat.models.Settings.find().observe({ + added(record) { + Meteor.settings[record._id] = record.value; + if (record.env === true) { + process.env[record._id] = record.value; + } + return RocketChat.settings.load(record._id, record.value, RocketChat.settings.initialLoad); + }, + changed(record) { + Meteor.settings[record._id] = record.value; + if (record.env === true) { + process.env[record._id] = record.value; + } + return RocketChat.settings.load(record._id, record.value, RocketChat.settings.initialLoad); + }, + removed(record) { + delete Meteor.settings[record._id]; + if (record.env === true) { + delete process.env[record._id]; + } + return RocketChat.settings.load(record._id, undefined, RocketChat.settings.initialLoad); + } + }); + RocketChat.settings.initialLoad = false; + RocketChat.settings.afterInitialLoad.forEach(fn => fn(Meteor.settings)); +}; + +RocketChat.settings.afterInitialLoad = []; + +RocketChat.settings.onAfterInitialLoad = function(fn) { + RocketChat.settings.afterInitialLoad.push(fn); + if (RocketChat.settings.initialLoad === false) { + return fn(Meteor.settings); + } +}; diff --git a/packages/rocketchat-lib/server/lib/PushNotification.js b/packages/rocketchat-lib/server/lib/PushNotification.js index fd1bdb8db6047f74c112d76ed6a8da96a97eddba..fca87e6455a869ddca2569533480a4802b12dec1 100644 --- a/packages/rocketchat-lib/server/lib/PushNotification.js +++ b/packages/rocketchat-lib/server/lib/PushNotification.js @@ -16,7 +16,7 @@ class PushNotification { return hash; } - send({ roomName, roomId, username, message, usersTo, payload }) { + send({ roomName, roomId, username, message, usersTo, payload, badge = 1 }) { let title; if (roomName && roomName !== '') { title = `${ roomName }`; @@ -27,7 +27,7 @@ class PushNotification { const icon = RocketChat.settings.get('Assets_favicon_192').url || RocketChat.settings.get('Assets_favicon_192').defaultUrl; const config = { from: 'push', - badge: 1, + badge, sound: 'default', title, text: message, diff --git a/packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js b/packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js index 4d1d5d3fe0fc5026a15763c697b66aafac3a546d..aab32579decd4bdee06c057929e44259950b0144 100644 --- a/packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js +++ b/packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js @@ -44,6 +44,7 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { RocketChat.models.Subscriptions.incUnreadOfDirectForRoomIdExcludingUserId(message.rid, message.u._id, 1); } else { let toAll = false; + let toHere = false; const mentionIds = []; const highlightsIds = []; const highlights = RocketChat.models.Users.findUsersByUsernamesWithHighlights(room.usernames, { fields: { '_id': 1, 'settings.preferences.highlights': 1 }}).fetch(); @@ -53,6 +54,9 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { if (!toAll && mention._id === 'all') { toAll = true; } + if (!toHere && mention._id === 'here') { + toHere = true; + } if (mention._id !== message.u._id) { mentionIds.push(mention._id); } @@ -67,7 +71,7 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { } }); - if (toAll) { + if (toAll || toHere) { RocketChat.models.Subscriptions.incUnreadForRoomIdExcludingUserId(room._id, message.u._id); } else if ((mentionIds && mentionIds.length > 0) || (highlightsIds && highlightsIds.length > 0)) { RocketChat.models.Subscriptions.incUnreadForRoomIdAndUserIds(room._id, _.compact(_.unique(mentionIds.concat(highlightsIds)))); diff --git a/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js b/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js index ea6ef470118a7f2b16568127f7e02eb2154c4700..7beab8fdfcfa8bcd9838b2d3bea1f0da461f1121 100644 --- a/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js +++ b/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js @@ -1,6 +1,89 @@ /* globals Push */ import moment from 'moment'; +/** + * Replaces @username with full name + * + * @param {string} message The message to replace + * @param {object[]} mentions Array of mentions used to make replacements + * + * @returns {string} + */ +function replaceMentionedUsernamesWithFullNames(message, mentions) { + if (!mentions || !mentions.length) { + return message; + } + mentions.forEach((mention) => { + const user = RocketChat.models.Users.findOneById(mention._id); + if (user && user.name) { + message = message.replace(`@${ mention.username }`, user.name); + } + }); + return message; +} + +/** + * Send notification to user + * + * @param {string} userId The user to notify + * @param {object} user The sender + * @param {object} room The room send from + * @param {number} duration Duration of notification + */ +function notifyUser(userId, user, message, room, duration) { + const UI_Use_Real_Name = RocketChat.settings.get('UI_Use_Real_Name') === true; + if (UI_Use_Real_Name) { + message.msg = replaceMentionedUsernamesWithFullNames(message.msg, message.mentions); + } + let title = UI_Use_Real_Name ? user.name : `@${ user.username }`; + if (room.t !== 'd' && room.name) { + title += ` @ #${ room.name }`; + } + RocketChat.Notifications.notifyUser(userId, 'notification', { + title, + text: message.msg, + duration, + payload: { + _id: message._id, + rid: message.rid, + sender: message.u, + type: room.t, + name: room.name + } + }); +} + +/** + * Checks if a message contains a user highlight + * + * @param {string} message + * @param {array|undefined} highlights + * + * @returns {boolean} + */ +function messageContainsHighlight(message, highlights) { + if (! highlights || highlights.length === 0) { return false; } + + let has = false; + highlights.some(function(highlight) { + const regexp = new RegExp(s.escapeRegExp(highlight), 'i'); + if (regexp.test(message.msg)) { + has = true; + return true; + } + }); + + return has; +} + +function getBadgeCount(userId) { + const subscriptions = RocketChat.models.Subscriptions.findUnreadByUserId(userId).fetch(); + + return subscriptions.reduce((unread, sub) => { + return sub.unread + unread; + }, 0); +} + RocketChat.callbacks.add('afterSaveMessage', function(message, room) { // skips this callback if the message was edited if (message.editedAt) { @@ -16,7 +99,13 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { /* Increment unread couter if direct messages */ - const settings = {}; + const settings = { + alwaysNotifyDesktopUsers: [], + dontNotifyDesktopUsers: [], + alwaysNotifyMobileUsers: [], + dontNotifyMobileUsers: [], + desktopNotificationDurations: {} + }; /** * Checks if a given user can be notified @@ -35,35 +124,6 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { return (settings[types[type][0]].indexOf(id) === -1 || settings[types[type][1]].indexOf(id) !== -1); } - /** - * Checks if a message contains a user highlight - * - * @param {string} message - * @param {array|undefined} highlights - * - * @returns {boolean} - */ - function messageContainsHighlight(message, highlights) { - if (! highlights || highlights.length === 0) { return false; } - - let has = false; - highlights.some(function(highlight) { - const regexp = new RegExp(s.escapeRegExp(highlight), 'i'); - if (regexp.test(message.msg)) { - has = true; - return true; - } - }); - - return has; - } - - settings.alwaysNotifyDesktopUsers = []; - settings.dontNotifyDesktopUsers = []; - settings.alwaysNotifyMobileUsers = []; - settings.dontNotifyMobileUsers = []; - settings.desktopNotificationDurations = {}; - const notificationPreferencesByRoom = RocketChat.models.Subscriptions.findNotificationPreferencesByRoom(room._id); notificationPreferencesByRoom.forEach(function(subscription) { if (subscription.disableNotifications) { @@ -132,18 +192,8 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { } if ((userOfMention != null) && canBeNotified(userOfMentionId, 'mobile')) { - RocketChat.Notifications.notifyUser(userOfMention._id, 'notification', { - title: RocketChat.settings.get('UI_Use_Real_Name') ? user.name : `@${ user.username }`, - text: message.msg, - duration: settings.desktopNotificationDurations[userOfMention._id], - payload: { - _id: message._id, - rid: message.rid, - sender: message.u, - type: room.t, - name: room.name - } - }); + const duration = settings.desktopNotificationDurations[userOfMention._id]; + notifyUser(userOfMention._id, user, message, room, duration); } if ((userOfMention != null) && canBeNotified(userOfMentionId, 'desktop')) { @@ -152,6 +202,7 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { roomId: message.rid, username: push_username, message: push_message, + badge: getBadgeCount(userOfMention._id), payload: { host: Meteor.absoluteUrl(), rid: message.rid, @@ -278,44 +329,32 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { if (userIdsToNotify.length > 0) { for (const usersOfMentionId of userIdsToNotify) { - let title = `@${ user.username }`; - if (room.name) { - title += ` @ #${ room.name }`; - } - RocketChat.Notifications.notifyUser(usersOfMentionId, 'notification', { - title, - text: message.msg, - duration: settings.desktopNotificationDurations[usersOfMentionId], - payload: { - _id: message._id, - rid: message.rid, - sender: message.u, - type: room.t, - name: room.name - } - }); + const duration = settings.desktopNotificationDurations[usersOfMentionId]; + notifyUser(usersOfMentionId, user, message, room, duration); } } if (userIdsToPushNotify.length > 0) { if (Push.enabled === true) { - RocketChat.PushNotification.send({ - roomId: message.rid, - roomName: push_room, - username: push_username, - message: push_message, - payload: { - host: Meteor.absoluteUrl(), - rid: message.rid, - sender: message.u, - type: room.t, - name: room.name - }, - usersTo: { - userId: { - $in: userIdsToPushNotify + // send a push notification for each user individually (to get his/her badge count) + userIdsToPushNotify.forEach((userIdToNotify) => { + RocketChat.PushNotification.send({ + roomId: message.rid, + roomName: push_room, + username: push_username, + message: push_message, + badge: getBadgeCount(userIdToNotify), + payload: { + host: Meteor.absoluteUrl(), + rid: message.rid, + sender: message.u, + type: room.t, + name: room.name + }, + usersTo: { + userId: userIdToNotify } - } + }); }); } } diff --git a/packages/rocketchat-lib/server/methods/deleteMessage.js b/packages/rocketchat-lib/server/methods/deleteMessage.js index 73dd607fafcfff64748b52ff567d55072a65402a..829c77e45eae0c78e22d8f1524520149465e993c 100644 --- a/packages/rocketchat-lib/server/methods/deleteMessage.js +++ b/packages/rocketchat-lib/server/methods/deleteMessage.js @@ -14,7 +14,8 @@ Meteor.methods({ fields: { u: 1, rid: 1, - file: 1 + file: 1, + ts: 1 } }); if (originalMessage == null) { @@ -23,17 +24,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/getChannelHistory.js b/packages/rocketchat-lib/server/methods/getChannelHistory.js index e2f40ebe748ae55ec1e7ab262cca7c92fd9688e6..0f3194991f569ebd89dcf29c7ccf76868469ecd7 100644 --- a/packages/rocketchat-lib/server/methods/getChannelHistory.js +++ b/packages/rocketchat-lib/server/methods/getChannelHistory.js @@ -57,6 +57,12 @@ Meteor.methods({ const user = RocketChat.models.Users.findOneById(message.u._id); message.u.name = user && user.name; } + if (message.mentions && message.mentions.length && UI_Use_Real_Name) { + message.mentions.forEach((mention) => { + const user = RocketChat.models.Users.findOneById(mention._id); + mention.name = user && user.name; + }); + } return message; }); diff --git a/packages/rocketchat-lib/server/methods/getSingleMessage.js b/packages/rocketchat-lib/server/methods/getSingleMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..72dc1a8516a3b057df3fcc917b33873d24756491 --- /dev/null +++ b/packages/rocketchat-lib/server/methods/getSingleMessage.js @@ -0,0 +1,19 @@ +Meteor.methods({ + getSingleMessage(msgId) { + check(msgId, String); + + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getSingleMessage' }); + } + + const msg = RocketChat.models.Messages.findOneById(msgId); + + if (!msg && !msg.rid) { + return undefined; + } + + Meteor.call('canAccessRoom', msg.rid, Meteor.userId()); + + return msg; + } +}); diff --git a/packages/rocketchat-lib/server/methods/saveSetting.js b/packages/rocketchat-lib/server/methods/saveSetting.js index 2c0402317bf7e87d971efc3b8ca54288c433ba07..963876864e3173cb90529e8d3d89c0c36b486dda 100644 --- a/packages/rocketchat-lib/server/methods/saveSetting.js +++ b/packages/rocketchat-lib/server/methods/saveSetting.js @@ -1,3 +1,5 @@ +/* eslint new-cap: 0 */ + Meteor.methods({ saveSetting(_id, value, editor) { if (Meteor.userId() === null) { @@ -20,7 +22,7 @@ Meteor.methods({ //Verify the value is what it should be switch (setting.type) { case 'roomPick': - check(value, [Object]); + check(value, Match.OneOf([Object], '')); break; case 'boolean': check(value, Boolean); diff --git a/packages/rocketchat-lib/server/methods/sendMessage.coffee b/packages/rocketchat-lib/server/methods/sendMessage.coffee deleted file mode 100644 index 95e8f71a66ab09d0ff5e427a4f07d33cf8ee3798..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/methods/sendMessage.coffee +++ /dev/null @@ -1,64 +0,0 @@ -import moment from 'moment' - -Meteor.methods - sendMessage: (message) -> - - check message, Object - - if not Meteor.userId() - throw new Meteor.Error('error-invalid-user', "Invalid user", { method: 'sendMessage' }) - - if message.ts - tsDiff = Math.abs(moment(message.ts).diff()) - if tsDiff > 60000 - throw new Meteor.Error('error-message-ts-out-of-sync', 'Message timestamp is out of sync', { method: 'sendMessage', message_ts: message.ts, server_ts: new Date().getTime() }) - else if tsDiff > 10000 - message.ts = new Date() - else - message.ts = new Date() - - if message.msg?.length > RocketChat.settings.get('Message_MaxAllowedSize') - throw new Meteor.Error('error-message-size-exceeded', 'Message size exceeds Message_MaxAllowedSize', { method: 'sendMessage' }) - - user = RocketChat.models.Users.findOneById Meteor.userId(), fields: username: 1, name: 1 - - room = Meteor.call 'canAccessRoom', message.rid, user._id - - if not room - return false - - subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(message.rid, Meteor.userId()); - if subscription and (subscription.blocked or subscription.blocker) - RocketChat.Notifications.notifyUser Meteor.userId(), 'message', { - _id: Random.id() - rid: room._id - ts: new Date - msg: TAPi18n.__('room_is_blocked', {}, user.language) - } - return false - - if user.username in (room.muted or []) - RocketChat.Notifications.notifyUser Meteor.userId(), 'message', { - _id: Random.id() - rid: room._id - ts: new Date - msg: TAPi18n.__('You_have_been_muted', {}, user.language) - } - return false - - message.alias = user.name if not message.alias? and RocketChat.settings.get 'Message_SetNameToAliasEnabled' - if Meteor.settings.public.sandstorm - message.sandstormSessionId = this.connection.sandstormSessionId() - - RocketChat.metrics.messagesSent.inc() # This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 - RocketChat.sendMessage user, message, room - -# Limit a user, who does not have the "bot" role, to sending 5 msgs/second -DDPRateLimiter.addRule - type: 'method' - name: 'sendMessage' - userId: (userId) -> - user = RocketChat.models.Users.findOneById(userId) - return true if not user?.roles - return 'bot' not in user.roles -, 5, 1000 diff --git a/packages/rocketchat-lib/server/methods/sendMessage.js b/packages/rocketchat-lib/server/methods/sendMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..2f6e41502fc59988185f727a94da2a6e074fdee4 --- /dev/null +++ b/packages/rocketchat-lib/server/methods/sendMessage.js @@ -0,0 +1,81 @@ +import moment from 'moment'; + +Meteor.methods({ + sendMessage(message) { + check(message, Object); + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'sendMessage' + }); + } + if (message.ts) { + const tsDiff = Math.abs(moment(message.ts).diff()); + if (tsDiff > 60000) { + throw new Meteor.Error('error-message-ts-out-of-sync', 'Message timestamp is out of sync', { + method: 'sendMessage', + message_ts: message.ts, + server_ts: new Date().getTime() + }); + } else if (tsDiff > 10000) { + message.ts = new Date(); + } + } else { + message.ts = new Date(); + } + if (message.msg && message.msg.length > RocketChat.settings.get('Message_MaxAllowedSize')) { + throw new Meteor.Error('error-message-size-exceeded', 'Message size exceeds Message_MaxAllowedSize', { + method: 'sendMessage' + }); + } + const user = RocketChat.models.Users.findOneById(Meteor.userId(), { + fields: { + username: 1, + name: 1 + } + }); + const room = Meteor.call('canAccessRoom', message.rid, user._id); + if (!room) { + return false; + } + const subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(message.rid, Meteor.userId()); + if (subscription && subscription.blocked || subscription.blocker) { + RocketChat.Notifications.notifyUser(Meteor.userId(), 'message', { + _id: Random.id(), + rid: room._id, + ts: new Date, + msg: TAPi18n.__('room_is_blocked', {}, user.language) + }); + return false; + } + + if ((room.muted||[]).includes(user.username)) { + RocketChat.Notifications.notifyUser(Meteor.userId(), 'message', { + _id: Random.id(), + rid: room._id, + ts: new Date, + msg: TAPi18n.__('You_have_been_muted', {}, user.language) + }); + return false; + } + if (message.alias == null && RocketChat.settings.get('Message_SetNameToAliasEnabled')) { + message.alias = user.name; + } + if (Meteor.settings['public'].sandstorm) { + message.sandstormSessionId = this.connection.sandstormSessionId(); + } + RocketChat.metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 + return RocketChat.sendMessage(user, message, room); + } +}); +// Limit a user, who does not have the "bot" role, to sending 5 msgs/second +DDPRateLimiter.addRule({ + type: 'method', + name: 'sendMessage', + userId(userId) { + const user = RocketChat.models.Users.findOneById(userId); + if (user == null || !user.roles) { + return true; + } + return user.roles.includes('bot'); + } +}, 5, 1000); diff --git a/packages/rocketchat-lib/server/models/Avatars.js b/packages/rocketchat-lib/server/models/Avatars.js new file mode 100644 index 0000000000000000000000000000000000000000..76d88df19f1e26c68477989efd0855b8a4c058ee --- /dev/null +++ b/packages/rocketchat-lib/server/models/Avatars.js @@ -0,0 +1,111 @@ +/* globals InstanceStatus */ + +RocketChat.models.Avatars = new class extends RocketChat.models._Base { + constructor() { + super('avatars'); + + this.model.before.insert((userId, doc) => { + doc.instanceId = InstanceStatus.id(); + }); + + this.tryEnsureIndex({ name: 1 }); + } + + insertAvatarFileInit(name, userId, store, file, extra) { + const fileData = { + _id: name, + name, + userId, + store, + complete: false, + uploading: true, + progress: 0, + extension: s.strRightBack(file.name, '.'), + uploadedAt: new Date() + }; + + _.extend(fileData, file, extra); + + return this.insertOrUpsert(fileData); + } + + updateFileComplete(fileId, userId, file) { + if (!fileId) { + return; + } + + const filter = { + _id: fileId, + userId + }; + + const update = { + $set: { + complete: true, + uploading: false, + progress: 1 + } + }; + + update.$set = _.extend(file, update.$set); + + if (this.model.direct && this.model.direct.update) { + return this.model.direct.update(filter, update); + } else { + return this.update(filter, update); + } + } + + findOneByName(name) { + return this.findOne({ name }); + } + + updateFileNameById(fileId, name) { + const filter = { _id: fileId }; + const update = { + $set: { + name + } + }; + if (this.model.direct && this.model.direct.update) { + return this.model.direct.update(filter, update); + } else { + return this.update(filter, update); + } + } + + // @TODO deprecated + updateFileCompleteByNameAndUserId(name, userId, url) { + if (!name) { + return; + } + + const filter = { + name, + userId + }; + + const update = { + $set: { + complete: true, + uploading: false, + progress: 1, + url + } + }; + + if (this.model.direct && this.model.direct.update) { + return this.model.direct.update(filter, update); + } else { + return this.update(filter, update); + } + } + + deleteFile(fileId) { + if (this.model.direct && this.model.direct.remove) { + return this.model.direct.remove({ _id: fileId }); + } else { + return this.remove({ _id: fileId }); + } + } +}; diff --git a/packages/rocketchat-lib/server/models/Messages.coffee b/packages/rocketchat-lib/server/models/Messages.coffee deleted file mode 100644 index 8ac7b4efcd7bb954d68857c58fce794e8f7ba67d..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/models/Messages.coffee +++ /dev/null @@ -1,462 +0,0 @@ -RocketChat.models.Messages = new class extends RocketChat.models._Base - constructor: -> - super('message') - - @tryEnsureIndex { 'rid': 1, 'ts': 1 } - @tryEnsureIndex { 'ts': 1 } - @tryEnsureIndex { 'u._id': 1 } - @tryEnsureIndex { 'editedAt': 1 }, { sparse: 1 } - @tryEnsureIndex { 'editedBy._id': 1 }, { sparse: 1 } - @tryEnsureIndex { 'rid': 1, 't': 1, 'u._id': 1 } - @tryEnsureIndex { 'expireAt': 1 }, { expireAfterSeconds: 0 } - @tryEnsureIndex { 'msg': 'text' } - @tryEnsureIndex { 'file._id': 1 }, { sparse: 1 } - @tryEnsureIndex { 'mentions.username': 1 }, { sparse: 1 } - @tryEnsureIndex { 'pinned': 1 }, { sparse: 1 } - @tryEnsureIndex { 'snippeted': 1 }, { sparse: 1 } - @tryEnsureIndex { 'location': '2dsphere' } - @tryEnsureIndex { 'slackBotId': 1, 'slackTs': 1 }, { sparse: 1 } - - # FIND - findByMention: (username, options) -> - query = - "mentions.username": username - - return @find query, options - - findVisibleByMentionAndRoomId: (username, rid, options) -> - query = - _hidden: { $ne: true } - "mentions.username": username - "rid": rid - - return @find query, options - - findVisibleByRoomId: (roomId, options) -> - query = - _hidden: - $ne: true - - rid: roomId - - return @find query, options - - findVisibleByRoomIdNotContainingTypes: (roomId, types, options) -> - query = - _hidden: - $ne: true - - rid: roomId - - if Match.test(types, [String]) and types.length > 0 - query.t = - $nin: types - - return @find query, options - - findInvisibleByRoomId: (roomId, options) -> - query = - _hidden: true - rid: roomId - - return @find query, options - - findVisibleByRoomIdAfterTimestamp: (roomId, timestamp, options) -> - query = - _hidden: - $ne: true - rid: roomId - ts: - $gt: timestamp - - return @find query, options - - findVisibleByRoomIdBeforeTimestamp: (roomId, timestamp, options) -> - query = - _hidden: - $ne: true - rid: roomId - ts: - $lt: timestamp - - return @find query, options - - findVisibleByRoomIdBeforeTimestampInclusive: (roomId, timestamp, options) -> - query = - _hidden: - $ne: true - rid: roomId - ts: - $lte: timestamp - - return @find query, options - - findVisibleByRoomIdBetweenTimestamps: (roomId, afterTimestamp, beforeTimestamp, options) -> - query = - _hidden: - $ne: true - rid: roomId - ts: - $gt: afterTimestamp - $lt: beforeTimestamp - - return @find query, options - - findVisibleByRoomIdBetweenTimestampsInclusive: (roomId, afterTimestamp, beforeTimestamp, options) -> - query = - _hidden: - $ne: true - rid: roomId - ts: - $gte: afterTimestamp - $lte: beforeTimestamp - - return @find query, options - - findVisibleByRoomIdBeforeTimestampNotContainingTypes: (roomId, timestamp, types, options) -> - query = - _hidden: - $ne: true - rid: roomId - ts: - $lt: timestamp - - if Match.test(types, [String]) and types.length > 0 - query.t = - $nin: types - - return @find query, options - - findVisibleByRoomIdBetweenTimestampsNotContainingTypes: (roomId, afterTimestamp, beforeTimestamp, types, options) -> - query = - _hidden: - $ne: true - rid: roomId - ts: - $gt: afterTimestamp - $lt: beforeTimestamp - - if Match.test(types, [String]) and types.length > 0 - query.t = - $nin: types - - return @find query, options - - findVisibleCreatedOrEditedAfterTimestamp: (timestamp, options) -> - query = - _hidden: { $ne: true } - $or: [ - ts: - $gt: timestamp - , - 'editedAt': - $gt: timestamp - ] - - return @find query, options - - findStarredByUserAtRoom: (userId, roomId, options) -> - query = - _hidden: { $ne: true } - 'starred._id': userId - rid: roomId - - return @find query, options - - findPinnedByRoom: (roomId, options) -> - query = - t: { $ne: 'rm' } - _hidden: { $ne: true } - pinned: true - rid: roomId - - return @find query, options - - findSnippetedByRoom: (roomId, options) -> - query = - _hidden: { $ne: true } - snippeted: true - rid: roomId - - return @find query, options - - getLastTimestamp: (options = {}) -> - query = { ts: { $exists: 1 } } - options.sort = { ts: -1 } - options.limit = 1 - - return @find(query, options)?.fetch?()?[0]?.ts - - findByRoomIdAndMessageIds: (rid, messageIds, options) -> - query = - rid: rid - _id: - $in: messageIds - - return @find query, options - - findOneBySlackBotIdAndSlackTs: (slackBotId, slackTs) -> - query = - slackBotId: slackBotId - slackTs: slackTs - - return @findOne query - - findOneBySlackTs: (slackTs) -> - query = - slackTs: slackTs - - return @findOne query - - cloneAndSaveAsHistoryById: (_id) -> - me = RocketChat.models.Users.findOneById Meteor.userId() - record = @findOneById _id - record._hidden = true - record.parent = record._id - record.editedAt = new Date - record.editedBy = - _id: Meteor.userId() - username: me.username - delete record._id - return @insert record - - # UPDATE - setHiddenById: (_id, hidden=true) -> - query = - _id: _id - - update = - $set: - _hidden: hidden - - return @update query, update - - setAsDeletedByIdAndUser: (_id, user) -> - query = - _id: _id - - update = - $set: - msg: '' - t: 'rm' - urls: [] - mentions: [] - attachments: [] - reactions: [] - editedAt: new Date() - editedBy: - _id: user._id - username: user.username - - return @update query, update - - setPinnedByIdAndUserId: (_id, pinnedBy, pinned=true, pinnedAt=0) -> - query = - _id: _id - - update = - $set: - pinned: pinned - pinnedAt: pinnedAt || new Date - pinnedBy: pinnedBy - - return @update query, update - - setSnippetedByIdAndUserId: (message, snippetName, snippetedBy, snippeted=true, snippetedAt=0) -> - query = - _id: message._id - - msg = "```" + message.msg + "```" - - update = - $set: - msg: msg - snippeted: snippeted - snippetedAt: snippetedAt || new Date - snippetedBy: snippetedBy - snippetName: snippetName - - return @update query, update - - setUrlsById: (_id, urls) -> - query = - _id: _id - - update = - $set: - urls: urls - - return @update query, update - - updateAllUsernamesByUserId: (userId, username) -> - query = - 'u._id': userId - - update = - $set: - "u.username": username - - return @update query, update, { multi: true } - - updateUsernameOfEditByUserId: (userId, username) -> - query = - 'editedBy._id': userId - - update = - $set: - "editedBy.username": username - - return @update query, update, { multi: true } - - updateUsernameAndMessageOfMentionByIdAndOldUsername: (_id, oldUsername, newUsername, newMessage) -> - query = - _id: _id - "mentions.username": oldUsername - - update = - $set: - "mentions.$.username": newUsername - "msg": newMessage - - return @update query, update - - updateUserStarById: (_id, userId, starred) -> - query = - _id: _id - - if starred - update = - $addToSet: - starred: { _id: userId } - else - update = - $pull: - starred: { _id: Meteor.userId() } - - return @update query, update - - upgradeEtsToEditAt: -> - query = - ets: { $exists: 1 } - - update = - $rename: - "ets": "editedAt" - - return @update query, update, { multi: true } - - setMessageAttachments: (_id, attachments) -> - query = - _id: _id - - update = - $set: - attachments: attachments - - return @update query, update - - setSlackBotIdAndSlackTs: (_id, slackBotId, slackTs) -> - query = - _id: _id - - update = - $set: - slackBotId: slackBotId - slackTs: slackTs - - return @update query, update - - - # INSERT - createWithTypeRoomIdMessageAndUser: (type, roomId, message, user, extraData) -> - room = RocketChat.models.Rooms.findOneById roomId, { fields: { sysMes: 1 }} - if room?.sysMes is false - return - record = - t: type - rid: roomId - ts: new Date - msg: message - u: - _id: user._id - username: user.username - groupable: false - - _.extend record, extraData - - record._id = @insertOrUpsert record - RocketChat.models.Rooms.incMsgCountById(room._id, 1) - return record - - createUserJoinWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'uj', roomId, message, user, extraData - - createUserLeaveWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'ul', roomId, message, user, extraData - - createUserRemovedWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'ru', roomId, message, user, extraData - - createUserAddedWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'au', roomId, message, user, extraData - - createCommandWithRoomIdAndUser: (command, roomId, user, extraData) -> - return @createWithTypeRoomIdMessageAndUser 'command', roomId, command, user, extraData - - createUserMutedWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'user-muted', roomId, message, user, extraData - - createUserUnmutedWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'user-unmuted', roomId, message, user, extraData - - createNewModeratorWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'new-moderator', roomId, message, user, extraData - - createModeratorRemovedWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'moderator-removed', roomId, message, user, extraData - - createNewOwnerWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'new-owner', roomId, message, user, extraData - - createOwnerRemovedWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'owner-removed', roomId, message, user, extraData - - createSubscriptionRoleAddedWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'subscription-role-added', roomId, message, user, extraData - - createSubscriptionRoleRemovedWithRoomIdAndUser: (roomId, user, extraData) -> - message = user.username - return @createWithTypeRoomIdMessageAndUser 'subscription-role-removed', roomId, message, user, extraData - - # REMOVE - removeById: (_id) -> - query = - _id: _id - - return @remove query - - removeByRoomId: (roomId) -> - query = - rid: roomId - - return @remove query - - removeByUserId: (userId) -> - query = - "u._id": userId - - return @remove query - - getMessageByFileId: (fileID) -> - return @findOne { 'file._id': fileID } diff --git a/packages/rocketchat-lib/server/models/Messages.js b/packages/rocketchat-lib/server/models/Messages.js new file mode 100644 index 0000000000000000000000000000000000000000..dabf189366333778851460137a3699200b24ef18 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Messages.js @@ -0,0 +1,580 @@ +RocketChat.models.Messages = new class extends RocketChat.models._Base { + constructor() { + super('message'); + + this.tryEnsureIndex({ 'rid': 1, 'ts': 1 }); + this.tryEnsureIndex({ 'ts': 1 }); + this.tryEnsureIndex({ 'u._id': 1 }); + this.tryEnsureIndex({ 'editedAt': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'editedBy._id': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'rid': 1, 't': 1, 'u._id': 1 }); + this.tryEnsureIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 }); + this.tryEnsureIndex({ 'msg': 'text' }); + this.tryEnsureIndex({ 'file._id': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'mentions.username': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'pinned': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'snippeted': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'location': '2dsphere' }); + this.tryEnsureIndex({ 'slackBotId': 1, 'slackTs': 1 }, { sparse: 1 }); + } + + // FIND + findByMention(username, options) { + const query = {'mentions.username': username}; + + return this.find(query, options); + } + + findVisibleByMentionAndRoomId(username, rid, options) { + const query = { + _hidden: { $ne: true }, + 'mentions.username': username, + rid + }; + + return this.find(query, options); + } + + findVisibleByRoomId(roomId, options) { + const query = { + _hidden: { + $ne: true + }, + + rid: roomId + }; + + return this.find(query, options); + } + + findVisibleByRoomIdNotContainingTypes(roomId, types, options) { + const query = { + _hidden: { + $ne: true + }, + + rid: roomId + }; + + if (Match.test(types, [String]) && (types.length > 0)) { + query.t = + {$nin: types}; + } + + return this.find(query, options); + } + + findInvisibleByRoomId(roomId, options) { + const query = { + _hidden: true, + rid: roomId + }; + + return this.find(query, options); + } + + findVisibleByRoomIdAfterTimestamp(roomId, timestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $gt: timestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBeforeTimestamp(roomId, timestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $lt: timestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBeforeTimestampInclusive(roomId, timestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $lte: timestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBetweenTimestamps(roomId, afterTimestamp, beforeTimestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $gt: afterTimestamp, + $lt: beforeTimestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBetweenTimestampsInclusive(roomId, afterTimestamp, beforeTimestamp, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $gte: afterTimestamp, + $lte: beforeTimestamp + } + }; + + return this.find(query, options); + } + + findVisibleByRoomIdBeforeTimestampNotContainingTypes(roomId, timestamp, types, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $lt: timestamp + } + }; + + if (Match.test(types, [String]) && (types.length > 0)) { + query.t = + {$nin: types}; + } + + return this.find(query, options); + } + + findVisibleByRoomIdBetweenTimestampsNotContainingTypes(roomId, afterTimestamp, beforeTimestamp, types, options) { + const query = { + _hidden: { + $ne: true + }, + rid: roomId, + ts: { + $gt: afterTimestamp, + $lt: beforeTimestamp + } + }; + + if (Match.test(types, [String]) && (types.length > 0)) { + query.t = + {$nin: types}; + } + + return this.find(query, options); + } + + findVisibleCreatedOrEditedAfterTimestamp(timestamp, options) { + const query = { + _hidden: { $ne: true }, + $or: [{ + ts: { + $gt: timestamp + } + }, + { + 'editedAt': { + $gt: timestamp + } + } + ] + }; + + return this.find(query, options); + } + + findStarredByUserAtRoom(userId, roomId, options) { + const query = { + _hidden: { $ne: true }, + 'starred._id': userId, + rid: roomId + }; + + return this.find(query, options); + } + + findPinnedByRoom(roomId, options) { + const query = { + t: { $ne: 'rm' }, + _hidden: { $ne: true }, + pinned: true, + rid: roomId + }; + + return this.find(query, options); + } + + findSnippetedByRoom(roomId, options) { + const query = { + _hidden: { $ne: true }, + snippeted: true, + rid: roomId + }; + + return this.find(query, options); + } + + getLastTimestamp(options) { + if (options == null) { options = {}; } + const query = { ts: { $exists: 1 } }; + options.sort = { ts: -1 }; + options.limit = 1; + const [message] = this.find(query, options).fetch(); + return message && message.ts; + } + + findByRoomIdAndMessageIds(rid, messageIds, options) { + const query = { + rid, + _id: { + $in: messageIds + } + }; + + return this.find(query, options); + } + + findOneBySlackBotIdAndSlackTs(slackBotId, slackTs) { + const query = { + slackBotId, + slackTs + }; + + return this.findOne(query); + } + + findOneBySlackTs(slackTs) { + const query = {slackTs}; + + return this.findOne(query); + } + + cloneAndSaveAsHistoryById(_id) { + const me = RocketChat.models.Users.findOneById(Meteor.userId()); + const record = this.findOneById(_id); + record._hidden = true; + record.parent = record._id; + record.editedAt = new Date; + record.editedBy = { + _id: Meteor.userId(), + username: me.username + }; + delete record._id; + return this.insert(record); + } + + // UPDATE + setHiddenById(_id, hidden) { + if (hidden == null) { hidden = true; } + const query = {_id}; + + const update = { + $set: { + _hidden: hidden + } + }; + + return this.update(query, update); + } + + setAsDeletedByIdAndUser(_id, user) { + const query = {_id}; + + const update = { + $set: { + msg: '', + t: 'rm', + urls: [], + mentions: [], + attachments: [], + reactions: [], + editedAt: new Date(), + editedBy: { + _id: user._id, + username: user.username + } + } + }; + + return this.update(query, update); + } + + setPinnedByIdAndUserId(_id, pinnedBy, pinned, pinnedAt) { + if (pinned == null) { pinned = true; } + if (pinnedAt == null) { pinnedAt = 0; } + const query = {_id}; + + const update = { + $set: { + pinned, + pinnedAt: pinnedAt || new Date, + pinnedBy + } + }; + + return this.update(query, update); + } + + setSnippetedByIdAndUserId(message, snippetName, snippetedBy, snippeted, snippetedAt) { + if (snippeted == null) { snippeted = true; } + if (snippetedAt == null) { snippetedAt = 0; } + const query = {_id: message._id}; + + const msg = `\`\`\`${ message.msg }\`\`\``; + + const update = { + $set: { + msg, + snippeted, + snippetedAt: snippetedAt || new Date, + snippetedBy, + snippetName + } + }; + + return this.update(query, update); + } + + setUrlsById(_id, urls) { + const query = {_id}; + + const update = { + $set: { + urls + } + }; + + return this.update(query, update); + } + + updateAllUsernamesByUserId(userId, username) { + const query = {'u._id': userId}; + + const update = { + $set: { + 'u.username': username + } + }; + + return this.update(query, update, { multi: true }); + } + + updateUsernameOfEditByUserId(userId, username) { + const query = {'editedBy._id': userId}; + + const update = { + $set: { + 'editedBy.username': username + } + }; + + return this.update(query, update, { multi: true }); + } + + updateUsernameAndMessageOfMentionByIdAndOldUsername(_id, oldUsername, newUsername, newMessage) { + const query = { + _id, + 'mentions.username': oldUsername + }; + + const update = { + $set: { + 'mentions.$.username': newUsername, + 'msg': newMessage + } + }; + + return this.update(query, update); + } + + updateUserStarById(_id, userId, starred) { + let update; + const query = {_id}; + + if (starred) { + update = { + $addToSet: { + starred: { _id: userId } + } + }; + } else { + update = { + $pull: { + starred: { _id: Meteor.userId() } + } + }; + } + + return this.update(query, update); + } + + upgradeEtsToEditAt() { + const query = {ets: { $exists: 1 }}; + + const update = { + $rename: { + 'ets': 'editedAt' + } + }; + + return this.update(query, update, { multi: true }); + } + + setMessageAttachments(_id, attachments) { + const query = {_id}; + + const update = { + $set: { + attachments + } + }; + + return this.update(query, update); + } + + setSlackBotIdAndSlackTs(_id, slackBotId, slackTs) { + const query = {_id}; + + const update = { + $set: { + slackBotId, + slackTs + } + }; + + return this.update(query, update); + } + + + // INSERT + createWithTypeRoomIdMessageAndUser(type, roomId, message, user, extraData) { + const room = RocketChat.models.Rooms.findOneById(roomId, { fields: { sysMes: 1 }}); + if ((room != null ? room.sysMes : undefined) === false) { + return; + } + const record = { + t: type, + rid: roomId, + ts: new Date, + msg: message, + u: { + _id: user._id, + username: user.username + }, + groupable: false + }; + + _.extend(record, extraData); + + record._id = this.insertOrUpsert(record); + RocketChat.models.Rooms.incMsgCountById(room._id, 1); + return record; + } + + createUserJoinWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('uj', roomId, message, user, extraData); + } + + createUserLeaveWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('ul', roomId, message, user, extraData); + } + + createUserRemovedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('ru', roomId, message, user, extraData); + } + + createUserAddedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('au', roomId, message, user, extraData); + } + + createCommandWithRoomIdAndUser(command, roomId, user, extraData) { + return this.createWithTypeRoomIdMessageAndUser('command', roomId, command, user, extraData); + } + + createUserMutedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('user-muted', roomId, message, user, extraData); + } + + createUserUnmutedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('user-unmuted', roomId, message, user, extraData); + } + + createNewModeratorWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('new-moderator', roomId, message, user, extraData); + } + + createModeratorRemovedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('moderator-removed', roomId, message, user, extraData); + } + + createNewOwnerWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('new-owner', roomId, message, user, extraData); + } + + createOwnerRemovedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('owner-removed', roomId, message, user, extraData); + } + + createSubscriptionRoleAddedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('subscription-role-added', roomId, message, user, extraData); + } + + createSubscriptionRoleRemovedWithRoomIdAndUser(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('subscription-role-removed', roomId, message, user, extraData); + } + + // REMOVE + removeById(_id) { + const query = {_id}; + + return this.remove(query); + } + + removeByRoomId(roomId) { + const query = {rid: roomId}; + + return this.remove(query); + } + + removeByUserId(userId) { + const query = {'u._id': userId}; + + return this.remove(query); + } + + getMessageByFileId(fileID) { + return this.findOne({ 'file._id': fileID }); + } +}; diff --git a/packages/rocketchat-lib/server/models/Rooms.coffee b/packages/rocketchat-lib/server/models/Rooms.coffee deleted file mode 100644 index 1d58e578e117942e42cdc12ab374527076574ddd..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/models/Rooms.coffee +++ /dev/null @@ -1,633 +0,0 @@ -class ModelRooms extends RocketChat.models._Base - constructor: -> - super(arguments...) - - @tryEnsureIndex { 'name': 1 }, { unique: 1, sparse: 1 } - @tryEnsureIndex { 'default': 1 } - @tryEnsureIndex { 'usernames': 1 } - @tryEnsureIndex { 't': 1 } - @tryEnsureIndex { 'u._id': 1 } - - this.cache.ignoreUpdatedFields.push('msgs', 'lm') - this.cache.ensureIndex(['t', 'name'], 'unique') - this.cache.options = {fields: {usernames: 0}} - - findOneByIdOrName: (_idOrName, options) -> - query = { - $or: [{ - _id: _idOrName - }, { - name: _idOrName - }] - } - - return this.findOne(query, options) - - findOneByImportId: (_id, options) -> - query = - importIds: _id - - return @findOne query, options - - findOneByName: (name, options) -> - query = - name: name - - return @findOne query, options - - findOneByNameAndType: (name, type, options) -> - query = - name: name - t: type - - return @findOne query, options - - findOneByIdContainingUsername: (_id, username, options) -> - query = - _id: _id - usernames: username - - return @findOne query, options - - findOneByNameAndTypeNotContainingUsername: (name, type, username, options) -> - query = - name: name - t: type - usernames: - $ne: username - - return @findOne query, options - - - # FIND - - findById: (roomId, options) -> - return @find { _id: roomId }, options - - findByIds: (roomIds, options) -> - return @find { _id: $in: [].concat roomIds }, options - - findByType: (type, options) -> - query = - t: type - - return @find query, options - - findByTypes: (types, options) -> - query = - t: - $in: types - - return @find query, options - - findByUserId: (userId, options) -> - query = - "u._id": userId - - return @find query, options - - findBySubscriptionUserId: (userId, options) -> - if this.useCache - data = RocketChat.models.Subscriptions.findByUserId(userId).fetch() - data = data.map (item) -> - if item._room - return item._room - console.log('Empty Room for Subscription', item); - return {} - return this.arrayToCursor this.processQueryOptionsOnResult(data, options) - - data = RocketChat.models.Subscriptions.findByUserId(userId, {fields: {rid: 1}}).fetch() - data = data.map (item) -> item.rid - - query = - _id: - $in: data - - this.find query, options - - findBySubscriptionUserIdUpdatedAfter: (userId, _updatedAt, options) -> - if this.useCache - data = RocketChat.models.Subscriptions.findByUserId(userId).fetch() - data = data.map (item) -> - if item._room - return item._room - console.log('Empty Room for Subscription', item); - return {} - data = data.filter (item) -> item._updatedAt > _updatedAt - return this.arrayToCursor this.processQueryOptionsOnResult(data, options) - - ids = RocketChat.models.Subscriptions.findByUserId(userId, {fields: {rid: 1}}).fetch() - ids = ids.map (item) -> item.rid - - query = - _id: - $in: ids - _updatedAt: - $gt: _updatedAt - - this.find query, options - - findByNameContaining: (name, options) -> - nameRegex = new RegExp s.trim(s.escapeRegExp(name)), "i" - - query = - $or: [ - name: nameRegex - , - t: 'd' - usernames: nameRegex - ] - - return @find query, options - - findByNameContainingTypesWithUsername: (name, types, options) -> - nameRegex = new RegExp s.trim(s.escapeRegExp(name)), "i" - - $or = [] - for type in types - obj = {name: nameRegex, t: type.type} - if type.username? - obj.usernames = type.username - if type.ids? - obj._id = $in: type.ids - $or.push obj - - query = - $or: $or - - return @find query, options - - findContainingTypesWithUsername: (types, options) -> - - $or = [] - for type in types - obj = {t: type.type} - if type.username? - obj.usernames = type.username - if type.ids? - obj._id = $in: type.ids - $or.push obj - - query = - $or: $or - - return @find query, options - - findByNameContainingAndTypes: (name, types, options) -> - nameRegex = new RegExp s.trim(s.escapeRegExp(name)), "i" - - query = - t: - $in: types - $or: [ - name: nameRegex - , - t: 'd' - usernames: nameRegex - ] - - return @find query, options - - findByNameAndType: (name, type, options) -> - query = - t: type - name: name - - return @find query, options - - findByNameAndTypeNotDefault: (name, type, options) -> - query = - t: type - name: name - default: - $ne: true - - return @find query, options - - findByNameAndTypeNotContainingUsername: (name, type, username, options) -> - query = - t: type - name: name - usernames: - $ne: username - - return @find query, options - - findByNameStartingAndTypes: (name, types, options) -> - nameRegex = new RegExp "^" + s.trim(s.escapeRegExp(name)), "i" - - query = - t: - $in: types - $or: [ - name: nameRegex - , - t: 'd' - usernames: nameRegex - ] - - return @find query, options - - findByDefaultAndTypes: (defaultValue, types, options) -> - query = - default: defaultValue - t: - $in: types - - return @find query, options - - findByTypeContainingUsername: (type, username, options) -> - query = - t: type - usernames: username - - return @find query, options - - findByTypeContainingUsernames: (type, username, options) -> - query = - t: type - usernames: { $all: [].concat(username) } - - return @find query, options - - findByTypesAndNotUserIdContainingUsername: (types, userId, username, options) -> - query = - t: - $in: types - uid: - $ne: userId - usernames: username - - return @find query, options - - findByContainingUsername: (username, options) -> - query = - usernames: username - - return @find query, options - - findByTypeAndName: (type, name, options) -> - if this.useCache - return this.cache.findByIndex('t,name', [type, name], options) - - query = - name: name - t: type - - return @find query, options - - findByTypeAndNameContainingUsername: (type, name, username, options) -> - query = - name: name - t: type - usernames: username - - return @find query, options - - findByTypeAndArchivationState: (type, archivationstate, options) -> - query = - t: type - - if archivationstate - query.archived = true - else - query.archived = { $ne: true } - - return @find query, options - - # UPDATE - addImportIds: (_id, importIds) -> - importIds = [].concat(importIds); - query = - _id: _id - - update = - $addToSet: - importIds: - $each: importIds - - return @update query, update - - archiveById: (_id) -> - query = - _id: _id - - update = - $set: - archived: true - - return @update query, update - - unarchiveById: (_id) -> - query = - _id: _id - - update = - $set: - archived: false - - return @update query, update - - addUsernameById: (_id, username, muted) -> - query = - _id: _id - - update = - $addToSet: - usernames: username - - if muted - update.$addToSet.muted = username - - return @update query, update - - addUsernamesById: (_id, usernames) -> - query = - _id: _id - - update = - $addToSet: - usernames: - $each: usernames - - return @update query, update - - addUsernameByName: (name, username) -> - query = - name: name - - update = - $addToSet: - usernames: username - - return @update query, update - - removeUsernameById: (_id, username) -> - query = - _id: _id - - update = - $pull: - usernames: username - - return @update query, update - - removeUsernamesById: (_id, usernames) -> - query = - _id: _id - - update = - $pull: - usernames: - $in: usernames - - return @update query, update - - removeUsernameFromAll: (username) -> - query = - usernames: username - - update = - $pull: - usernames: username - - return @update query, update, { multi: true } - - removeUsernameByName: (name, username) -> - query = - name: name - - update = - $pull: - usernames: username - - return @update query, update - - setNameById: (_id, name) -> - query = - _id: _id - - update = - $set: - name: name - - return @update query, update - - incMsgCountById: (_id, inc=1) -> - query = - _id: _id - - update = - $inc: - msgs: inc - - return @update query, update - - incMsgCountAndSetLastMessageTimestampById: (_id, inc=1, lastMessageTimestamp) -> - query = - _id: _id - - update = - $set: - lm: lastMessageTimestamp - $inc: - msgs: inc - - return @update query, update - - replaceUsername: (previousUsername, username) -> - query = - usernames: previousUsername - - update = - $set: - "usernames.$": username - - return @update query, update, { multi: true } - - replaceMutedUsername: (previousUsername, username) -> - query = - muted: previousUsername - - update = - $set: - "muted.$": username - - return @update query, update, { multi: true } - - replaceUsernameOfUserByUserId: (userId, username) -> - query = - "u._id": userId - - update = - $set: - "u.username": username - - return @update query, update, { multi: true } - - setJoinCodeById: (_id, joinCode) -> - query = - _id: _id - - if joinCode?.trim() isnt '' - update = - $set: - joinCodeRequired: true - joinCode: joinCode - else - update = - $set: - joinCodeRequired: false - $unset: - joinCode: 1 - - return @update query, update - - setUserById: (_id, user) -> - query = - _id: _id - - update = - $set: - u: - _id: user._id - username: user.username - - return @update query, update - - setTypeById: (_id, type) -> - query = - _id: _id - update = - $set: - t: type - if type == 'p' - update.$unset = {default: ''} - - return @update query, update - - setTopicById: (_id, topic) -> - query = - _id: _id - - update = - $set: - topic: topic - - return @update query, update - - setAnnouncementById: (_id, announcement) -> - query = - _id: _id - - update = - $set: - announcement: announcement - - return @update query, update - - muteUsernameByRoomId: (_id, username) -> - query = - _id: _id - - update = - $addToSet: - muted: username - - return @update query, update - - unmuteUsernameByRoomId: (_id, username) -> - query = - _id: _id - - update = - $pull: - muted: username - - return @update query, update - - saveDefaultById: (_id, defaultValue) -> - query = - _id: _id - - update = - $set: - default: defaultValue is 'true' - - return @update query, update - - setTopicAndTagsById: (_id, topic, tags) -> - setData = {} - unsetData = {} - - if topic? - if not _.isEmpty(s.trim(topic)) - setData.topic = s.trim(topic) - else - unsetData.topic = 1 - - if tags? - if not _.isEmpty(s.trim(tags)) - setData.tags = s.trim(tags).split(',').map((tag) => return s.trim(tag)) - else - unsetData.tags = 1 - - update = {} - - if not _.isEmpty setData - update.$set = setData - - if not _.isEmpty unsetData - update.$unset = unsetData - - if _.isEmpty update - return - - return @update { _id: _id }, update - - # INSERT - createWithTypeNameUserAndUsernames: (type, name, user, usernames, extraData) -> - room = - name: name - t: type - usernames: usernames - msgs: 0 - u: - _id: user._id - username: user.username - - _.extend room, extraData - - room._id = @insert room - return room - - createWithIdTypeAndName: (_id, type, name, extraData) -> - room = - _id: _id - ts: new Date() - t: type - name: name - usernames: [] - msgs: 0 - - _.extend room, extraData - - @insert room - return room - - - # REMOVE - removeById: (_id) -> - query = - _id: _id - - return @remove query - - removeByTypeContainingUsername: (type, username) -> - query = - t: type - usernames: username - - return @remove query - -RocketChat.models.Rooms = new ModelRooms('room', true) diff --git a/packages/rocketchat-lib/server/models/Rooms.js b/packages/rocketchat-lib/server/models/Rooms.js new file mode 100644 index 0000000000000000000000000000000000000000..7cc89041c72b7dc08b6d873b827f8198c2c48c4f --- /dev/null +++ b/packages/rocketchat-lib/server/models/Rooms.js @@ -0,0 +1,774 @@ +class ModelRooms extends RocketChat.models._Base { + constructor() { + super(...arguments); + + this.tryEnsureIndex({ 'name': 1 }, { unique: 1, sparse: 1 }); + this.tryEnsureIndex({ 'default': 1 }); + this.tryEnsureIndex({ 'usernames': 1 }); + this.tryEnsureIndex({ 't': 1 }); + this.tryEnsureIndex({ 'u._id': 1 }); + + this.cache.ignoreUpdatedFields.push('msgs', 'lm'); + this.cache.ensureIndex(['t', 'name'], 'unique'); + this.cache.options = {fields: {usernames: 0}}; + } + + findOneByIdOrName(_idOrName, options) { + const query = { + $or: [{ + _id: _idOrName + }, { + name: _idOrName + }] + }; + + return this.findOne(query, options); + } + + findOneByImportId(_id, options) { + const query = {importIds: _id}; + + return this.findOne(query, options); + } + + findOneByName(name, options) { + const query = {name}; + + return this.findOne(query, options); + } + + findOneByNameAndType(name, type, options) { + const query = { + name, + t: type + }; + + return this.findOne(query, options); + } + + findOneByIdContainingUsername(_id, username, options) { + const query = { + _id, + usernames: username + }; + + return this.findOne(query, options); + } + + findOneByNameAndTypeNotContainingUsername(name, type, username, options) { + const query = { + name, + t: type, + usernames: { + $ne: username + } + }; + + return this.findOne(query, options); + } + + + // FIND + + findById(roomId, options) { + return this.find({ _id: roomId }, options); + } + + findByIds(roomIds, options) { + return this.find({ _id: {$in: [].concat(roomIds)} }, options); + } + + findByType(type, options) { + const query = {t: type}; + + return this.find(query, options); + } + + findByTypes(types, options) { + const query = { + t: { + $in: types + } + }; + + return this.find(query, options); + } + + findByUserId(userId, options) { + const query = {'u._id': userId}; + + return this.find(query, options); + } + + findBySubscriptionUserId(userId, options) { + let data; + if (this.useCache) { + data = RocketChat.models.Subscriptions.findByUserId(userId).fetch(); + data = data.map(function(item) { + if (item._room) { + return item._room; + } + console.log('Empty Room for Subscription', item); + return {}; + }); + return this.arrayToCursor(this.processQueryOptionsOnResult(data, options)); + } + + data = RocketChat.models.Subscriptions.findByUserId(userId, {fields: {rid: 1}}).fetch(); + data = data.map(item => item.rid); + + const query = { + _id: { + $in: data + } + }; + + return this.find(query, options); + } + + findBySubscriptionUserIdUpdatedAfter(userId, _updatedAt, options) { + if (this.useCache) { + let data = RocketChat.models.Subscriptions.findByUserId(userId).fetch(); + data = data.map(function(item) { + if (item._room) { + return item._room; + } + console.log('Empty Room for Subscription', item); + return {}; + }); + data = data.filter(item => item._updatedAt > _updatedAt); + return this.arrayToCursor(this.processQueryOptionsOnResult(data, options)); + } + + let ids = RocketChat.models.Subscriptions.findByUserId(userId, {fields: {rid: 1}}).fetch(); + ids = ids.map(item => item.rid); + + const query = { + _id: { + $in: ids + }, + _updatedAt: { + $gt: _updatedAt + } + }; + + return this.find(query, options); + } + + findByNameContaining(name, options) { + const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i'); + + const query = { + $or: [ + {name: nameRegex}, + { + t: 'd', + usernames: nameRegex + } + ] + }; + + return this.find(query, options); + } + + findByNameContainingTypesWithUsername(name, types, options) { + const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i'); + + const $or = []; + for (const type of Array.from(types)) { + const obj = {name: nameRegex, t: type.type}; + if (type.username != null) { + obj.usernames = type.username; + } + if (type.ids != null) { + obj._id = {$in: type.ids}; + } + $or.push(obj); + } + + const query = {$or}; + + return this.find(query, options); + } + + findContainingTypesWithUsername(types, options) { + + const $or = []; + for (const type of Array.from(types)) { + const obj = {t: type.type}; + if (type.username != null) { + obj.usernames = type.username; + } + if (type.ids != null) { + obj._id = {$in: type.ids}; + } + $or.push(obj); + } + + const query = {$or}; + + return this.find(query, options); + } + + findByNameContainingAndTypes(name, types, options) { + const nameRegex = new RegExp(s.trim(s.escapeRegExp(name)), 'i'); + + const query = { + t: { + $in: types + }, + $or: [ + {name: nameRegex}, + { + t: 'd', + usernames: nameRegex + } + ] + }; + + return this.find(query, options); + } + + findByNameAndTypeNotDefault(name, type, options) { + const query = { + t: type, + name, + default: { + $ne: true + } + }; + + // do not use cache + return this._db.find(query, options); + } + + findByNameAndTypeNotContainingUsername(name, type, username, options) { + const query = { + t: type, + name, + usernames: { + $ne: username + } + }; + + // do not use cache + return this._db.find(query, options); + } + + findByNameStartingAndTypes(name, types, options) { + const nameRegex = new RegExp(`^${ s.trim(s.escapeRegExp(name)) }`, 'i'); + + const query = { + t: { + $in: types + }, + $or: [ + {name: nameRegex}, + { + t: 'd', + usernames: nameRegex + } + ] + }; + + return this.find(query, options); + } + + findByDefaultAndTypes(defaultValue, types, options) { + const query = { + default: defaultValue, + t: { + $in: types + } + }; + + return this.find(query, options); + } + + findByTypeContainingUsername(type, username, options) { + const query = { + t: type, + usernames: username + }; + + return this.find(query, options); + } + + findByTypeContainingUsernames(type, username, options) { + const query = { + t: type, + usernames: { $all: [].concat(username) } + }; + + return this.find(query, options); + } + + findByTypesAndNotUserIdContainingUsername(types, userId, username, options) { + const query = { + t: { + $in: types + }, + uid: { + $ne: userId + }, + usernames: username + }; + + return this.find(query, options); + } + + findByContainingUsername(username, options) { + const query = {usernames: username}; + + return this.find(query, options); + } + + findByTypeAndName(type, name, options) { + if (this.useCache) { + return this.cache.findByIndex('t,name', [type, name], options); + } + + const query = { + name, + t: type + }; + + return this.find(query, options); + } + + findByTypeAndNameContainingUsername(type, name, username, options) { + const query = { + name, + t: type, + usernames: username + }; + + return this.find(query, options); + } + + findByTypeAndArchivationState(type, archivationstate, options) { + const query = {t: type}; + + if (archivationstate) { + query.archived = true; + } else { + query.archived = { $ne: true }; + } + + return this.find(query, options); + } + + // UPDATE + addImportIds(_id, importIds) { + importIds = [].concat(importIds); + const query = {_id}; + + const update = { + $addToSet: { + importIds: { + $each: importIds + } + } + }; + + return this.update(query, update); + } + + archiveById(_id) { + const query = {_id}; + + const update = { + $set: { + archived: true + } + }; + + return this.update(query, update); + } + + unarchiveById(_id) { + const query = {_id}; + + const update = { + $set: { + archived: false + } + }; + + return this.update(query, update); + } + + addUsernameById(_id, username, muted) { + const query = {_id}; + + const update = { + $addToSet: { + usernames: username + } + }; + + if (muted) { + update.$addToSet.muted = username; + } + + return this.update(query, update); + } + + addUsernamesById(_id, usernames) { + const query = {_id}; + + const update = { + $addToSet: { + usernames: { + $each: usernames + } + } + }; + + return this.update(query, update); + } + + addUsernameByName(name, username) { + const query = {name}; + + const update = { + $addToSet: { + usernames: username + } + }; + + return this.update(query, update); + } + + removeUsernameById(_id, username) { + const query = {_id}; + + const update = { + $pull: { + usernames: username + } + }; + + return this.update(query, update); + } + + removeUsernamesById(_id, usernames) { + const query = {_id}; + + const update = { + $pull: { + usernames: { + $in: usernames + } + } + }; + + return this.update(query, update); + } + + removeUsernameFromAll(username) { + const query = {usernames: username}; + + const update = { + $pull: { + usernames: username + } + }; + + return this.update(query, update, { multi: true }); + } + + removeUsernameByName(name, username) { + const query = {name}; + + const update = { + $pull: { + usernames: username + } + }; + + return this.update(query, update); + } + + setNameById(_id, name) { + const query = {_id}; + + const update = { + $set: { + name + } + }; + + return this.update(query, update); + } + + incMsgCountById(_id, inc) { + if (inc == null) { inc = 1; } + const query = {_id}; + + const update = { + $inc: { + msgs: inc + } + }; + + return this.update(query, update); + } + + incMsgCountAndSetLastMessageTimestampById(_id, inc, lastMessageTimestamp) { + if (inc == null) { inc = 1; } + const query = {_id}; + + const update = { + $set: { + lm: lastMessageTimestamp + }, + $inc: { + msgs: inc + } + }; + + return this.update(query, update); + } + + replaceUsername(previousUsername, username) { + const query = {usernames: previousUsername}; + + const update = { + $set: { + 'usernames.$': username + } + }; + + return this.update(query, update, { multi: true }); + } + + replaceMutedUsername(previousUsername, username) { + const query = {muted: previousUsername}; + + const update = { + $set: { + 'muted.$': username + } + }; + + return this.update(query, update, { multi: true }); + } + + replaceUsernameOfUserByUserId(userId, username) { + const query = {'u._id': userId}; + + const update = { + $set: { + 'u.username': username + } + }; + + return this.update(query, update, { multi: true }); + } + + setJoinCodeById(_id, joinCode) { + let update; + const query = {_id}; + + if ((joinCode != null ? joinCode.trim() : undefined) !== '') { + update = { + $set: { + joinCodeRequired: true, + joinCode + } + }; + } else { + update = { + $set: { + joinCodeRequired: false + }, + $unset: { + joinCode: 1 + } + }; + } + + return this.update(query, update); + } + + setUserById(_id, user) { + const query = {_id}; + + const update = { + $set: { + u: { + _id: user._id, + username: user.username + } + } + }; + + return this.update(query, update); + } + + setTypeById(_id, type) { + const query = {_id}; + const update = { + $set: { + t: type + } + }; + if (type === 'p') { + update.$unset = {default: ''}; + } + + return this.update(query, update); + } + + setTopicById(_id, topic) { + const query = {_id}; + + const update = { + $set: { + topic + } + }; + + return this.update(query, update); + } + + setAnnouncementById(_id, announcement) { + const query = {_id}; + + const update = { + $set: { + announcement + } + }; + + return this.update(query, update); + } + + muteUsernameByRoomId(_id, username) { + const query = {_id}; + + const update = { + $addToSet: { + muted: username + } + }; + + return this.update(query, update); + } + + unmuteUsernameByRoomId(_id, username) { + const query = {_id}; + + const update = { + $pull: { + muted: username + } + }; + + return this.update(query, update); + } + + saveDefaultById(_id, defaultValue) { + const query = {_id}; + + const update = { + $set: { + default: defaultValue === 'true' + } + }; + + return this.update(query, update); + } + + setTopicAndTagsById(_id, topic, tags) { + const setData = {}; + const unsetData = {}; + + if (topic != null) { + if (!_.isEmpty(s.trim(topic))) { + setData.topic = s.trim(topic); + } else { + unsetData.topic = 1; + } + } + + if (tags != null) { + if (!_.isEmpty(s.trim(tags))) { + setData.tags = s.trim(tags).split(',').map(tag => s.trim(tag)); + } else { + unsetData.tags = 1; + } + } + + const update = {}; + + if (!_.isEmpty(setData)) { + update.$set = setData; + } + + if (!_.isEmpty(unsetData)) { + update.$unset = unsetData; + } + + if (_.isEmpty(update)) { + return; + } + + return this.update({ _id }, update); + } + + // INSERT + createWithTypeNameUserAndUsernames(type, name, user, usernames, extraData) { + const room = { + name, + t: type, + usernames, + msgs: 0, + u: { + _id: user._id, + username: user.username + } + }; + + _.extend(room, extraData); + + room._id = this.insert(room); + return room; + } + + createWithIdTypeAndName(_id, type, name, extraData) { + const room = { + _id, + ts: new Date(), + t: type, + name, + usernames: [], + msgs: 0 + }; + + _.extend(room, extraData); + + this.insert(room); + return room; + } + + + // REMOVE + removeById(_id) { + const query = {_id}; + + return this.remove(query); + } + + removeByTypeContainingUsername(type, username) { + const query = { + t: type, + usernames: username + }; + + return this.remove(query); + } +} + +RocketChat.models.Rooms = new ModelRooms('room', true); diff --git a/packages/rocketchat-lib/server/models/Settings.coffee b/packages/rocketchat-lib/server/models/Settings.coffee deleted file mode 100644 index ce0d805be06fba3013292ad68fbb7877d4fba131..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/models/Settings.coffee +++ /dev/null @@ -1,144 +0,0 @@ -class ModelSettings extends RocketChat.models._Base - constructor: -> - super(arguments...) - - @tryEnsureIndex { 'blocked': 1 }, { sparse: 1 } - @tryEnsureIndex { 'hidden': 1 }, { sparse: 1 } - - # FIND - findById: (_id) -> - query = - _id: _id - - return @find query - - findOneNotHiddenById: (_id) -> - query = - _id: _id - hidden: { $ne: true } - - return @findOne query - - findByIds: (_id = []) -> - _id = [].concat _id - - query = - _id: - $in: _id - - return @find query - - findByRole: (role, options) -> - query = - role: role - - return @find query, options - - findPublic: (options) -> - query = - public: true - - return @find query, options - - findNotHiddenPublic: (ids = [])-> - filter = - hidden: { $ne: true } - public: true - - if ids.length > 0 - filter._id = - $in: ids - - return @find filter, { fields: _id: 1, value: 1 } - - findNotHiddenPublicUpdatedAfter: (updatedAt) -> - filter = - hidden: { $ne: true } - public: true - _updatedAt: - $gt: updatedAt - - return @find filter, { fields: _id: 1, value: 1 } - - findNotHiddenPrivate: -> - return @find { - hidden: { $ne: true } - public: { $ne: true } - } - - findNotHidden: (options) -> - return @find { hidden: { $ne: true } }, options - - findNotHiddenUpdatedAfter: (updatedAt)-> - return @find { - hidden: { $ne: true } - _updatedAt: - $gt: updatedAt - } - - # UPDATE - updateValueById: (_id, value) -> - query = - blocked: { $ne: true } - value: { $ne: value } - _id: _id - - update = - $set: - value: value - - return @update query, update - - updateValueAndEditorById: (_id, value, editor) -> - query = - blocked: { $ne: true } - value: { $ne: value } - _id: _id - - update = - $set: - value: value - editor: editor - - return @update query, update - - updateValueNotHiddenById: (_id, value) -> - query = - _id: _id - hidden: { $ne: true } - blocked: { $ne: true } - - update = - $set: - value: value - - return @update query, update - - updateOptionsById: (_id, options) -> - query = - blocked: { $ne: true } - _id: _id - - update = - $set: options - - return @update query, update - - # INSERT - createWithIdAndValue: (_id, value) -> - record = - _id: _id - value: value - _createdAt: new Date - - return @insert record - - # REMOVE - removeById: (_id) -> - query = - blocked: { $ne: true } - _id: _id - - return @remove query - -RocketChat.models.Settings = new ModelSettings('settings', true) diff --git a/packages/rocketchat-lib/server/models/Settings.js b/packages/rocketchat-lib/server/models/Settings.js new file mode 100644 index 0000000000000000000000000000000000000000..7da29bee37fafcb3cf00e99b8e4cd1cf2dadf580 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Settings.js @@ -0,0 +1,178 @@ +class ModelSettings extends RocketChat.models._Base { + constructor() { + super(...arguments); + + this.tryEnsureIndex({ 'blocked': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'hidden': 1 }, { sparse: 1 }); + } + + // FIND + findById(_id) { + const query = {_id}; + + return this.find(query); + } + + findOneNotHiddenById(_id) { + const query = { + _id, + hidden: { $ne: true } + }; + + return this.findOne(query); + } + + findByIds(_id = []) { + _id = [].concat(_id); + + const query = { + _id: { + $in: _id + } + }; + + return this.find(query); + } + + findByRole(role, options) { + const query = {role}; + + return this.find(query, options); + } + + findPublic(options) { + const query = {public: true}; + + return this.find(query, options); + } + + findNotHiddenPublic(ids = []) { + const filter = { + hidden: { $ne: true }, + public: true + }; + + if (ids.length > 0) { + filter._id = + {$in: ids}; + } + + return this.find(filter, { fields: {_id: 1, value: 1} }); + } + + findNotHiddenPublicUpdatedAfter(updatedAt) { + const filter = { + hidden: { $ne: true }, + public: true, + _updatedAt: { + $gt: updatedAt + } + }; + + return this.find(filter, { fields: {_id: 1, value: 1} }); + } + + findNotHiddenPrivate() { + return this.find({ + hidden: { $ne: true }, + public: { $ne: true } + }); + } + + findNotHidden(options) { + return this.find({ hidden: { $ne: true } }, options); + } + + findNotHiddenUpdatedAfter(updatedAt) { + return this.find({ + hidden: { $ne: true }, + _updatedAt: { + $gt: updatedAt + } + }); + } + + // UPDATE + updateValueById(_id, value) { + const query = { + blocked: { $ne: true }, + value: { $ne: value }, + _id + }; + + const update = { + $set: { + value + } + }; + + return this.update(query, update); + } + + updateValueAndEditorById(_id, value, editor) { + const query = { + blocked: { $ne: true }, + value: { $ne: value }, + _id + }; + + const update = { + $set: { + value, + editor + } + }; + + return this.update(query, update); + } + + updateValueNotHiddenById(_id, value) { + const query = { + _id, + hidden: { $ne: true }, + blocked: { $ne: true } + }; + + const update = { + $set: { + value + } + }; + + return this.update(query, update); + } + + updateOptionsById(_id, options) { + const query = { + blocked: { $ne: true }, + _id + }; + + const update = {$set: options}; + + return this.update(query, update); + } + + // INSERT + createWithIdAndValue(_id, value) { + const record = { + _id, + value, + _createdAt: new Date + }; + + return this.insert(record); + } + + // REMOVE + removeById(_id) { + const query = { + blocked: { $ne: true }, + _id + }; + + return this.remove(query); + } +} + +RocketChat.models.Settings = new ModelSettings('settings', true); diff --git a/packages/rocketchat-lib/server/models/Subscriptions.coffee b/packages/rocketchat-lib/server/models/Subscriptions.coffee deleted file mode 100644 index 95f12bff51ff2b341bc873211297db331cc5f31e..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/models/Subscriptions.coffee +++ /dev/null @@ -1,438 +0,0 @@ -class ModelSubscriptions extends RocketChat.models._Base - constructor: -> - super(arguments...) - - @tryEnsureIndex { 'rid': 1, 'u._id': 1 }, { unique: 1 } - @tryEnsureIndex { 'rid': 1, 'alert': 1, 'u._id': 1 } - @tryEnsureIndex { 'rid': 1, 'roles': 1 } - @tryEnsureIndex { 'u._id': 1, 'name': 1, 't': 1 } - @tryEnsureIndex { 'u._id': 1, 'name': 1, 't': 1, 'code': 1 }, { unique: 1 } - @tryEnsureIndex { 'open': 1 } - @tryEnsureIndex { 'alert': 1 } - @tryEnsureIndex { 'unread': 1 } - @tryEnsureIndex { 'ts': 1 } - @tryEnsureIndex { 'ls': 1 } - @tryEnsureIndex { 'audioNotification': 1 }, { sparse: 1 } - @tryEnsureIndex { 'desktopNotifications': 1 }, { sparse: 1 } - @tryEnsureIndex { 'mobilePushNotifications': 1 }, { sparse: 1 } - @tryEnsureIndex { 'emailNotifications': 1 }, { sparse: 1 } - @tryEnsureIndex { 'autoTranslate': 1 }, { sparse: 1 } - @tryEnsureIndex { 'autoTranslateLanguage': 1 }, { sparse: 1 } - - this.cache.ensureIndex('rid', 'array') - this.cache.ensureIndex('u._id', 'array') - this.cache.ensureIndex('name', 'array') - this.cache.ensureIndex(['rid', 'u._id'], 'unique') - this.cache.ensureIndex(['name', 'u._id'], 'unique') - - - # FIND ONE - findOneByRoomIdAndUserId: (roomId, userId) -> - if this.useCache - return this.cache.findByIndex('rid,u._id', [roomId, userId]).fetch() - query = - rid: roomId - "u._id": userId - - return @findOne query - - findOneByRoomNameAndUserId: (roomName, userId) -> - if this.useCache - return this.cache.findByIndex('name,u._id', [roomName, userId]).fetch() - query = - name: roomName - "u._id": userId - - return @findOne query - - # FIND - findByUserId: (userId, options) -> - if this.useCache - return this.cache.findByIndex('u._id', userId, options) - - query = - "u._id": userId - - return @find query, options - - findByUserIdUpdatedAfter: (userId, updatedAt, options) -> - query = - "u._id": userId - _updatedAt: - $gt: updatedAt - - return @find query, options - - # FIND - findByRoomIdAndRoles: (roomId, roles, options) -> - roles = [].concat roles - query = - "rid": roomId - "roles": { $in: roles } - - return @find query, options - - findByType: (types, options) -> - query = - t: - $in: types - - return @find query, options - - findByTypeAndUserId: (type, userId, options) -> - query = - t: type - 'u._id': userId - - return @find query, options - - findByTypeNameAndUserId: (type, name, userId, options) -> - query = - t: type - name: name - 'u._id': userId - - return @find query, options - - findByRoomId: (roomId, options) -> - if this.useCache - return this.cache.findByIndex('rid', roomId, options) - - query = - rid: roomId - - return @find query, options - - findByRoomIdAndNotUserId: (roomId, userId, options) -> - query = - rid: roomId - 'u._id': - $ne: userId - - return @find query, options - - getLastSeen: (options = {}) -> - query = { ls: { $exists: 1 } } - options.sort = { ls: -1 } - options.limit = 1 - - return @find(query, options)?.fetch?()?[0]?.ls - - findByRoomIdAndUserIds: (roomId, userIds) -> - query = - rid: roomId - 'u._id': - $in: userIds - - return @find query - - # UPDATE - archiveByRoomId: (roomId) -> - query = - rid: roomId - - update = - $set: - alert: false - open: false - archived: true - - return @update query, update, { multi: true } - - unarchiveByRoomId: (roomId) -> - query = - rid: roomId - - update = - $set: - alert: false - open: true - archived: false - - return @update query, update, { multi: true } - - hideByRoomIdAndUserId: (roomId, userId) -> - query = - rid: roomId - 'u._id': userId - - update = - $set: - alert: false - open: false - - return @update query, update - - openByRoomIdAndUserId: (roomId, userId) -> - query = - rid: roomId - 'u._id': userId - - update = - $set: - open: true - - return @update query, update - - setAsReadByRoomIdAndUserId: (roomId, userId) -> - query = - rid: roomId - 'u._id': userId - - update = - $set: - open: true - alert: false - unread: 0 - ls: new Date - - return @update query, update - - setAsUnreadByRoomIdAndUserId: (roomId, userId, firstMessageUnreadTimestamp) -> - query = - rid: roomId - 'u._id': userId - - update = - $set: - open: true - alert: true - ls: firstMessageUnreadTimestamp - - return @update query, update - - setFavoriteByRoomIdAndUserId: (roomId, userId, favorite=true) -> - query = - rid: roomId - 'u._id': userId - - update = - $set: - f: favorite - - return @update query, update - - updateNameAndAlertByRoomId: (roomId, name) -> - query = - rid: roomId - - update = - $set: - name: name - alert: true - - return @update query, update, { multi: true } - - updateNameByRoomId: (roomId, name) -> - query = - rid: roomId - - update = - $set: - name: name - - return @update query, update, { multi: true } - - setUserUsernameByUserId: (userId, username) -> - query = - "u._id": userId - - update = - $set: - "u.username": username - - return @update query, update, { multi: true } - - setNameForDirectRoomsWithOldName: (oldName, name) -> - query = - name: oldName - t: "d" - - update = - $set: - name: name - - return @update query, update, { multi: true } - - incUnreadOfDirectForRoomIdExcludingUserId: (roomId, userId, inc=1) -> - query = - rid: roomId - t: 'd' - 'u._id': - $ne: userId - - update = - $set: - alert: true - open: true - $inc: - unread: inc - - return @update query, update, { multi: true } - - incUnreadForRoomIdExcludingUserId: (roomId, userId, inc=1) -> - query = - rid: roomId - 'u._id': - $ne: userId - - update = - $set: - alert: true - open: true - $inc: - unread: inc - - return @update query, update, { multi: true } - - incUnreadForRoomIdAndUserIds: (roomId, userIds, inc=1) -> - query = - rid: roomId - 'u._id': - $in: userIds - - update = - $set: - alert: true - open: true - $inc: - unread: inc - - return @update query, update, { multi: true } - - setAlertForRoomIdExcludingUserId: (roomId, userId) -> - query = - rid: roomId - 'u._id': - $ne: userId - $or: [ - { alert: { $ne: true } } - { open: { $ne: true } } - ] - - update = - $set: - alert: true - open: true - - return @update query, update, { multi: true } - - setBlockedByRoomId: (rid, blocked, blocker) -> - query = - rid: rid - 'u._id': blocked - - update = - $set: - blocked: true - - query2 = - rid: rid - 'u._id': blocker - - update2 = - $set: - blocker: true - - return @update(query, update) and @update(query2, update2) - - unsetBlockedByRoomId: (rid, blocked, blocker) -> - query = - rid: rid - 'u._id': blocked - - update = - $unset: - blocked: 1 - - query2 = - rid: rid - 'u._id': blocker - - update2 = - $unset: - blocker: 1 - - return @update(query, update) and @update(query2, update2) - - updateTypeByRoomId: (roomId, type) -> - query = - rid: roomId - - update = - $set: - t: type - - return @update query, update, { multi: true } - - addRoleById: (_id, role) -> - query = - _id: _id - - update = - $addToSet: - roles: role - - return @update query, update - - removeRoleById: (_id, role) -> - query = - _id: _id - - update = - $pull: - roles: role - - return @update query, update - - setArchivedByUsername: (username, archived) -> - query = - t: 'd' - name: username - - update = - $set: - archived: archived - - return @update query, update, { multi: true } - - # INSERT - createWithRoomAndUser: (room, user, extraData) -> - subscription = - open: false - alert: false - unread: 0 - ts: room.ts - rid: room._id - name: room.name - t: room.t - u: - _id: user._id - username: user.username - - _.extend subscription, extraData - - return @insert subscription - - - # REMOVE - removeByUserId: (userId) -> - query = - "u._id": userId - - return @remove query - - removeByRoomId: (roomId) -> - query = - rid: roomId - - return @remove query - - removeByRoomIdAndUserId: (roomId, userId) -> - query = - rid: roomId - "u._id": userId - - return @remove query - -RocketChat.models.Subscriptions = new ModelSubscriptions('subscription', true) diff --git a/packages/rocketchat-lib/server/models/Subscriptions.js b/packages/rocketchat-lib/server/models/Subscriptions.js new file mode 100644 index 0000000000000000000000000000000000000000..2cd8febf09a14693888428326d5c5d117adec6c4 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Subscriptions.js @@ -0,0 +1,581 @@ +class ModelSubscriptions extends RocketChat.models._Base { + constructor() { + super(...arguments); + + this.tryEnsureIndex({ 'rid': 1, 'u._id': 1 }, { unique: 1 }); + this.tryEnsureIndex({ 'rid': 1, 'alert': 1, 'u._id': 1 }); + this.tryEnsureIndex({ 'rid': 1, 'roles': 1 }); + this.tryEnsureIndex({ 'u._id': 1, 'name': 1, 't': 1 }); + this.tryEnsureIndex({ 'u._id': 1, 'name': 1, 't': 1, 'code': 1 }, { unique: 1 }); + this.tryEnsureIndex({ 'open': 1 }); + this.tryEnsureIndex({ 'alert': 1 }); + this.tryEnsureIndex({ 'unread': 1 }); + this.tryEnsureIndex({ 'ts': 1 }); + this.tryEnsureIndex({ 'ls': 1 }); + this.tryEnsureIndex({ 'audioNotification': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'desktopNotifications': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'mobilePushNotifications': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'emailNotifications': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'autoTranslate': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'autoTranslateLanguage': 1 }, { sparse: 1 }); + + this.cache.ensureIndex('rid', 'array'); + this.cache.ensureIndex('u._id', 'array'); + this.cache.ensureIndex('name', 'array'); + this.cache.ensureIndex(['rid', 'u._id'], 'unique'); + this.cache.ensureIndex(['name', 'u._id'], 'unique'); + } + + + // FIND ONE + findOneByRoomIdAndUserId(roomId, userId) { + if (this.useCache) { + return this.cache.findByIndex('rid,u._id', [roomId, userId]).fetch(); + } + const query = { + rid: roomId, + 'u._id': userId + }; + + return this.findOne(query); + } + + findOneByRoomNameAndUserId(roomName, userId) { + if (this.useCache) { + return this.cache.findByIndex('name,u._id', [roomName, userId]).fetch(); + } + const query = { + name: roomName, + 'u._id': userId + }; + + return this.findOne(query); + } + + // FIND + findByUserId(userId, options) { + if (this.useCache) { + return this.cache.findByIndex('u._id', userId, options); + } + + const query = + {'u._id': userId}; + + return this.find(query, options); + } + + findByUserIdUpdatedAfter(userId, updatedAt, options) { + const query = { + 'u._id': userId, + _updatedAt: { + $gt: updatedAt + } + }; + + return this.find(query, options); + } + + // FIND + findByRoomIdAndRoles(roomId, roles, options) { + roles = [].concat(roles); + const query = { + 'rid': roomId, + 'roles': { $in: roles } + }; + + return this.find(query, options); + } + + findByType(types, options) { + const query = { + t: { + $in: types + } + }; + + return this.find(query, options); + } + + findByTypeAndUserId(type, userId, options) { + const query = { + t: type, + 'u._id': userId + }; + + return this.find(query, options); + } + + findByTypeNameAndUserId(type, name, userId, options) { + const query = { + t: type, + name, + 'u._id': userId + }; + + return this.find(query, options); + } + + findByRoomId(roomId, options) { + if (this.useCache) { + return this.cache.findByIndex('rid', roomId, options); + } + + const query = + {rid: roomId}; + + return this.find(query, options); + } + + findByRoomIdAndNotUserId(roomId, userId, options) { + const query = { + rid: roomId, + 'u._id': { + $ne: userId + } + }; + + return this.find(query, options); + } + + getLastSeen(options) { + if (options == null) { options = {}; } + const query = { ls: { $exists: 1 } }; + options.sort = { ls: -1 }; + options.limit = 1; + const [subscription] = this.find(query, options).fetch(); + return subscription && subscription.ls; + } + + findByRoomIdAndUserIds(roomId, userIds) { + const query = { + rid: roomId, + 'u._id': { + $in: userIds + } + }; + + return this.find(query); + } + + findUnreadByUserId(userId) { + const query = { + 'u._id': userId, + unread: { + $gt: 0 + } + }; + + return this.find(query, { fields: { unread: 1 } }); + } + + // UPDATE + archiveByRoomId(roomId) { + const query = + {rid: roomId}; + + const update = { + $set: { + alert: false, + open: false, + archived: true + } + }; + + return this.update(query, update, { multi: true }); + } + + unarchiveByRoomId(roomId) { + const query = + {rid: roomId}; + + const update = { + $set: { + alert: false, + open: true, + archived: false + } + }; + + return this.update(query, update, { multi: true }); + } + + hideByRoomIdAndUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + alert: false, + open: false + } + }; + + return this.update(query, update); + } + + openByRoomIdAndUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + open: true + } + }; + + return this.update(query, update); + } + + setAsReadByRoomIdAndUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + open: true, + alert: false, + unread: 0, + ls: new Date + } + }; + + return this.update(query, update); + } + + setAsUnreadByRoomIdAndUserId(roomId, userId, firstMessageUnreadTimestamp) { + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + open: true, + alert: true, + ls: firstMessageUnreadTimestamp + } + }; + + return this.update(query, update); + } + + setFavoriteByRoomIdAndUserId(roomId, userId, favorite) { + if (favorite == null) { favorite = true; } + const query = { + rid: roomId, + 'u._id': userId + }; + + const update = { + $set: { + f: favorite + } + }; + + return this.update(query, update); + } + + updateNameAndAlertByRoomId(roomId, name) { + const query = + {rid: roomId}; + + const update = { + $set: { + name, + alert: true + } + }; + + return this.update(query, update, { multi: true }); + } + + updateNameByRoomId(roomId, name) { + const query = + {rid: roomId}; + + const update = { + $set: { + name + } + }; + + return this.update(query, update, { multi: true }); + } + + setUserUsernameByUserId(userId, username) { + const query = + {'u._id': userId}; + + const update = { + $set: { + 'u.username': username + } + }; + + return this.update(query, update, { multi: true }); + } + + setNameForDirectRoomsWithOldName(oldName, name) { + const query = { + name: oldName, + t: 'd' + }; + + const update = { + $set: { + name + } + }; + + return this.update(query, update, { multi: true }); + } + + incUnreadOfDirectForRoomIdExcludingUserId(roomId, userId, inc) { + if (inc == null) { inc = 1; } + const query = { + rid: roomId, + t: 'd', + 'u._id': { + $ne: userId + } + }; + + const update = { + $set: { + alert: true, + open: true + }, + $inc: { + unread: inc + } + }; + + return this.update(query, update, { multi: true }); + } + + incUnreadForRoomIdExcludingUserId(roomId, userId, inc) { + if (inc == null) { inc = 1; } + const query = { + rid: roomId, + 'u._id': { + $ne: userId + } + }; + + const update = { + $set: { + alert: true, + open: true + }, + $inc: { + unread: inc + } + }; + + return this.update(query, update, { multi: true }); + } + + incUnreadForRoomIdAndUserIds(roomId, userIds, inc) { + if (inc == null) { inc = 1; } + const query = { + rid: roomId, + 'u._id': { + $in: userIds + } + }; + + const update = { + $set: { + alert: true, + open: true + }, + $inc: { + unread: inc + } + }; + + return this.update(query, update, { multi: true }); + } + + setAlertForRoomIdExcludingUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': { + $ne: userId + }, + $or: [ + { alert: { $ne: true } }, + { open: { $ne: true } } + ] + }; + + const update = { + $set: { + alert: true, + open: true + } + }; + + return this.update(query, update, { multi: true }); + } + + setBlockedByRoomId(rid, blocked, blocker) { + const query = { + rid, + 'u._id': blocked + }; + + const update = { + $set: { + blocked: true + } + }; + + const query2 = { + rid, + 'u._id': blocker + }; + + const update2 = { + $set: { + blocker: true + } + }; + + return this.update(query, update) && this.update(query2, update2); + } + + unsetBlockedByRoomId(rid, blocked, blocker) { + const query = { + rid, + 'u._id': blocked + }; + + const update = { + $unset: { + blocked: 1 + } + }; + + const query2 = { + rid, + 'u._id': blocker + }; + + const update2 = { + $unset: { + blocker: 1 + } + }; + + return this.update(query, update) && this.update(query2, update2); + } + + updateTypeByRoomId(roomId, type) { + const query = + {rid: roomId}; + + const update = { + $set: { + t: type + } + }; + + return this.update(query, update, { multi: true }); + } + + addRoleById(_id, role) { + const query = + {_id}; + + const update = { + $addToSet: { + roles: role + } + }; + + return this.update(query, update); + } + + removeRoleById(_id, role) { + const query = + {_id}; + + const update = { + $pull: { + roles: role + } + }; + + return this.update(query, update); + } + + setArchivedByUsername(username, archived) { + const query = { + t: 'd', + name: username + }; + + const update = { + $set: { + archived + } + }; + + return this.update(query, update, { multi: true }); + } + + // INSERT + createWithRoomAndUser(room, user, extraData) { + const subscription = { + open: false, + alert: false, + unread: 0, + ts: room.ts, + rid: room._id, + name: room.name, + t: room.t, + u: { + _id: user._id, + username: user.username + } + }; + + _.extend(subscription, extraData); + + return this.insert(subscription); + } + + + // REMOVE + removeByUserId(userId) { + const query = + {'u._id': userId}; + + return this.remove(query); + } + + removeByRoomId(roomId) { + const query = + {rid: roomId}; + + return this.remove(query); + } + + removeByRoomIdAndUserId(roomId, userId) { + const query = { + rid: roomId, + 'u._id': userId + }; + + return this.remove(query); + } +} + +RocketChat.models.Subscriptions = new ModelSubscriptions('subscription', true); diff --git a/packages/rocketchat-lib/server/models/Uploads.coffee b/packages/rocketchat-lib/server/models/Uploads.coffee deleted file mode 100644 index 53dd79256891953202f59590c6f88784485a227a..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/models/Uploads.coffee +++ /dev/null @@ -1,73 +0,0 @@ -RocketChat.models.Uploads = new class extends RocketChat.models._Base - constructor: -> - super('uploads') - - @tryEnsureIndex { 'rid': 1 } - @tryEnsureIndex { 'uploadedAt': 1 } - - findNotHiddenFilesOfRoom: (roomId, limit) -> - fileQuery = - rid: roomId - complete: true - uploading: false - _hidden: - $ne: true - - fileOptions = - limit: limit - sort: - uploadedAt: -1 - fields: - _id: 1 - userId: 1 - rid: 1 - name: 1 - description: 1 - type: 1 - url: 1 - uploadedAt: 1 - - return @find fileQuery, fileOptions - - insertFileInit: (roomId, userId, store, file, extra) -> - fileData = - rid: roomId - userId: userId - store: store - complete: false - uploading: true - progress: 0 - extension: s.strRightBack(file.name, '.') - uploadedAt: new Date() - - _.extend(fileData, file, extra); - - if @model.direct?.insert? - file = @model.direct.insert fileData - else - file = @insert fileData - - return file - - updateFileComplete: (fileId, userId, file) -> - if not fileId - return - - filter = - _id: fileId - userId: userId - - update = - $set: - complete: true - uploading: false - progress: 1 - - update.$set = _.extend file, update.$set - - if @model.direct?.insert? - result = @model.direct.update filter, update - else - result = @update filter, update - - return result diff --git a/packages/rocketchat-lib/server/models/Uploads.js b/packages/rocketchat-lib/server/models/Uploads.js new file mode 100644 index 0000000000000000000000000000000000000000..34d1dc6b9ea5c984c42a581448e80d51797e075a --- /dev/null +++ b/packages/rocketchat-lib/server/models/Uploads.js @@ -0,0 +1,104 @@ +/* globals InstanceStatus */ + +RocketChat.models.Uploads = new class extends RocketChat.models._Base { + constructor() { + super('uploads'); + + this.model.before.insert((userId, doc) => { + doc.instanceId = InstanceStatus.id(); + }); + + this.tryEnsureIndex({ 'rid': 1 }); + this.tryEnsureIndex({ 'uploadedAt': 1 }); + } + + findNotHiddenFilesOfRoom(roomId, limit) { + const fileQuery = { + rid: roomId, + complete: true, + uploading: false, + _hidden: { + $ne: true + } + }; + + const fileOptions = { + limit, + sort: { + uploadedAt: -1 + }, + fields: { + _id: 1, + userId: 1, + rid: 1, + name: 1, + description: 1, + type: 1, + url: 1, + uploadedAt: 1 + } + }; + + return this.find(fileQuery, fileOptions); + } + + insertFileInit(userId, store, file, extra) { + const fileData = { + userId, + store, + complete: false, + uploading: true, + progress: 0, + extension: s.strRightBack(file.name, '.'), + uploadedAt: new Date() + }; + + _.extend(fileData, file, extra); + + if (this.model.direct && this.model.direct.insert != null) { + file = this.model.direct.insert(fileData); + } else { + file = this.insert(fileData); + } + + return file; + } + + updateFileComplete(fileId, userId, file) { + let result; + if (!fileId) { + return; + } + + const filter = { + _id: fileId, + userId + }; + + const update = { + $set: { + complete: true, + uploading: false, + progress: 1 + } + }; + + update.$set = _.extend(file, update.$set); + + if (this.model.direct && this.model.direct.update != null) { + result = this.model.direct.update(filter, update); + } else { + result = this.update(filter, update); + } + + return result; + } + + deleteFile(fileId) { + if (this.model.direct && this.model.direct.remove != null) { + return this.model.direct.remove({ _id: fileId }); + } else { + return this.remove({ _id: fileId }); + } + } +}; diff --git a/packages/rocketchat-lib/server/models/Users.coffee b/packages/rocketchat-lib/server/models/Users.coffee deleted file mode 100644 index 83519874f9fa19860fbe18696af350ca3a88e233..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/server/models/Users.coffee +++ /dev/null @@ -1,428 +0,0 @@ -class ModelUsers extends RocketChat.models._Base - constructor: -> - super(arguments...) - - @tryEnsureIndex { 'roles': 1 }, { sparse: 1 } - @tryEnsureIndex { 'name': 1 } - @tryEnsureIndex { 'lastLogin': 1 } - @tryEnsureIndex { 'status': 1 } - @tryEnsureIndex { 'active': 1 }, { sparse: 1 } - @tryEnsureIndex { 'statusConnection': 1 }, { sparse: 1 } - @tryEnsureIndex { 'type': 1 } - - this.cache.ensureIndex('username', 'unique') - - findOneByImportId: (_id, options) -> - return @findOne { importIds: _id }, options - - findOneByUsername: (username, options) -> - query = - username: username - - return @findOne query, options - - findOneByEmailAddress: (emailAddress, options) -> - query = - 'emails.address': new RegExp("^" + s.escapeRegExp(emailAddress) + "$", 'i') - - return @findOne query, options - - findOneAdmin: (admin, options) -> - query = - admin: admin - - return @findOne query, options - - findOneByIdAndLoginToken: (_id, token, options) -> - query = - _id: _id - 'services.resume.loginTokens.hashedToken' : Accounts._hashLoginToken(token) - - return @findOne query, options - - - # FIND - findById: (userId) -> - query = - _id: userId - - return @find query - - findUsersNotOffline: (options) -> - query = - username: - $exists: 1 - status: - $in: ['online', 'away', 'busy'] - - return @find query, options - - - findByUsername: (username, options) -> - query = - username: username - - return @find query, options - - findUsersByUsernamesWithHighlights: (usernames, options) -> - if this.useCache - result = - fetch: () -> - return RocketChat.models.Users.getDynamicView('highlights').data().filter (record) -> - return usernames.indexOf(record.username) > -1 - count: () -> - return result.fetch().length - forEach: (fn) -> - return result.fetch().forEach(fn) - return result - - query = - username: { $in: usernames } - 'settings.preferences.highlights.0': - $exists: true - - return @find query, options - - findActiveByUsernameOrNameRegexWithExceptions: (searchTerm, exceptions = [], options = {}) -> - if not _.isArray exceptions - exceptions = [ exceptions ] - - termRegex = new RegExp s.escapeRegExp(searchTerm), 'i' - query = { - $or: [{ - username: termRegex - }, { - name: termRegex - }], - active: true, - type: { - $in: ['user', 'bot'] - }, - $and: [{ - username: { - $exists: true - } - }, { - username: { - $nin: exceptions - } - }] - } - - return @find query, options - - findByActiveUsersExcept: (searchTerm, exceptions = [], options = {}) -> - if not _.isArray exceptions - exceptions = [ exceptions ] - - termRegex = new RegExp s.escapeRegExp(searchTerm), 'i' - query = - $and: [ - { - active: true - $or: [ - { - username: termRegex - } - { - name: termRegex - } - ] - } - { - username: { $exists: true, $nin: exceptions } - } - ] - - return @find query, options - - findUsersByNameOrUsername: (nameOrUsername, options) -> - query = - username: - $exists: 1 - - $or: [ - {name: nameOrUsername} - {username: nameOrUsername} - ] - - type: - $in: ['user'] - - return @find query, options - - findByUsernameNameOrEmailAddress: (usernameNameOrEmailAddress, options) -> - query = - $or: [ - {name: usernameNameOrEmailAddress} - {username: usernameNameOrEmailAddress} - {'emails.address': usernameNameOrEmailAddress} - ] - type: - $in: ['user', 'bot'] - - return @find query, options - - findLDAPUsers: (options) -> - query = - ldap: true - - return @find query, options - - findCrowdUsers: (options) -> - query = - crowd: true - - return @find query, options - - getLastLogin: (options = {}) -> - query = { lastLogin: { $exists: 1 } } - options.sort = { lastLogin: -1 } - options.limit = 1 - - return @find(query, options)?.fetch?()?[0]?.lastLogin - - findUsersByUsernames: (usernames, options) -> - query = - username: - $in: usernames - - return @find query, options - - # UPDATE - addImportIds: (_id, importIds) -> - importIds = [].concat(importIds) - - query = - _id: _id - - update = - $addToSet: - importIds: - $each: importIds - - return @update query, update - - updateLastLoginById: (_id) -> - update = - $set: - lastLogin: new Date - - return @update _id, update - - setServiceId: (_id, serviceName, serviceId) -> - update = - $set: {} - - serviceIdKey = "services.#{serviceName}.id" - update.$set[serviceIdKey] = serviceId - - return @update _id, update - - setUsername: (_id, username) -> - update = - $set: username: username - - return @update _id, update - - setEmail: (_id, email) -> - update = - $set: - emails: [ - address: email - verified: false - ] - - return @update _id, update - - setEmailVerified: (_id, email) -> - query = - _id: _id - emails: - $elemMatch: - address: email - verified: false - - update = - $set: - 'emails.$.verified': true - - return @update query, update - - setName: (_id, name) -> - update = - $set: - name: name - - return @update _id, update - - setCustomFields: (_id, fields) -> - values = {} - for key, value of fields - values["customFields.#{key}"] = value - - update = - $set: values - - return @update _id, update - - setAvatarOrigin: (_id, origin) -> - update = - $set: - avatarOrigin: origin - - return @update _id, update - - unsetAvatarOrigin: (_id) -> - update = - $unset: - avatarOrigin: 1 - - return @update _id, update - - setUserActive: (_id, active=true) -> - update = - $set: - active: active - - return @update _id, update - - setAllUsersActive: (active) -> - update = - $set: - active: active - - return @update {}, update, { multi: true } - - unsetLoginTokens: (_id) -> - update = - $set: - "services.resume.loginTokens" : [] - - return @update _id, update - - unsetRequirePasswordChange: (_id) -> - update = - $unset: - "requirePasswordChange" : true - "requirePasswordChangeReason" : true - - return @update _id, update - - resetPasswordAndSetRequirePasswordChange: (_id, requirePasswordChange, requirePasswordChangeReason) -> - update = - $unset: - "services.password": 1 - $set: - "requirePasswordChange" : requirePasswordChange, - "requirePasswordChangeReason": requirePasswordChangeReason - - return @update _id, update - - setLanguage: (_id, language) -> - update = - $set: - language: language - - return @update _id, update - - setProfile: (_id, profile) -> - update = - $set: - "settings.profile": profile - - return @update _id, update - - setPreferences: (_id, preferences) -> - update = - $set: - "settings.preferences": preferences - - return @update _id, update - - setUtcOffset: (_id, utcOffset) -> - query = - _id: _id - utcOffset: - $ne: utcOffset - - update = - $set: - utcOffset: utcOffset - - return @update query, update - - saveUserById: (_id, data) -> - setData = {} - unsetData = {} - - if data.name? - if not _.isEmpty(s.trim(data.name)) - setData.name = s.trim(data.name) - else - unsetData.name = 1 - - if data.email? - if not _.isEmpty(s.trim(data.email)) - setData.emails = [ - address: s.trim(data.email) - ] - else - unsetData.emails = 1 - - if data.phone? - if not _.isEmpty(s.trim(data.phone)) - setData.phone = [ - phoneNumber: s.trim(data.phone) - ] - else - unsetData.phone = 1 - - update = {} - - if not _.isEmpty setData - update.$set = setData - - if not _.isEmpty unsetData - update.$unset = unsetData - - if _.isEmpty update - return true - - return @update { _id: _id }, update - - # INSERT - create: (data) -> - user = - createdAt: new Date - avatarOrigin: 'none' - - _.extend user, data - - return @insert user - - - # REMOVE - removeById: (_id) -> - return @remove _id - - ### - Find users to send a message by email if: - - he is not online - - has a verified email - - has not disabled email notifications - - `active` is equal to true (false means they were deactivated and can't login) - ### - getUsersToSendOfflineEmail: (usersIds) -> - query = - _id: - $in: usersIds - active: true - status: 'offline' - statusConnection: - $ne: 'online' - 'emails.verified': true - - return @find query, { fields: { name: 1, username: 1, emails: 1, 'settings.preferences.emailNotificationMode': 1 } } - -RocketChat.models.Users = new ModelUsers(Meteor.users, true) diff --git a/packages/rocketchat-lib/server/models/Users.js b/packages/rocketchat-lib/server/models/Users.js new file mode 100644 index 0000000000000000000000000000000000000000..00513f5adc9602213dfa2fc412d15f6c23b36134 --- /dev/null +++ b/packages/rocketchat-lib/server/models/Users.js @@ -0,0 +1,538 @@ +class ModelUsers extends RocketChat.models._Base { + constructor() { + super(...arguments); + + this.tryEnsureIndex({ 'roles': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'name': 1 }); + this.tryEnsureIndex({ 'lastLogin': 1 }); + this.tryEnsureIndex({ 'status': 1 }); + this.tryEnsureIndex({ 'active': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'statusConnection': 1 }, { sparse: 1 }); + this.tryEnsureIndex({ 'type': 1 }); + + this.cache.ensureIndex('username', 'unique'); + } + + findOneByImportId(_id, options) { + return this.findOne({ importIds: _id }, options); + } + + findOneByUsername(username, options) { + const query = {username}; + + return this.findOne(query, options); + } + + findOneByEmailAddress(emailAddress, options) { + const query = {'emails.address': new RegExp(`^${ s.escapeRegExp(emailAddress) }$`, 'i')}; + + return this.findOne(query, options); + } + + findOneAdmin(admin, options) { + const query = {admin}; + + return this.findOne(query, options); + } + + findOneByIdAndLoginToken(_id, token, options) { + const query = { + _id, + 'services.resume.loginTokens.hashedToken' : Accounts._hashLoginToken(token) + }; + + return this.findOne(query, options); + } + + + // FIND + findById(userId) { + const query = {_id: userId}; + + return this.find(query); + } + + findUsersNotOffline(options) { + const query = { + username: { + $exists: 1 + }, + status: { + $in: ['online', 'away', 'busy'] + } + }; + + return this.find(query, options); + } + + + findByUsername(username, options) { + const query = {username}; + + return this.find(query, options); + } + + findUsersByUsernamesWithHighlights(usernames, options) { + if (this.useCache) { + const result = { + fetch() { + return RocketChat.models.Users.getDynamicView('highlights').data().filter(record => usernames.indexOf(record.username) > -1); + }, + count() { + return result.fetch().length; + }, + forEach(fn) { + return result.fetch().forEach(fn); + } + }; + return result; + } + + const query = { + username: { $in: usernames }, + 'settings.preferences.highlights.0': { + $exists: true + } + }; + + return this.find(query, options); + } + + findActiveByUsernameOrNameRegexWithExceptions(searchTerm, exceptions, options) { + if (exceptions == null) { exceptions = []; } + if (options == null) { options = {}; } + if (!_.isArray(exceptions)) { + exceptions = [ exceptions ]; + } + + const termRegex = new RegExp(s.escapeRegExp(searchTerm), 'i'); + const query = { + $or: [{ + username: termRegex + }, { + name: termRegex + }], + active: true, + type: { + $in: ['user', 'bot'] + }, + $and: [{ + username: { + $exists: true + } + }, { + username: { + $nin: exceptions + } + }] + }; + + return this.find(query, options); + } + + findByActiveUsersExcept(searchTerm, exceptions, options) { + if (exceptions == null) { exceptions = []; } + if (options == null) { options = {}; } + if (!_.isArray(exceptions)) { + exceptions = [ exceptions ]; + } + + const termRegex = new RegExp(s.escapeRegExp(searchTerm), 'i'); + const query = { + $and: [ + { + active: true, + $or: [ + { + username: termRegex + }, + { + name: termRegex + } + ] + }, + { + username: { $exists: true, $nin: exceptions } + } + ] + }; + + // do not use cache + return this._db.find(query, options); + } + + findUsersByNameOrUsername(nameOrUsername, options) { + const query = { + username: { + $exists: 1 + }, + + $or: [ + {name: nameOrUsername}, + {username: nameOrUsername} + ], + + type: { + $in: ['user'] + } + }; + + return this.find(query, options); + } + + findByUsernameNameOrEmailAddress(usernameNameOrEmailAddress, options) { + const query = { + $or: [ + {name: usernameNameOrEmailAddress}, + {username: usernameNameOrEmailAddress}, + {'emails.address': usernameNameOrEmailAddress} + ], + type: { + $in: ['user', 'bot'] + } + }; + + return this.find(query, options); + } + + findLDAPUsers(options) { + const query = {ldap: true}; + + return this.find(query, options); + } + + findCrowdUsers(options) { + const query = {crowd: true}; + + return this.find(query, options); + } + + getLastLogin(options) { + if (options == null) { options = {}; } + const query = { lastLogin: { $exists: 1 } }; + options.sort = { lastLogin: -1 }; + options.limit = 1; + const [user] = this.find(query, options).fetch(); + return user && user.lastLogin; + } + + findUsersByUsernames(usernames, options) { + const query = { + username: { + $in: usernames + } + }; + + return this.find(query, options); + } + + // UPDATE + addImportIds(_id, importIds) { + importIds = [].concat(importIds); + + const query = {_id}; + + const update = { + $addToSet: { + importIds: { + $each: importIds + } + } + }; + + return this.update(query, update); + } + + updateLastLoginById(_id) { + const update = { + $set: { + lastLogin: new Date + } + }; + + return this.update(_id, update); + } + + setServiceId(_id, serviceName, serviceId) { + const update = + {$set: {}}; + + const serviceIdKey = `services.${ serviceName }.id`; + update.$set[serviceIdKey] = serviceId; + + return this.update(_id, update); + } + + setUsername(_id, username) { + const update = + {$set: {username}}; + + return this.update(_id, update); + } + + setEmail(_id, email) { + const update = { + $set: { + emails: [{ + address: email, + verified: false + } + ] + } + }; + + return this.update(_id, update); + } + + setEmailVerified(_id, email) { + const query = { + _id, + emails: { + $elemMatch: { + address: email, + verified: false + } + } + }; + + const update = { + $set: { + 'emails.$.verified': true + } + }; + + return this.update(query, update); + } + + setName(_id, name) { + const update = { + $set: { + name + } + }; + + return this.update(_id, update); + } + + setCustomFields(_id, fields) { + const values = {}; + Object.keys(fields).forEach(key => { + values[`customFields.${ key }`] = fields[key]; + }); + + const update = {$set: values}; + + return this.update(_id, update); + } + + setAvatarOrigin(_id, origin) { + const update = { + $set: { + avatarOrigin: origin + } + }; + + return this.update(_id, update); + } + + unsetAvatarOrigin(_id) { + const update = { + $unset: { + avatarOrigin: 1 + } + }; + + return this.update(_id, update); + } + + setUserActive(_id, active) { + if (active == null) { active = true; } + const update = { + $set: { + active + } + }; + + return this.update(_id, update); + } + + setAllUsersActive(active) { + const update = { + $set: { + active + } + }; + + return this.update({}, update, { multi: true }); + } + + unsetLoginTokens(_id) { + const update = { + $set: { + 'services.resume.loginTokens' : [] + } + }; + + return this.update(_id, update); + } + + unsetRequirePasswordChange(_id) { + const update = { + $unset: { + 'requirePasswordChange' : true, + 'requirePasswordChangeReason' : true + } + }; + + return this.update(_id, update); + } + + resetPasswordAndSetRequirePasswordChange(_id, requirePasswordChange, requirePasswordChangeReason) { + const update = { + $unset: { + 'services.password': 1 + }, + $set: { + requirePasswordChange, + requirePasswordChangeReason + } + }; + + return this.update(_id, update); + } + + setLanguage(_id, language) { + const update = { + $set: { + language + } + }; + + return this.update(_id, update); + } + + setProfile(_id, profile) { + const update = { + $set: { + 'settings.profile': profile + } + }; + + return this.update(_id, update); + } + + setPreferences(_id, preferences) { + const update = { + $set: { + 'settings.preferences': preferences + } + }; + + return this.update(_id, update); + } + + setUtcOffset(_id, utcOffset) { + const query = { + _id, + utcOffset: { + $ne: utcOffset + } + }; + + const update = { + $set: { + utcOffset + } + }; + + return this.update(query, update); + } + + saveUserById(_id, data) { + const setData = {}; + const unsetData = {}; + + if (data.name != null) { + if (!_.isEmpty(s.trim(data.name))) { + setData.name = s.trim(data.name); + } else { + unsetData.name = 1; + } + } + + if (data.email != null) { + if (!_.isEmpty(s.trim(data.email))) { + setData.emails = [{address: s.trim(data.email)}]; + } else { + unsetData.emails = 1; + } + } + + if (data.phone != null) { + if (!_.isEmpty(s.trim(data.phone))) { + setData.phone = [{phoneNumber: s.trim(data.phone)}]; + } else { + unsetData.phone = 1; + } + } + + const update = {}; + + if (!_.isEmpty(setData)) { + update.$set = setData; + } + + if (!_.isEmpty(unsetData)) { + update.$unset = unsetData; + } + + if (_.isEmpty(update)) { + return true; + } + + return this.update({ _id }, update); + } + +// INSERT + create(data) { + const user = { + createdAt: new Date, + avatarOrigin: 'none' + }; + + _.extend(user, data); + + return this.insert(user); + } + + +// REMOVE + removeById(_id) { + return this.remove(_id); + } + +/* +Find users to send a message by email if: +- he is not online +- has a verified email +- has not disabled email notifications +- `active` is equal to true (false means they were deactivated and can't login) +*/ + getUsersToSendOfflineEmail(usersIds) { + const query = { + _id: { + $in: usersIds + }, + active: true, + status: 'offline', + statusConnection: { + $ne: 'online' + }, + 'emails.verified': true + }; + + return this.find(query, { fields: { name: 1, username: 1, emails: 1, 'settings.preferences.emailNotificationMode': 1 } }); + } +} + +RocketChat.models.Users = new ModelUsers(Meteor.users, true); diff --git a/packages/rocketchat-lib/server/startup/settings.js b/packages/rocketchat-lib/server/startup/settings.js index bd77f407b7d70c0e630c151e1142befee161be11..1671d5185bee19e4f00298c735d1a01f98f73757 100644 --- a/packages/rocketchat-lib/server/startup/settings.js +++ b/packages/rocketchat-lib/server/startup/settings.js @@ -169,25 +169,7 @@ RocketChat.settings.addGroup('Accounts', function() { value: true } }); - this.add('Accounts_AvatarStoreType', 'GridFS', { - type: 'select', - values: [ - { - key: 'GridFS', - i18nLabel: 'GridFS' - }, { - key: 'FileSystem', - i18nLabel: 'FileSystem' - } - ] - }); - this.add('Accounts_AvatarStorePath', '', { - type: 'string', - enableQuery: { - _id: 'Accounts_AvatarStoreType', - value: 'FileSystem' - } - }); + return this.add('Accounts_SetDefaultAvatar', true, { type: 'boolean' }); @@ -467,13 +449,13 @@ RocketChat.settings.addGroup('General', function() { RocketChat.settings.addGroup('Email', function() { this.section('Header_and_Footer', function() { - this.add('Email_Header', '

[Site_Name]

', { + this.add('Email_Header', '

[Site_Name]

', { type: 'code', code: 'text/html', multiline: true, i18nLabel: 'Header' }); - return this.add('Email_Footer', '
Powered by Rocket.Chat
', { + return this.add('Email_Footer', '
Powered by Rocket.Chat
', { type: 'code', code: 'text/html', multiline: true, @@ -505,6 +487,15 @@ RocketChat.settings.addGroup('Email', function() { env: true, i18nLabel: 'Port' }); + this.add('SMTP_IgnoreTLS', false, { + type: 'boolean', + env: true, + i18nLabel: 'IgnoreTLS', + enableQuery: { + _id: 'SMTP_Protocol', + value: 'smtp' + } + }); this.add('SMTP_Pool', true, { type: 'boolean', env: true, diff --git a/packages/rocketchat-lib/server/startup/settingsOnLoadSMTP.js b/packages/rocketchat-lib/server/startup/settingsOnLoadSMTP.js index 8a3601061aa628763199130f1c8761aea2a0def4..70b8d4f7d2533e3d9a3596edd78af94362be8885 100644 --- a/packages/rocketchat-lib/server/startup/settingsOnLoadSMTP.js +++ b/packages/rocketchat-lib/server/startup/settingsOnLoadSMTP.js @@ -16,6 +16,10 @@ const buildMailURL = _.debounce(function() { process.env.MAIL_URL += `?pool=${ RocketChat.settings.get('SMTP_Pool') }`; + if (RocketChat.settings.get('SMTP_Protocol') === 'smtp' && RocketChat.settings.get('SMTP_IgnoreTLS')) { + process.env.MAIL_URL += '&secure=false&ignoreTLS=true'; + } + return process.env.MAIL_URL; } }, 500); @@ -50,6 +54,10 @@ RocketChat.settings.onload('SMTP_Pool', function() { return buildMailURL(); }); +RocketChat.settings.onload('SMTP_IgnoreTLS', 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 40fe79c9daf12417d55d6102592f5fc557f7f05b..a206f6386fdc983436987f972b1c6ede261eb1fa 100644 --- a/packages/rocketchat-lib/startup/defaultRoomTypes.js +++ b/packages/rocketchat-lib/startup/defaultRoomTypes.js @@ -1,12 +1,12 @@ /* globals openRoom */ RocketChat.roomTypes.add(null, 0, { - template: 'starredRooms', - icon: 'icon-star' + icon: 'icon-star', + label: 'Favorites' }); RocketChat.roomTypes.add('c', 10, { - template: 'channels', icon: 'icon-hash', + label: 'Channels', route: { name: 'channel', path: '/channel/:name', @@ -37,8 +37,8 @@ RocketChat.roomTypes.add('c', 10, { }); RocketChat.roomTypes.add('d', 20, { - template: 'directMessages', icon: 'icon-at', + label: 'Direct_Messages', route: { name: 'direct', path: '/direct/:username', @@ -94,8 +94,8 @@ RocketChat.roomTypes.add('d', 20, { }); RocketChat.roomTypes.add('p', 30, { - template: 'privateGroups', icon: 'icon-lock', + label: 'Private_Groups', route: { name: 'group', path: '/group/:name', diff --git a/packages/rocketchat-lib/tests/jasmine/server/unit/models/_Base.spec.coffee b/packages/rocketchat-lib/tests/jasmine/server/unit/models/_Base.spec.coffee deleted file mode 100644 index 8ff995ea49505f2642ebb543caa208a4abfee23c..0000000000000000000000000000000000000000 --- a/packages/rocketchat-lib/tests/jasmine/server/unit/models/_Base.spec.coffee +++ /dev/null @@ -1,24 +0,0 @@ -describe 'rocketchat:lib Server | Models | Base', -> - - beforeEach -> - MeteorStubs.install() - this.obj = new RocketChat.models._Base - - afterEach -> - MeteorStubs.uninstall() - - it 'should exist', -> - expect(this.obj).toBeDefined() - - it 'should provide a basename for collections', -> - expect(typeof this.obj._baseName()).toBe('string') - - it 'should carry a Mongo.Collection object when initialized', -> - expect(this.obj.model).toBeFalsy() - expect(this.obj._initModel('carry')).toBeTruthy() - expect(typeof this.obj.model).toBe('object') - - it 'should apply a basename to the Mongo.Collection created', -> - name = 'apply' - expect(this.obj._initModel(name)).toBeTruthy() - expect(this.obj.model._name).toBe(this.obj._baseName() + name) diff --git a/packages/rocketchat-livechat/app/.meteor/packages b/packages/rocketchat-livechat/app/.meteor/packages index e777a77fc027b856e0c3128f4b8034b3089d4457..b9e32033860a889e457a9f7a8d6688bd6c875d66 100644 --- a/packages/rocketchat-livechat/app/.meteor/packages +++ b/packages/rocketchat-livechat/app/.meteor/packages @@ -22,7 +22,6 @@ underscore@1.0.10 jquery@1.11.10 random@1.0.10 ejson@1.0.13 -coffeescript rocketchat:streamer kadira:flow-router kadira:blaze-layout diff --git a/packages/rocketchat-livechat/app/client/lib/_visitor.coffee b/packages/rocketchat-livechat/app/client/lib/_visitor.coffee deleted file mode 100644 index 9b93ddba257dd0fb6901188a0dd56f86bb701255..0000000000000000000000000000000000000000 --- a/packages/rocketchat-livechat/app/client/lib/_visitor.coffee +++ /dev/null @@ -1,57 +0,0 @@ -msgStream = new Meteor.Streamer 'room-messages' - -@visitor = new class - token = new ReactiveVar null - room = new ReactiveVar null - roomToSubscribe = new ReactiveVar null - roomSubscribed = null - - register = -> - if not localStorage.getItem 'visitorToken' - localStorage.setItem 'visitorToken', Random.id() - - token.set localStorage.getItem 'visitorToken' - - getToken = -> - return token.get() - - setRoom = (rid) -> - room.set rid - - getRoom = (createOnEmpty = false) -> - roomId = room.get() - if not roomId? and createOnEmpty - roomId = Random.id() - room.set roomId - - return roomId - - isSubscribed = (roomId) -> - return roomSubscribed is roomId - - subscribeToRoom = (roomId) -> - if roomSubscribed? - return if roomSubscribed is roomId - - roomSubscribed = roomId - - msgStream.on roomId, (msg) -> - if msg.t is 'command' - Commands[msg.msg]?() - else if msg.t isnt 'livechat_video_call' - ChatMessage.upsert { _id: msg._id }, msg - - if msg.t is 'livechat-close' - parentCall('callback', 'chat-ended') - - # notification sound - if Session.equals('sound', true) - if msg.u._id isnt Meteor.user()._id - $('#chatAudioNotification')[0].play(); - - register: register - getToken: getToken - setRoom: setRoom - getRoom: getRoom - subscribeToRoom: subscribeToRoom - isSubscribed: isSubscribed diff --git a/packages/rocketchat-livechat/app/client/lib/_visitor.js b/packages/rocketchat-livechat/app/client/lib/_visitor.js new file mode 100644 index 0000000000000000000000000000000000000000..21080388f65282766cf240c0b4a0564f87110e46 --- /dev/null +++ b/packages/rocketchat-livechat/app/client/lib/_visitor.js @@ -0,0 +1,66 @@ +/* globals Commands */ +const msgStream = new Meteor.Streamer('room-messages'); + +this.visitor = new class { + constructor() { + this.token = new ReactiveVar(null); + this.room = new ReactiveVar(null); + this.roomToSubscribe = new ReactiveVar(null); + this.roomSubscribed = null; + } + + register() { + if (!localStorage.getItem('visitorToken')) { + localStorage.setItem('visitorToken', Random.id()); + } + + this.token.set(localStorage.getItem('visitorToken')); + } + + getToken() { + return this.token.get(); + } + + setRoom(rid) { + this.room.set(rid); + } + + getRoom(createOnEmpty = false) { + let roomId = this.room.get(); + if (!roomId && createOnEmpty) { + roomId = Random.id(); + this.room.set(roomId); + } + + return roomId; + } + + isSubscribed(roomId) { + return this.roomSubscribed === roomId; + } + + subscribeToRoom(roomId) { + if (this.roomSubscribed && this.roomSubscribed === roomId) { + return; + } + + this.roomSubscribed = roomId; + + msgStream.on(roomId, (msg) => { + if (msg.t === 'command') { + Commands[msg.msg] && Commands[msg.msg](); + } else if (msg.t !== 'livechat_video_call') { + ChatMessage.upsert({ _id: msg._id }, msg); + + if (msg.t === 'livechat-close') { + parentCall('callback', 'chat-ended'); + } + + // notification sound + if (Session.equals('sound', true) && msg.u._id !== Meteor.userId()) { + $('#chatAudioNotification')[0].play(); + } + } + }); + } +}; diff --git a/packages/rocketchat-livechat/app/client/lib/chatMessages.coffee b/packages/rocketchat-livechat/app/client/lib/chatMessages.coffee deleted file mode 100644 index 271a7caf5f51ab311a480a0b39d163c4e31bf1dd..0000000000000000000000000000000000000000 --- a/packages/rocketchat-livechat/app/client/lib/chatMessages.coffee +++ /dev/null @@ -1,220 +0,0 @@ -import toastr from 'toastr' -class @ChatMessages - init: (node) -> - this.editing = {} - - # this.messageMaxSize = RocketChat.settings.get('Message_MaxAllowedSize') - this.wrapper = $(node).find(".wrapper") - this.input = $(node).find(".input-message").get(0) - # this.bindEvents() - return - - resize: -> - dif = 60 + $(".messages-container").find("footer").outerHeight() - $(".messages-box").css - height: "calc(100% - #{dif}px)" - - toPrevMessage: -> - msgs = this.wrapper.get(0).querySelectorAll(".own:not(.system)") - if msgs.length - if this.editing.element - if msgs[this.editing.index - 1] - this.edit msgs[this.editing.index - 1], this.editing.index - 1 - else - this.edit msgs[msgs.length - 1], msgs.length - 1 - - toNextMessage: -> - if this.editing.element - msgs = this.wrapper.get(0).querySelectorAll(".own:not(.system)") - if msgs[this.editing.index + 1] - this.edit msgs[this.editing.index + 1], this.editing.index + 1 - else - this.clearEditing() - - getEditingIndex: (element) -> - msgs = this.wrapper.get(0).querySelectorAll(".own:not(.system)") - index = 0 - for msg in msgs - if msg is element - return index - index++ - return -1 - - edit: (element, index) -> - return if element.classList.contains("system") - this.clearEditing() - id = element.getAttribute("id") - message = ChatMessage.findOne { _id: id, 'u._id': Meteor.userId() } - this.input.value = message.msg - this.editing.element = element - this.editing.index = index or this.getEditingIndex(element) - this.editing.id = id - element.classList.add("editing") - this.input.classList.add("editing") - setTimeout => - this.input.focus() - , 5 - - clearEditing: -> - if this.editing.element - this.editing.element.classList.remove("editing") - this.input.classList.remove("editing") - this.editing.id = null - this.editing.element = null - this.editing.index = null - this.input.value = this.editing.saved or "" - else - this.editing.saved = this.input.value - - send: (rid, input) -> - if s.trim(input.value) isnt '' - if this.isMessageTooLong(input) - return toastr.error t('Message_too_long') - # KonchatNotification.removeRoomNotification(rid) - msg = input.value - input.value = '' - rid ?= visitor.getRoom(true) - - sendMessage = (callback) -> - msgObject = { - _id: Random.id(), - rid: rid, - msg: msg, - token: visitor.getToken() - } - MsgTyping.stop(rid) - - Meteor.call 'sendMessageLivechat', msgObject, (error, result) -> - if error - ChatMessage.update msgObject._id, { $set: { error: true } } - showError error.reason - - if result?.rid? and not visitor.isSubscribed(result.rid) - Livechat.connecting = result.showConnecting - ChatMessage.update result._id, _.omit(result, '_id') - Livechat.room = result.rid - - parentCall('callback', 'chat-started'); - - if not Meteor.userId() - guest = { - token: visitor.getToken() - } - - if Livechat.department - guest.department = Livechat.department - - Meteor.call 'livechat:registerGuest', guest, (error, result) -> - if error? - return showError error.reason - - Meteor.loginWithToken result.token, (error) -> - if error - return showError error.reason - - sendMessage() - else - sendMessage() - - deleteMsg: (message) -> - Meteor.call 'deleteMessage', message, (error, result) -> - if error - return handleError(error) - - update: (id, rid, input) -> - if s.trim(input.value) isnt '' - msg = input.value - Meteor.call 'updateMessage', { id: id, msg: msg } - this.clearEditing() - MsgTyping.stop(rid) - - startTyping: (rid, input) -> - if s.trim(input.value) isnt '' - MsgTyping.start(rid) - else - MsgTyping.stop(rid) - - bindEvents: -> - if this.wrapper?.length - $(".input-message").autogrow - postGrowCallback: => - this.resize() - - tryCompletion: (input) -> - value = input.value.match(/[^\s]+$/) - if value?.length > 0 - value = value[0] - - re = new RegExp value, 'i' - - user = Meteor.users.findOne username: re - if user? - input.value = input.value.replace value, "@#{user.username} " - - keyup: (rid, event) -> - input = event.currentTarget - k = event.which - keyCodes = [ - 13, # Enter - 20, # Caps lock - 16, # Shift - 9, # Tab - 27, # Escape Key - 17, # Control Key - 91, # Windows Command Key - 19, # Pause Break - 18, # Alt Key - 93, # Right Click Point Key - 45, # Insert Key - 34, # Page Down - 35, # Page Up - 144, # Num Lock - 145 # Scroll Lock - ] - keyCodes.push i for i in [35..40] # Home, End, Arrow Keys - keyCodes.push i for i in [112..123] # F1 - F12 - - unless k in keyCodes - this.startTyping(rid, input) - - keydown: (rid, event) -> - input = event.currentTarget - k = event.which - this.resize(input) - if k is 13 and not event.shiftKey and not event.ctrlKey and not event.altKey # Enter without shift/ctrl/alt - event.preventDefault() - event.stopPropagation() - if this.editing.id - this.update(this.editing.id, rid, input) - else - this.send(rid, input) - return - - if k is 9 - event.preventDefault() - event.stopPropagation() - @tryCompletion input - - if k is 27 - if this.editing.id - event.preventDefault() - event.stopPropagation() - this.clearEditing() - return - # else if k is 38 or k is 40 # Arrow Up or down - # if k is 38 - # return if input.value.slice(0, input.selectionStart).match(/[\n]/) isnt null - # this.toPrevMessage() - # else - # return if input.value.slice(input.selectionEnd, input.value.length).match(/[\n]/) isnt null - # this.toNextMessage() - - # event.preventDefault() - # event.stopPropagation() - - # ctrl (command) + shift + k -> clear room messages - else if k is 75 and ((navigator?.platform?.indexOf('Mac') isnt -1 and event.metaKey and event.shiftKey) or (navigator?.platform?.indexOf('Mac') is -1 and event.ctrlKey and event.shiftKey)) - RoomHistoryManager.clear rid - - isMessageTooLong: (input) -> - input?.value.length > this.messageMaxSize diff --git a/packages/rocketchat-livechat/app/client/lib/chatMessages.js b/packages/rocketchat-livechat/app/client/lib/chatMessages.js new file mode 100644 index 0000000000000000000000000000000000000000..2649809fd2861c88d6cb7aec672fd272d744ab52 --- /dev/null +++ b/packages/rocketchat-livechat/app/client/lib/chatMessages.js @@ -0,0 +1,281 @@ +/* globals MsgTyping, showError, Livechat */ +import toastr from 'toastr'; + +this.ChatMessages = class ChatMessages { + init(node) { + this.editing = {}; + + // this.messageMaxSize = RocketChat.settings.get('Message_MaxAllowedSize') + this.wrapper = $(node).find('.wrapper'); + this.input = $(node).find('.input-message').get(0); + // this.bindEvents() + return; + } + + resize() { + const dif = 60 + $('.messages-container').find('footer').outerHeight(); + return $('.messages-box').css({ + height: `calc(100% - ${ dif }px)` + }); + } + + toPrevMessage() { + const msgs = this.wrapper.get(0).querySelectorAll('.own:not(.system)'); + if (msgs.length) { + if (this.editing.element) { + if (msgs[this.editing.index - 1]) { + this.edit(msgs[this.editing.index - 1], this.editing.index - 1); + } + } else { + this.edit(msgs[msgs.length - 1], msgs.length - 1); + } + } + } + + toNextMessage() { + if (this.editing.element) { + const msgs = this.wrapper.get(0).querySelectorAll('.own:not(.system)'); + if (msgs[this.editing.index + 1]) { + this.edit(msgs[this.editing.index + 1], this.editing.index + 1); + } else { + this.clearEditing(); + } + } + } + + getEditingIndex(element) { + const msgs = this.wrapper.get(0).querySelectorAll('.own:not(.system)'); + let index = 0; + for (const msg of Array.from(msgs)) { + if (msg === element) { + return index; + } + index++; + } + return -1; + } + + edit(element, index) { + if (element.classList.contains('system')) { + return; + } + this.clearEditing(); + const id = element.getAttribute('id'); + const message = ChatMessage.findOne({ _id: id, 'u._id': Meteor.userId() }); + this.input.value = message.msg; + this.editing.element = element; + this.editing.index = index || this.getEditingIndex(element); + this.editing.id = id; + element.classList.add('editing'); + this.input.classList.add('editing'); + setTimeout(() => { + this.input.focus(); + }, 5); + } + + clearEditing() { + if (this.editing.element) { + this.editing.element.classList.remove('editing'); + this.input.classList.remove('editing'); + this.editing.id = null; + this.editing.element = null; + this.editing.index = null; + this.input.value = this.editing.saved || ''; + } else { + this.editing.saved = this.input.value; + } + } + + send(rid, input) { + if (s.trim(input.value) === '') { + return; + } + if (this.isMessageTooLong(input)) { + return toastr.error(t('Message_too_long')); + } + // KonchatNotification.removeRoomNotification(rid) + const msg = input.value; + input.value = ''; + if (!rid) { + rid = visitor.getRoom(true); + } + + const sendMessage = () => { + const msgObject = { + _id: Random.id(), + rid, + msg, + token: visitor.getToken() + }; + MsgTyping.stop(rid); + + Meteor.call('sendMessageLivechat', msgObject, (error, result) => { + if (error) { + ChatMessage.update(msgObject._id, { $set: { error: true } }); + showError(error.reason); + } + + if (result && result.rid && !visitor.isSubscribed(result.rid)) { + Livechat.connecting = result.showConnecting; + ChatMessage.update(result._id, _.omit(result, '_id')); + Livechat.room = result.rid; + + parentCall('callback', 'chat-started'); + } + }); + }; + + if (!Meteor.userId()) { + const guest = { + token: visitor.getToken() + }; + + if (Livechat.department) { + guest.department = Livechat.department; + } + + Meteor.call('livechat:registerGuest', guest, (error, result) => { + if (error) { + return showError(error.reason); + } + + Meteor.loginWithToken(result.token, (error) => { + if (error) { + return showError(error.reason); + } + + sendMessage(); + }); + }); + } else { + sendMessage(); + } + } + + deleteMsg(message) { + Meteor.call('deleteMessage', message, (error) => { + if (error) { + return handleError(error); + } + }); + } + + update(id, rid, input) { + if (s.trim(input.value) !== '') { + const msg = input.value; + Meteor.call('updateMessage', { id, msg }); + this.clearEditing(); + MsgTyping.stop(rid); + } + } + + startTyping(rid, input) { + if (s.trim(input.value) !== '') { + MsgTyping.start(rid); + } else { + MsgTyping.stop(rid); + } + } + + bindEvents() { + if (this.wrapper && this.wrapper.length) { + $('.input-message').autogrow({ + postGrowCallback: () => { + this.resize(); + } + }); + } + } + + tryCompletion(input) { + let value = input.value.match(/[^\s]+$/); + if (value && value.length > 0) { + value = value[0]; + + const re = new RegExp(value, 'i'); + + const user = Meteor.users.findOne({ username: re }, { fields: { username: 1 } }); + if (user) { + input.value = input.value.replace(value, `@${ user.username } `); + } + } + } + + keyup(rid, event) { + let i; + const input = event.currentTarget; + const k = event.which; + const keyCodes = [ + 13, // Enter + 20, // Caps lock + 16, // Shift + 9, // Tab + 27, // Escape Key + 17, // Control Key + 91, // Windows Command Key + 19, // Pause Break + 18, // Alt Key + 93, // Right Click Point Key + 45, // Insert Key + 34, // Page Down + 35, // Page Up + 144, // Num Lock + 145 // Scroll Lock + ]; + for (i = 35; i <= 40; i++) { keyCodes.push(i); } // Home, End, Arrow Keys + for (i = 112; i <= 123; i++) { keyCodes.push(i); } // F1 - F12 + + if (!Array.from(keyCodes).includes(k)) { + this.startTyping(rid, input); + } + } + + keydown(rid, event) { + const input = event.currentTarget; + const k = event.which; + this.resize(input); + if (k === 13 && !event.shiftKey && !event.ctrlKey && !event.altKey) { // Enter without shift/ctrl/alt + event.preventDefault(); + event.stopPropagation(); + if (this.editing.id) { + this.update(this.editing.id, rid, input); + } else { + this.send(rid, input); + } + return; + } + + if (k === 9) { + event.preventDefault(); + event.stopPropagation(); + this.tryCompletion(input); + } + + if (k === 27) { + if (this.editing.id) { + event.preventDefault(); + event.stopPropagation(); + this.clearEditing(); + return; + } + // else if k is 38 or k is 40 # Arrow Up or down + // if k is 38 + // return if input.value.slice(0, input.selectionStart).match(/[\n]/) isnt null + // this.toPrevMessage() + // else + // return if input.value.slice(input.selectionEnd, input.value.length).match(/[\n]/) isnt null + // this.toNextMessage() + + // event.preventDefault() + // event.stopPropagation() + + // ctrl (command) + shift + k -> clear room messages + } else if (k === 75 && ((navigator.platform.indexOf('Mac') !== -1 && event.metaKey && event.shiftKey) || (navigator.platform.indexOf('Mac') === -1 && event.ctrlKey && event.shiftKey))) { + RoomHistoryManager.clear(rid); + } + } + + isMessageTooLong(input) { + return input && input.value.length > this.messageMaxSize; + } +}; diff --git a/packages/rocketchat-livechat/app/client/lib/collections.coffee b/packages/rocketchat-livechat/app/client/lib/collections.coffee deleted file mode 100644 index 12de3445db87d7dab5613fab421e273fcd424204..0000000000000000000000000000000000000000 --- a/packages/rocketchat-livechat/app/client/lib/collections.coffee +++ /dev/null @@ -1,2 +0,0 @@ -@ChatMessage = new Mongo.Collection null -@Department = new Mongo.Collection null diff --git a/packages/rocketchat-livechat/app/client/lib/collections.js b/packages/rocketchat-livechat/app/client/lib/collections.js new file mode 100644 index 0000000000000000000000000000000000000000..1feb9caace84cc3ba510f1398a2af35451bc1119 --- /dev/null +++ b/packages/rocketchat-livechat/app/client/lib/collections.js @@ -0,0 +1,2 @@ +this.ChatMessage = new Mongo.Collection(null); +this.Department = new Mongo.Collection(null); diff --git a/packages/rocketchat-livechat/app/client/lib/error.coffee b/packages/rocketchat-livechat/app/client/lib/error.coffee deleted file mode 100644 index 0b3058978b731e17bedeb42c936cb6e27321a38a..0000000000000000000000000000000000000000 --- a/packages/rocketchat-livechat/app/client/lib/error.coffee +++ /dev/null @@ -1,2 +0,0 @@ -@showError = (msg) -> - $('.error').addClass('show').find('span').html(msg) diff --git a/packages/rocketchat-livechat/app/client/lib/error.js b/packages/rocketchat-livechat/app/client/lib/error.js new file mode 100644 index 0000000000000000000000000000000000000000..7fd5501fc2a10d7d9a20ad97e0e8dcc620824337 --- /dev/null +++ b/packages/rocketchat-livechat/app/client/lib/error.js @@ -0,0 +1,3 @@ +this.showError = msg => { + $('.error').addClass('show').find('span').html(msg); +}; diff --git a/packages/rocketchat-livechat/app/client/lib/fromApp/Notifications.coffee b/packages/rocketchat-livechat/app/client/lib/fromApp/Notifications.coffee deleted file mode 100644 index 9fd588378e23cb1f64d947a378ad812c7a79f4c1..0000000000000000000000000000000000000000 --- a/packages/rocketchat-livechat/app/client/lib/fromApp/Notifications.coffee +++ /dev/null @@ -1,45 +0,0 @@ -@Notifications = new class - constructor: -> - @debug = false - @streamAll = new Meteor.Streamer 'notify-all' - @streamRoom = new Meteor.Streamer 'notify-room' - @streamUser = new Meteor.Streamer 'notify-user' - - if @debug is true - @onAll -> console.log "RocketChat.Notifications: onAll", arguments - @onUser -> console.log "RocketChat.Notifications: onAll", arguments - - - 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 - - onAll: (eventName, callback) -> - @streamAll.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 - - unRoom: (room, eventName, callback) -> - @streamRoom.removeListener "#{room}/#{eventName}", callback - - unUser: (callback) -> - @streamUser.removeListener Meteor.userId(), callback diff --git a/packages/rocketchat-livechat/app/client/lib/fromApp/Notifications.js b/packages/rocketchat-livechat/app/client/lib/fromApp/Notifications.js new file mode 100644 index 0000000000000000000000000000000000000000..d337054869954e16db56f90190b3039e2c5b7ac5 --- /dev/null +++ b/packages/rocketchat-livechat/app/client/lib/fromApp/Notifications.js @@ -0,0 +1,79 @@ +this.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); + } + 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-livechat/app/client/lib/fromApp/RoomHistoryManager.coffee b/packages/rocketchat-livechat/app/client/lib/fromApp/RoomHistoryManager.coffee deleted file mode 100644 index d21a2a296144c0c111377321e28972a2e5a63c67..0000000000000000000000000000000000000000 --- a/packages/rocketchat-livechat/app/client/lib/fromApp/RoomHistoryManager.coffee +++ /dev/null @@ -1,70 +0,0 @@ -@RoomHistoryManager = new class - defaultLimit = 50 - - histories = {} - - getRoom = (rid) -> - if not histories[rid]? - histories[rid] = - hasMore: ReactiveVar true - isLoading: ReactiveVar false - loaded: 0 - - return histories[rid] - - getMore = (rid, limit=defaultLimit) -> - room = getRoom rid - if room.hasMore.curValue isnt true - return - - room.isLoading.set true - - #$('.messages-box .wrapper').data('previous-height', $('.messages-box .wrapper').get(0)?.scrollHeight - $('.messages-box .wrapper').get(0)?.scrollTop) - # ScrollListener.setLoader true - lastMessage = ChatMessage.findOne({rid: rid}, {sort: {ts: 1}}) - # lastMessage ?= ChatMessage.findOne({rid: rid}, {sort: {ts: 1}}) - - if lastMessage? - ts = lastMessage.ts - else - ts = new Date - - Meteor.call 'loadHistory', rid, ts, limit, undefined, (err, result) -> - return if err? - - for item in result?.messages or [] - if item.t isnt 'command' - ChatMessage.upsert {_id: item._id}, item - room.isLoading.set false - room.loaded += result.messages.length - if result.messages.length < limit - room.hasMore.set false - - hasMore = (rid) -> - room = getRoom rid - - return room.hasMore.get() - - getMoreIfIsEmpty = (rid) -> - room = getRoom rid - - if room.loaded is 0 - getMore rid - - isLoading = (rid) -> - room = getRoom rid - - return room.isLoading.get() - - clear = (rid) -> - ChatMessage.remove({ rid: rid }) - if histories[rid]? - histories[rid].hasMore.set true - histories[rid].isLoading.set false - histories[rid].loaded = 0 - - getMore: getMore - getMoreIfIsEmpty: getMoreIfIsEmpty - hasMore: hasMore - isLoading: isLoading - clear: clear diff --git a/packages/rocketchat-livechat/app/client/lib/fromApp/RoomHistoryManager.js b/packages/rocketchat-livechat/app/client/lib/fromApp/RoomHistoryManager.js new file mode 100644 index 0000000000000000000000000000000000000000..2f94b4d1c5b49b5bb51a785d96193dbc93aeddc9 --- /dev/null +++ b/packages/rocketchat-livechat/app/client/lib/fromApp/RoomHistoryManager.js @@ -0,0 +1,232 @@ +/* globals readMessage UserRoles RoomRoles*/ +export const RoomHistoryManager = new class { + constructor() { + this.defaultLimit = 50; + this.histories = {}; + } + getRoom(rid) { + if ((this.histories[rid] == null)) { + this.histories[rid] = { + hasMore: new ReactiveVar(true), + hasMoreNext: new ReactiveVar(false), + isLoading: new ReactiveVar(false), + unreadNotLoaded: new ReactiveVar(0), + firstUnread: new ReactiveVar, + loaded: undefined + }; + } + + return this.histories[rid]; + } + + getMore(rid, limit) { + if (limit == null) { limit = this.defaultLimit; } + const room = this.getRoom(rid); + if (room.hasMore.curValue !== true) { + return; + } + + room.isLoading.set(true); + + //$('.messages-box .wrapper').data('previous-height', $('.messages-box .wrapper').get(0)?.scrollHeight - $('.messages-box .wrapper').get(0)?.scrollTop) + // ScrollListener.setLoader true + const lastMessage = ChatMessage.findOne({rid}, { fields: { ts: 1 }, sort: { ts: 1 }}); + // lastMessage ?= ChatMessage.findOne({rid: rid}, {sort: {ts: 1}}) + + let ts; + if (lastMessage) { + ts = lastMessage.ts; + } else { + ts = new Date(); + } + + Meteor.call('loadHistory', rid, ts, limit, undefined, (err, result) => { + if (err) { + return; + } + + if (result && result.messages) { + result.messages.forEach((item) => { + if (item.t !== 'command') { + ChatMessage.upsert({_id: item._id}, item); + } + }); + room.isLoading.set(false); + room.loaded += result.messages.length; + if (result.messages.length < limit) { + room.hasMore.set(false); + } + } + }); + } + + getMoreNext(rid, limit) { + if (limit == null) { limit = this.defaultLimit; } + const room = this.getRoom(rid); + if (room.hasMoreNext.curValue !== true) { + return; + } + + const instance = Blaze.getView($('.messages-box .wrapper')[0]).templateInstance(); + instance.atBottom = false; + + room.isLoading.set(true); + + const lastMessage = ChatMessage.findOne({rid}, {sort: {ts: -1}}); + + let typeName = undefined; + + const subscription = ChatSubscription.findOne({rid}); + if (subscription != null) { + // const { ls } = subscription; + typeName = subscription.t + subscription.name; + } else { + const curRoomDoc = ChatRoom.findOne({_id: rid}); + typeName = (curRoomDoc != null ? curRoomDoc.t : undefined) + (curRoomDoc != null ? curRoomDoc.name : undefined); + } + + const { ts } = lastMessage; + + if (ts) { + return Meteor.call('loadNextMessages', rid, ts, limit, function(err, result) { + for (const item of Array.from((result != null ? result.messages : undefined) || [])) { + if (item.t !== 'command') { + const roles = [ + (item.u && item.u._id && UserRoles.findOne(item.u._id, { fields: { roles: 1 }})) || {}, + (item.u && item.u._id && RoomRoles.findOne({rid: item.rid, 'u._id': item.u._id})) || {} + ].map(e => e.roles); + item.roles = _.union.apply(_.union, roles); + ChatMessage.upsert({_id: item._id}, item); + } + } + + Meteor.defer(() => RoomManager.updateMentionsMarksOfRoom(typeName)); + + room.isLoading.set(false); + if (room.loaded == null) { room.loaded = 0; } + + room.loaded += result.messages.length; + if (result.messages.length < limit) { + room.hasMoreNext.set(false); + } + }); + } + } + + getSurroundingMessages(message, limit) { + if (limit == null) { limit = this.defaultLimit; } + if (!(message != null ? message.rid : undefined)) { + return; + } + + const instance = Blaze.getView($('.messages-box .wrapper')[0]).templateInstance(); + + if (ChatMessage.findOne(message._id)) { + const wrapper = $('.messages-box .wrapper'); + const msgElement = $(`#${ message._id }`, wrapper); + const pos = (wrapper.scrollTop() + msgElement.offset().top) - (wrapper.height()/2); + wrapper.animate({ + scrollTop: pos + }, 500); + msgElement.addClass('highlight'); + + setTimeout(function() { + const messages = wrapper[0]; + return instance.atBottom = messages.scrollTop >= (messages.scrollHeight - messages.clientHeight); + }); + + return setTimeout(() => msgElement.removeClass('highlight') + , 500); + } else { + const room = this.getRoom(message.rid); + room.isLoading.set(true); + ChatMessage.remove({ rid: message.rid }); + + let typeName = undefined; + + const subscription = ChatSubscription.findOne({rid: message.rid}); + if (subscription) { + // const { ls } = subscription; + typeName = subscription.t + subscription.name; + } else { + const curRoomDoc = ChatRoom.findOne({_id: message.rid}); + typeName = (curRoomDoc != null ? curRoomDoc.t : undefined) + (curRoomDoc != null ? curRoomDoc.name : undefined); + } + + return Meteor.call('loadSurroundingMessages', message, limit, function(err, result) { + for (const item of Array.from((result != null ? result.messages : undefined) || [])) { + if (item.t !== 'command') { + const roles = [ + (item.u && item.u._id && UserRoles.findOne(item.u._id, { fields: { roles: 1 }})) || {}, + (item.u && item.u._id && RoomRoles.findOne({rid: item.rid, 'u._id': item.u._id})) || {} + ].map(e => e.roles); + item.roles = _.union.apply(_.union, roles); + ChatMessage.upsert({_id: item._id}, item); + } + } + + Meteor.defer(function() { + readMessage.refreshUnreadMark(message.rid, true); + RoomManager.updateMentionsMarksOfRoom(typeName); + const wrapper = $('.messages-box .wrapper'); + const msgElement = $(`#${ message._id }`, wrapper); + const pos = (wrapper.scrollTop() + msgElement.offset().top) - (wrapper.height()/2); + wrapper.animate({ + scrollTop: pos + }, 500); + + msgElement.addClass('highlight'); + + setTimeout(function() { + room.isLoading.set(false); + const messages = wrapper[0]; + instance.atBottom = !result.moreAfter && (messages.scrollTop >= (messages.scrollHeight - messages.clientHeight)); + return 500; + }); + + return setTimeout(() => msgElement.removeClass('highlight') + , 500); + }); + if (room.loaded == null) { room.loaded = 0; } + room.loaded += result.messages.length; + room.hasMore.set(result.moreBefore); + return room.hasMoreNext.set(result.moreAfter); + }); + } + } + + hasMore(rid) { + const room = this.getRoom(rid); + return room.hasMore.get(); + } + + hasMoreNext(rid) { + const room = this.getRoom(rid); + return room.hasMoreNext.get(); + } + + + getMoreIfIsEmpty(rid) { + const room = this.getRoom(rid); + + if (room.loaded === undefined) { + return this.getMore(rid); + } + } + + + isLoading(rid) { + const room = this.getRoom(rid); + return room.isLoading.get(); + } + + clear(rid) { + ChatMessage.remove({ rid }); + if (this.histories[rid] != null) { + this.histories[rid].hasMore.set(true); + this.histories[rid].isLoading.set(false); + return this.histories[rid].loaded = undefined; + } + } +}; +this.RoomHistoryManager = RoomHistoryManager; diff --git a/packages/rocketchat-livechat/app/client/lib/fromApp/avatar.coffee b/packages/rocketchat-livechat/app/client/lib/fromApp/avatar.coffee deleted file mode 100644 index 7988062501b20fad333e24edc3e7d559e6ca1382..0000000000000000000000000000000000000000 --- a/packages/rocketchat-livechat/app/client/lib/fromApp/avatar.coffee +++ /dev/null @@ -1,18 +0,0 @@ -@getAvatarUrlFromUsername = (username) -> - key = "avatar_random_#{username}" - random = Session.keys[key] or 0 - if not username? - return - - return "#{Meteor.absoluteUrl()}avatar/#{username}.jpg?_dc=#{random}" - -@updateAvatarOfUsername = (username) -> - key = "avatar_random_#{username}" - Session.set key, Math.round(Math.random() * 1000) - - for key, room of RoomManager.openedRooms - url = getAvatarUrlFromUsername username - - $(room.dom).find(".message[data-username='#{username}'] .avatar-image").css('background-image', "url(#{url})"); - - return true diff --git a/packages/rocketchat-livechat/app/client/lib/fromApp/avatar.js b/packages/rocketchat-livechat/app/client/lib/fromApp/avatar.js new file mode 100644 index 0000000000000000000000000000000000000000..07aad70c42cae3cdd520b9c136c0634c0d5dddf4 --- /dev/null +++ b/packages/rocketchat-livechat/app/client/lib/fromApp/avatar.js @@ -0,0 +1,21 @@ +this.getAvatarUrlFromUsername = username => { + const key = `avatar_random_${ username }`; + const random = Session.keys[key] || 0; + if (!username) { + return; + } + + return `${ Meteor.absoluteUrl() }avatar/${ username }.jpg?_dc=${ random }`; +}; + +this.updateAvatarOfUsername = username => { + const key = `avatar_random_${ username }`; + Session.set(key, Math.round(Math.random() * 1000)); + + Object.keys(RoomManager.openedRooms).forEach((key) => { + const room = RoomManager.openedRooms[key]; + const url = getAvatarUrlFromUsername(username); + $(room.dom).find(`.message[data-username='${ username }'] .avatar-image`).css('background-image', `url(${ url })`); + }); + return true; +}; diff --git a/packages/rocketchat-livechat/app/client/lib/msgTyping.coffee b/packages/rocketchat-livechat/app/client/lib/msgTyping.coffee deleted file mode 100644 index ac6bd5b72269e1b54a7447411c6a219795eb1343..0000000000000000000000000000000000000000 --- a/packages/rocketchat-livechat/app/client/lib/msgTyping.coffee +++ /dev/null @@ -1,69 +0,0 @@ -@MsgTyping = do -> - timeout = 15000 - timeouts = {} - renew = true - renewTimeout = 10000 - selfTyping = new ReactiveVar false - usersTyping = {} - dep = new Tracker.Dependency - - addStream = (room) -> - if _.isEmpty usersTyping[room]?.users - usersTyping[room] = { users: {} } - Notifications.onRoom room, 'typing', (username, typing) -> - unless username is Meteor.user()?.username - if typing is true - users = usersTyping[room].users - users[username] = Meteor.setTimeout -> - delete users[username] - usersTyping[room].users = users - dep.changed() - , timeout - usersTyping[room].users = users - dep.changed() - else - users = usersTyping[room].users - delete users[username] - usersTyping[room].users = users - dep.changed() - - Tracker.autorun -> - if visitor.getRoom() and Meteor.userId() - addStream visitor.getRoom() - - start = (room) -> - return unless renew - - setTimeout -> - renew = true - , renewTimeout - - renew = false - selfTyping.set true - Notifications.notifyRoom room, 'typing', Meteor.user()?.username, true - clearTimeout timeouts[room] - timeouts[room] = Meteor.setTimeout -> - stop(room) - , timeout - - stop = (room) -> - renew = true - selfTyping.set false - if timeouts?[room]? - clearTimeout(timeouts[room]) - timeouts[room] = null - Notifications.notifyRoom room, 'typing', Meteor.user()?.username, false - - get = (room) -> - dep.depend() - unless usersTyping[room] - usersTyping[room] = { users: {} } - users = usersTyping[room].users - return _.keys(users) or [] - - return { - start: start - stop: stop - get: get - selfTyping: selfTyping - } diff --git a/packages/rocketchat-livechat/app/client/lib/msgTyping.js b/packages/rocketchat-livechat/app/client/lib/msgTyping.js new file mode 100644 index 0000000000000000000000000000000000000000..1d387f744e0665125860fb722ce6e976094e6cbf --- /dev/null +++ b/packages/rocketchat-livechat/app/client/lib/msgTyping.js @@ -0,0 +1,77 @@ +/* globals Notifications */ +export const MsgTyping = (function() { + const timeout = 15000; + const timeouts = {}; + let renew = true; + const renewTimeout = 10000; + const selfTyping = new ReactiveVar(false); + const usersTyping = {}; + const dep = new Tracker.Dependency; + + const addStream = function(room) { + if (!_.isEmpty(usersTyping[room] && usersTyping[room].users)) { + return; + } + usersTyping[room] = { users: {} }; + return Notifications.onRoom(room, 'typing', function(username, typing) { + const user = Meteor.user(); + if (username === (user && user.username)) { + return; + } + const { users } = usersTyping[room]; + if (typing === true) { + users[username] = Meteor.setTimeout(function() { + delete users[username]; + usersTyping[room].users = users; + return dep.changed(); + }, timeout); + } else { + delete users[username]; + } + usersTyping[room].users = users; + return dep.changed(); + }); + }; + + Tracker.autorun(() => { + if (visitor.getRoom() && Meteor.userId()) { + addStream(visitor.getRoom()); + } + }); + + const stop = function(room) { + renew = true; + selfTyping.set(false); + if (timeouts && timeouts[room]) { + clearTimeout(timeouts[room]); + timeouts[room] = null; + } + const user = Meteor.user(); + return Notifications.notifyRoom(room, 'typing', user && user.username, false); + }; + const start = function(room) { + if (!renew) { return; } + + setTimeout(() => renew = true, renewTimeout); + + renew = false; + selfTyping.set(true); + const user = Meteor.user(); + Notifications.notifyRoom(room, 'typing', user && user.username, true); + clearTimeout(timeouts[room]); + return timeouts[room] = Meteor.setTimeout(() => stop(room), timeout); + }; + + const get = function(room) { + dep.depend(); + if (!usersTyping[room]) { + usersTyping[room] = { users: {} }; + } + const { users } = usersTyping[room]; + return _.keys(users) || []; + }; + + return { start, stop, get, selfTyping }; +}()); + +this.MsgTyping = MsgTyping; diff --git a/packages/rocketchat-livechat/app/client/lib/parentCall.coffee b/packages/rocketchat-livechat/app/client/lib/parentCall.coffee deleted file mode 100644 index e09e1103c95f3422ca1d2cdad8d86c3b749d37ed..0000000000000000000000000000000000000000 --- a/packages/rocketchat-livechat/app/client/lib/parentCall.coffee +++ /dev/null @@ -1,7 +0,0 @@ -@parentCall = (method, args = []) -> - data = - src: 'rocketchat' - fn: method - args: args - - window.parent.postMessage data, '*' diff --git a/packages/rocketchat-livechat/app/client/lib/parentCall.js b/packages/rocketchat-livechat/app/client/lib/parentCall.js new file mode 100644 index 0000000000000000000000000000000000000000..ae705ec7618f38d3fd1c10d60adf8b29dd33b7fe --- /dev/null +++ b/packages/rocketchat-livechat/app/client/lib/parentCall.js @@ -0,0 +1,9 @@ +this.parentCall = (method, args = []) => { + const data = { + src: 'rocketchat', + fn: method, + args + }; + + window.parent.postMessage(data, '*'); +}; diff --git a/packages/rocketchat-livechat/app/client/lib/tapi18n.coffee b/packages/rocketchat-livechat/app/client/lib/tapi18n.coffee deleted file mode 100644 index 1cf705e3952ea946204756436130477c86f6fc33..0000000000000000000000000000000000000000 --- a/packages/rocketchat-livechat/app/client/lib/tapi18n.coffee +++ /dev/null @@ -1,15 +0,0 @@ -@t = (key, replaces...) -> - if _.isObject replaces[0] - return TAPi18n.__ key, replaces - else - return TAPi18n.__ key, { postProcess: 'sprintf', sprintf: replaces } - -@tr = (key, options, replaces...) -> - if _.isObject replaces[0] - return TAPi18n.__ key, options, replaces - else - return TAPi18n.__ key, options, { postProcess: 'sprintf', sprintf: replaces } - -@isRtl = (language) -> - # https://en.wikipedia.org/wiki/Right-to-left#cite_note-2 - return language?.split('-').shift().toLowerCase() in ['ar', 'dv', 'fa', 'he', 'ku', 'ps', 'sd', 'ug', 'ur', 'yi'] diff --git a/packages/rocketchat-livechat/app/client/lib/tapi18n.js b/packages/rocketchat-livechat/app/client/lib/tapi18n.js new file mode 100644 index 0000000000000000000000000000000000000000..2a3f6ae10d0b74b94b71e7c852b5dfbe464db224 --- /dev/null +++ b/packages/rocketchat-livechat/app/client/lib/tapi18n.js @@ -0,0 +1,23 @@ +this.t = function(key, ...replaces) { + if (_.isObject(replaces[0])) { + return TAPi18n.__(key, replaces); + } else { + return TAPi18n.__(key, { + postProcess: 'sprintf', + sprintf: replaces + }); + } +}; + +this.tr = function(key, options, ...replaces) { + if (_.isObject(replaces[0])) { + return TAPi18n.__(key, options, replaces); + } else { + return TAPi18n.__(key, options, { + postProcess: 'sprintf', + sprintf: replaces + }); + } +}; + +this.isRtl = (language) => language != null && ['ar', 'dv', 'fa', 'he', 'ku', 'ps', 'sd', 'ug', 'ur', 'yi'].includes(language.split('-').shift().toLowerCase()); diff --git a/packages/rocketchat-livechat/app/client/methods/sendMessageExternal.coffee b/packages/rocketchat-livechat/app/client/methods/sendMessageExternal.coffee deleted file mode 100644 index 24e493f8913ca6bd22d7bf4f9ae3c8dff5dc8738..0000000000000000000000000000000000000000 --- a/packages/rocketchat-livechat/app/client/methods/sendMessageExternal.coffee +++ /dev/null @@ -1,18 +0,0 @@ -Meteor.methods - sendMessageLivechat: (message) -> - if s.trim(message.msg) isnt '' - - if isNaN(TimeSync.serverOffset()) - message.ts = new Date() - else - message.ts = new Date(Date.now() + TimeSync.serverOffset()) - - message.u = - _id: Meteor.userId() - username: Meteor.user()?.username || 'visitor' - - message.temp = true - - # message = RocketChat.callbacks.run 'beforeSaveMessage', message - - ChatMessage.insert message diff --git a/packages/rocketchat-livechat/app/client/methods/sendMessageExternal.js b/packages/rocketchat-livechat/app/client/methods/sendMessageExternal.js new file mode 100644 index 0000000000000000000000000000000000000000..35c8844fa881d6f5c80170daff4c9abfe9c11f68 --- /dev/null +++ b/packages/rocketchat-livechat/app/client/methods/sendMessageExternal.js @@ -0,0 +1,24 @@ +Meteor.methods({ + sendMessageLivechat(message) { + if (s.trim(message.msg) !== '') { + if (isNaN(TimeSync.serverOffset())) { + message.ts = new Date(); + } else { + message.ts = new Date(Date.now() + TimeSync.serverOffset()); + } + + const user = Meteor.user(); + + message.u = { + _id: Meteor.userId(), + username: user && user.username || 'visitor' + }; + + message.temp = true; + + // message = RocketChat.callbacks.run 'beforeSaveMessage', message + + ChatMessage.insert(message); + } + } +}); diff --git a/packages/rocketchat-livechat/app/client/routes/router.coffee b/packages/rocketchat-livechat/app/client/routes/router.coffee deleted file mode 100644 index 6b1712849214f691f2ef3fc8c8c8f8c94955b1dd..0000000000000000000000000000000000000000 --- a/packages/rocketchat-livechat/app/client/routes/router.coffee +++ /dev/null @@ -1,12 +0,0 @@ -BlazeLayout.setRoot('body'); - -FlowRouter.route '/livechat', - name: 'index' - - triggersEnter: [ - -> - visitor.register() - ] - - action: -> - BlazeLayout.render 'main', {center: 'livechatWindow'} diff --git a/packages/rocketchat-livechat/app/client/routes/router.js b/packages/rocketchat-livechat/app/client/routes/router.js new file mode 100644 index 0000000000000000000000000000000000000000..6762a019a5e278446924716b5a5f0bac914b9431 --- /dev/null +++ b/packages/rocketchat-livechat/app/client/routes/router.js @@ -0,0 +1,11 @@ +BlazeLayout.setRoot('body'); + +FlowRouter.route('/livechat', { + name: 'index', + triggersEnter: [ + () => visitor.register() + ], + action() { + BlazeLayout.render('main', { center: 'livechatWindow' }); + } +}); diff --git a/packages/rocketchat-livechat/app/client/startup/visitor.coffee b/packages/rocketchat-livechat/app/client/startup/visitor.coffee deleted file mode 100644 index ddacd7c4134a3f51440b4db2756c8d8b0aa11b11..0000000000000000000000000000000000000000 --- a/packages/rocketchat-livechat/app/client/startup/visitor.coffee +++ /dev/null @@ -1,14 +0,0 @@ -@visitorId = new ReactiveVar null - -Meteor.startup -> - if not localStorage.getItem('rocketChatLivechat')? - localStorage.setItem('rocketChatLivechat', Random.id()) - else - Tracker.autorun (c) -> - if not Meteor.userId() and visitor.getToken() - Meteor.call 'livechat:loginByToken', visitor.getToken(), (err, result) -> - if result?.token - Meteor.loginWithToken result.token, (err, result) -> - c.stop() - - visitorId.set localStorage.getItem('rocketChatLivechat') diff --git a/packages/rocketchat-livechat/app/client/startup/visitor.js b/packages/rocketchat-livechat/app/client/startup/visitor.js new file mode 100644 index 0000000000000000000000000000000000000000..bd27d17e99af33dada30b70d1a174984c3a753cf --- /dev/null +++ b/packages/rocketchat-livechat/app/client/startup/visitor.js @@ -0,0 +1,21 @@ +this.visitorId = new ReactiveVar(null); + +Meteor.startup(() => { + if (!localStorage.getItem('rocketChatLivechat')) { + localStorage.setItem('rocketChatLivechat', Random.id()); + } else { + Tracker.autorun(c => { + if (!Meteor.userId() && visitor.getToken()) { + Meteor.call('livechat:loginByToken', visitor.getToken(), (err, result) => { + if (result && result.token) { + Meteor.loginWithToken(result.token, () => { + c.stop(); + }); + } + }); + } + }); + } + + this.visitorId.set(localStorage.getItem('rocketChatLivechat')); +}); diff --git a/packages/rocketchat-livechat/app/client/views/avatar.coffee b/packages/rocketchat-livechat/app/client/views/avatar.coffee deleted file mode 100644 index 5e2e62ab9ef3dccec20d19c86b9457726773d451..0000000000000000000000000000000000000000 --- a/packages/rocketchat-livechat/app/client/views/avatar.coffee +++ /dev/null @@ -1,14 +0,0 @@ -Template.avatar.helpers - imageUrl: -> - username = this.username - if not username? and this.userId? - username = Meteor.users.findOne(this.userId)?.username - - if not username? or Meteor.user()?.username is username - return - - Session.get "avatar_random_#{username}" - - url = getAvatarUrlFromUsername(username) - - return "background-image:url(#{url});" diff --git a/packages/rocketchat-livechat/app/client/views/avatar.js b/packages/rocketchat-livechat/app/client/views/avatar.js new file mode 100644 index 0000000000000000000000000000000000000000..5ab46c366a45a3b106f4253e89a0e9e8ed416aa8 --- /dev/null +++ b/packages/rocketchat-livechat/app/client/views/avatar.js @@ -0,0 +1,18 @@ +Template.avatar.helpers({ + imageUrl() { + let username = this.username; + if (!username && this.userId) { + const user = Meteor.users.findOne(this.userId, { fields: { username: 1 }}); + username = user && user.username; + } + + const currentUser = Meteor.users.findOne(Meteor.userId(), { fields: { username: 1 }}); + if (!username || (currentUser && currentUser.username === username)) { + return; + } + + Session.get(`avatar_random_${ username }`); + + return `background-image:url(${ getAvatarUrlFromUsername(username) });`; + } +}); diff --git a/packages/rocketchat-livechat/app/client/views/message.coffee b/packages/rocketchat-livechat/app/client/views/message.coffee deleted file mode 100644 index ebe1b3b017a577ee64882fa5afd61683fcc799a2..0000000000000000000000000000000000000000 --- a/packages/rocketchat-livechat/app/client/views/message.coffee +++ /dev/null @@ -1,86 +0,0 @@ -import moment from 'moment' - -Template.message.helpers - own: -> - return 'own' if this.u?._id is Meteor.userId() - - time: -> - return moment(this.ts).format('LT') - - date: -> - return moment(this.ts).format('LL') - - isTemp: -> - if @temp is true - return 'temp' - return - - error: -> - return 'msg-error' if @error - - body: -> - switch this.t - when 'r' then t('Room_name_changed', { room_name: this.msg, user_by: this.u.username }) - when 'au' then t('User_added_by', { user_added: this.msg, user_by: this.u.username }) - when 'ru' then t('User_removed_by', { user_removed: this.msg, user_by: this.u.username }) - when 'ul' then tr('User_left', { context: this.u.gender }, { user_left: this.u.username }) - when 'uj' then tr('User_joined', { context: this.u.gender }, { user: this.u.username }) - when 'wm' then t('Welcome', { user: this.u.username }) - when 'livechat-close' then t('Conversation_finished') - # when 'rtc' then RocketChat.callbacks.run 'renderRtcMessage', this - else - this.html = this.msg - if s.trim(this.html) isnt '' - this.html = s.escapeHTML this.html - # message = RocketChat.callbacks.run 'renderMessage', this - message = this - this.html = message.html.replace /\n/gm, '
' - return livechatAutolinker.link this.html - - system: -> - return 'system' if this.t in ['s', 'p', 'f', 'r', 'au', 'ru', 'ul', 'wm', 'uj', 'livechat-close'] - - sender: -> - agent = Livechat.agent - if agent && @u.username is agent.username - return agent.name or agent.username - return @u.username - - -Template.message.onViewRendered = (context) -> - view = this - this._domrange.onAttached (domRange) -> - lastNode = domRange.lastNode() - if lastNode.previousElementSibling?.dataset?.date isnt lastNode.dataset.date - $(lastNode).addClass('new-day') - $(lastNode).removeClass('sequential') - else if lastNode.previousElementSibling?.dataset?.username isnt lastNode.dataset.username - $(lastNode).removeClass('sequential') - - if lastNode.nextElementSibling?.dataset?.date is lastNode.dataset.date - $(lastNode.nextElementSibling).removeClass('new-day') - $(lastNode.nextElementSibling).addClass('sequential') - else - $(lastNode.nextElementSibling).addClass('new-day') - $(lastNode.nextElementSibling).removeClass('sequential') - - if lastNode.nextElementSibling?.dataset?.username isnt lastNode.dataset.username - $(lastNode.nextElementSibling).removeClass('sequential') - - ul = lastNode.parentElement - wrapper = ul.parentElement - - if context.urls?.length > 0 and Template.oembedBaseWidget? - for item in context.urls - do (item) -> - urlNode = lastNode.querySelector('.body a[href="'+item.url+'"]') - if urlNode? - $(urlNode).replaceWith Blaze.toHTMLWithData Template.oembedBaseWidget, item - - if not lastNode.nextElementSibling? - if lastNode.classList.contains('own') is true - view.parentView.parentView.parentView.parentView.parentView.templateInstance().atBottom = true - else - if view.parentView.parentView.parentView.parentView.parentView.templateInstance().atBottom isnt true - newMessage = document.querySelector(".new-message") - newMessage.className = "new-message" diff --git a/packages/rocketchat-livechat/app/client/views/message.js b/packages/rocketchat-livechat/app/client/views/message.js new file mode 100644 index 0000000000000000000000000000000000000000..86c64ed26e79bd90355313aa8c0ccb0f64a7a32f --- /dev/null +++ b/packages/rocketchat-livechat/app/client/views/message.js @@ -0,0 +1,114 @@ +/* globals Livechat, t, tr, livechatAutolinker */ +import moment from 'moment'; + +Template.message.helpers({ + own() { + if (this.u && this.u._id === Meteor.userId()) { + return 'own'; + } + }, + time() { + return moment(this.ts).format('LT'); + }, + date() { + return moment(this.ts).format('LL'); + }, + isTemp() { + if (this.temp === true) { + return 'temp'; + } + }, + error() { + if (this.error) { + return 'msg-error'; + } + }, + body() { + switch (this.t) { + case 'r': + return t('Room_name_changed', { room_name: this.msg, user_by: this.u.username }); + case 'au': + return t('User_added_by', { user_added: this.msg, user_by: this.u.username }); + case 'ru': + return t('User_removed_by', { user_removed: this.msg, user_by: this.u.username }); + case 'ul': + return tr('User_left', { context: this.u.gender }, { user_left: this.u.username }); + case 'uj': + return tr('User_joined', { context: this.u.gender }, { user: this.u.username }); + case 'wm': + return t('Welcome', { user: this.u.username }); + case 'livechat-close': + return t('Conversation_finished'); + // case 'rtc': return RocketChat.callbacks.run('renderRtcMessage', this); + default: + this.html = this.msg; + if (s.trim(this.html) !== '') { + this.html = s.escapeHTML(this.html); + } + // message = RocketChat.callbacks.run 'renderMessage', this + const message = this; + this.html = message.html.replace(/\n/gm, '
'); + return livechatAutolinker.link(this.html); + } + }, + + system() { + if (['s', 'p', 'f', 'r', 'au', 'ru', 'ul', 'wm', 'uj', 'livechat-close'].includes(this.t)) { + return 'system'; + } + }, + + sender() { + const agent = Livechat.agent; + if (agent && this.u.username === agent.username) { + return agent.name || agent.username; + } + return this.u.username; + } +}); + +Template.message.onViewRendered = function(context) { + const view = this; + this._domrange.onAttached(function(domRange) { + const lastNode = domRange.lastNode(); + const previousNode = lastNode.previousElementSibling; + const nextNode = lastNode.nextElementSibling; + + if (!previousNode || previousNode.dataset.date !== lastNode.dataset.date) { + $(lastNode).addClass('new-day'); + $(lastNode).removeClass('sequential'); + } else if (previousNode.dataset.username !== lastNode.dataset.username) { + $(lastNode).removeClass('sequential'); + } + + if (nextNode && nextNode.dataset.date === lastNode.dataset.date) { + $(nextNode).removeClass('new-day'); + $(nextNode).addClass('sequential'); + } else { + $(nextNode).addClass('new-day'); + $(nextNode).removeClass('sequential'); + } + + if (!nextNode || nextNode.dataset.username !== lastNode.dataset.username) { + $(nextNode).removeClass('sequential'); + } + + if (context.urls && context.urls.length > 0 && Template.oembedBaseWidget) { + context.urls.forEach(item => { + const urlNode = lastNode.querySelector(`.body a[href="${ item.url }"]`); + if (urlNode) { + $(urlNode).replaceWith(Blaze.toHTMLWithData(Template.oembedBaseWidget, item)); + } + }); + } + + if (!nextNode) { + if (lastNode.classList.contains('own')) { + view.parentView.parentView.parentView.parentView.parentView.templateInstance().atBottom = true; + } else if (view.parentView.parentView.parentView.parentView.parentView.templateInstance().atBottom !== true) { + const newMessage = document.querySelector('.new-message'); + newMessage.className = 'new-message'; + } + } + }); +}; diff --git a/packages/rocketchat-livechat/app/client/views/switchDepartment.js b/packages/rocketchat-livechat/app/client/views/switchDepartment.js index 9f2990ecd4bacc25ba5a2bac757f63320798d902..70ce6893d17bed3f6f71950ed2829e0c264a029a 100644 --- a/packages/rocketchat-livechat/app/client/views/switchDepartment.js +++ b/packages/rocketchat-livechat/app/client/views/switchDepartment.js @@ -44,7 +44,7 @@ Template.switchDepartment.events({ closeOnConfirm: true, html: false }, () => { - Meteor.call('livechat:closeByVisitor', (error) => { + Meteor.call('livechat:closeByVisitor', visitor.getRoom(), (error) => { if (error) { return console.log('Error ->', error); } diff --git a/packages/rocketchat-livechat/app/i18n/az.i18n.json b/packages/rocketchat-livechat/app/i18n/az.i18n.json new file mode 100644 index 0000000000000000000000000000000000000000..4d1f06b96d0c8f08d9e36e5452ce221ca444234a --- /dev/null +++ b/packages/rocketchat-livechat/app/i18n/az.i18n.json @@ -0,0 +1,3 @@ +{ + "Additional_Feedback": "əlavə Əlaqə" +} \ No newline at end of file diff --git a/packages/rocketchat-livechat/app/i18n/de.i18n.json b/packages/rocketchat-livechat/app/i18n/de.i18n.json index 2474b11a94af432dd4ce804d57cccf4c25c02241..625393ec2916c035c7c79ea01c4d942700d3c4d3 100644 --- a/packages/rocketchat-livechat/app/i18n/de.i18n.json +++ b/packages/rocketchat-livechat/app/i18n/de.i18n.json @@ -2,9 +2,11 @@ "Additional_Feedback": "Zusätzliches Feedback", "Appearance": "Erscheinungsbild", "Are_you_sure_do_you_want_end_this_chat": "Sind Sie sich sicher diesen Chat zu beenden?", + "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "Sind Sie sich sicher diesen Chat zu beenden und den Bereich zu wechseln?", "Cancel": "Abbrechen", "Change": "Ändern", "Chat_ended": "Chat beendet!", + "Choose_a_new_department": "Wähle einen neuen Bereich", "Close_menu": "Menü schließen", "Conversation_finished": "Gespräch beendet", "End_chat": "Chat beenden", @@ -17,6 +19,7 @@ "No": "Nein", "Options": "Optionen", "Please_answer_survey": "Bitte nehmen Sie sich einen Moment Zeit, um kurz einige Fragen zu dem Gespräch zu beantworten.", + "Please_choose_a_department": "Wähle einen Bereich", "Please_fill_name_and_email": "Bitte geben Sie einen Namen und eine E-Mail-Adresse ein.", "Powered_by": "Unterstützt von", "Request_video_chat": "Video-Chat anfragen", diff --git a/packages/rocketchat-livechat/app/i18n/fr.i18n.json b/packages/rocketchat-livechat/app/i18n/fr.i18n.json index 61c3e8275f9535f5f04d4fff47883223ea071d66..1e07751cfee850f71f29c8db077827c480b1a6f9 100644 --- a/packages/rocketchat-livechat/app/i18n/fr.i18n.json +++ b/packages/rocketchat-livechat/app/i18n/fr.i18n.json @@ -2,30 +2,30 @@ "Additional_Feedback": "Commentaires supplémentaires", "Appearance": "Apparence", "Are_you_sure_do_you_want_end_this_chat": "Êtes-vous sûr de vouloir mettre fin à cette conversation ?", - "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "Etes vous sûr de vouloir terminer ce chat en direct et changer de département ?", + "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "Etes vous sûr de vouloir terminer ce chat en direct et changer de service ?", "Cancel": "Annuler", "Change": "Changer", "Chat_ended": "Conversation terminée !", - "Choose_a_new_department": "Choisir un nouveau département", + "Choose_a_new_department": "Choisir un nouveau service", "Close_menu": "Fermer le menu", "Conversation_finished": "Conversation terminée", "End_chat": "Mettre fin à la conversation", - "How_friendly_was_the_chat_agent": "L'assistant du chat était-il sympathique ?", - "How_knowledgeable_was_the_chat_agent": "Les réponses de l'assistant du chat était-elles adaptées ?", - "How_responsive_was_the_chat_agent": "L'assistant du chat a-t-il répondu à vos questions ?", + "How_friendly_was_the_chat_agent": "Votre interlocuteur était-il sympathique ?", + "How_knowledgeable_was_the_chat_agent": "Les réponses de votre interlocuteur étaient-elles adaptées ?", + "How_responsive_was_the_chat_agent": "Votre interlocuteur a-t-il répondu à vos questions ?", "How_satisfied_were_you_with_this_chat": "Êtes-vous satisfait de ce chat?", "Installation": "Installation", "New_messages": "Nouveaux messages", "No": "Non", "Options": "Options", "Please_answer_survey": "Merci de prendre un moment pour répondre à un sondage rapide à propos de ce chat ", - "Please_choose_a_department": "Merci de choisir un département", - "Please_fill_name_and_email": "Veuillez remplir votre nom et votre adresse e-mail", + "Please_choose_a_department": "Merci de choisir un service", + "Please_fill_name_and_email": "Veuillez saisir votre nom et votre adresse e-mail", "Powered_by": "Propulsé par", "Request_video_chat": "Demander un chat vidéo", - "Select_a_department": "Sélectionner un département", - "Switch_department": "Changer de département", - "Department_switched": "Département changé", + "Select_a_department": "Sélectionner un service", + "Switch_department": "Changer de service", + "Department_switched": "Changement de service effectué", "Send": "Envoyer", "Skip": "Passer", "Start_Chat": "Démarrer un chat", diff --git a/packages/rocketchat-livechat/app/i18n/ko.i18n.json b/packages/rocketchat-livechat/app/i18n/ko.i18n.json index 569097d1f0411eafdfb62e43a354fc82712b98e8..04b448a9da550beb7da684f58e86a97f6c95f8ba 100644 --- a/packages/rocketchat-livechat/app/i18n/ko.i18n.json +++ b/packages/rocketchat-livechat/app/i18n/ko.i18n.json @@ -2,30 +2,30 @@ "Additional_Feedback": "추가 의견", "Appearance": "모양", "Are_you_sure_do_you_want_end_this_chat": "이 채팅을 정말 끝내시겠습니까?", - "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "정말 이 채팅을 종료하고 부서를 변경하겠습니까?", + "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "현재 진행중인 채팅을 종료하고 부서를 변경하시겠습니까?", "Cancel": "취소", "Change": "변경", - "Chat_ended": "채팅 종료", - "Choose_a_new_department": "새로운 부서를 선택하세요.", + "Chat_ended": "채팅이 종료되었습니다. ", + "Choose_a_new_department": "새 부서를 선택해주세요", "Close_menu": "메뉴 닫기", "Conversation_finished": "대화 종료됨", "End_chat": "채팅 끝남", - "How_friendly_was_the_chat_agent": "채팅 담당자는 얼마나 친절했나요?", - "How_knowledgeable_was_the_chat_agent": "채팅 에이전트의 기반 지식이 풍부했나요?", - "How_responsive_was_the_chat_agent": "채팅 담당자는 얼마나 빠르게 응답했나요?", - "How_satisfied_were_you_with_this_chat": "채팅에 얼마나 만족했나요?", + "How_friendly_was_the_chat_agent": "상담사가 친절했나요?", + "How_knowledgeable_was_the_chat_agent": "상담사의 관련 업무 지식이 충분했나요?", + "How_responsive_was_the_chat_agent": "삼담사가 빠르게 응답했나요?", + "How_satisfied_were_you_with_this_chat": "채팅 내용에 얼마나 만족 하였나요?", "Installation": "설치", "New_messages": "새 메시지", "No": "아니오", "Options": "옵션", "Please_answer_survey": "이 채팅에 대한 간단한 설문 조사에 응답하기 위해 잠시 시간을내어 주시기 바랍니다", - "Please_choose_a_department": "부서를 선택하세요.", + "Please_choose_a_department": "부서를 선택해주세요", "Please_fill_name_and_email": "이름과 이메일을 입력하세요", - "Powered_by": "에 의해 구동", + "Powered_by": "지원을받는", "Request_video_chat": "비디오채팅 요청", "Select_a_department": "부서를 선택해주세요", "Switch_department": "부서 변경", - "Department_switched": "부서가 변경되었습니다.", + "Department_switched": "부서가 변경되었습니다", "Send": "전송", "Skip": "건너뛰기", "Start_Chat": "채팅 시작", @@ -33,13 +33,14 @@ "Survey_instructions": "당신의 만족도를 평가해 주세요. 매우불만은 1, 매우 만족은 5 입니다.", "Thank_you_for_your_feedback": "의견을 보내 주셔서 감사합니다", "Thanks_We_ll_get_back_to_you_soon": "감사합니다! 곧 다시 연락드리겠습니다", - "transcript_sent": "채팅 내용을 발송했습니다.", - "Type_your_email": "이메일을 입력", - "Type_your_message": "메시지를 입력", - "Type_your_name": "당신의 이름을 입력", - "User_joined": "사용자 가입", - "User_left": "사용자 왼쪽", - "We_are_offline_Sorry_for_the_inconvenience": "우리는 오프라인 상태입니다. 불편을 드려 죄송합니다.", + "transcript_sent": "채팅 내용을 발송했습니다", + "Type_your_email": "이메일을 입력해주세요", + "Type_your_message": "메시지를 입력해주세요", + "Type_your_name": "이름을 입력해주세요", + "User_joined": "사용자가 참여하였습니다", + "User_left": "사용자가 떠났습니다", + "We_are_offline_Sorry_for_the_inconvenience": "현재 오프라인 상태입니다. 불편을 드려 죄송합니다", "Yes": "예", + "You": "당신", "You_must_complete_all_fields": "모든 필드를 작성해야합니다" } \ No newline at end of file diff --git a/packages/rocketchat-livechat/app/i18n/no.i18n.json b/packages/rocketchat-livechat/app/i18n/no.i18n.json index d6955adb8572b4c553abd56118203f4cf81085bd..539088fa6dc64634a059a906eb8aa1673a967c2a 100644 --- a/packages/rocketchat-livechat/app/i18n/no.i18n.json +++ b/packages/rocketchat-livechat/app/i18n/no.i18n.json @@ -1,4 +1,29 @@ { "Additional_Feedback": "Tilleggs Tilbakemelding", - "No": "Nei" + "Appearance": "Utseende", + "Are_you_sure_do_you_want_end_this_chat": "Er du sikker på at du vil avslutte denne samtalen?", + "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "Er du sikker på at du vil avslutte denne samtalen og bytte avdeling?", + "Cancel": "Avbryt", + "Change": "Endre", + "Chat_ended": "Samtalen er avsluttet!", + "Choose_a_new_department": "Velg ny avdeling", + "Close_menu": "Lukk meny", + "Conversation_finished": "Samtalen er avsluttet", + "End_chat": "Avslutt samtale", + "How_friendly_was_the_chat_agent": "Hvor vennlig var personen du pratet med?", + "How_responsive_was_the_chat_agent": "Hvor raskt svarte personen du pratet med?", + "How_satisfied_were_you_with_this_chat": "Er du fornøyd med samtalen?", + "Installation": "Installasjon", + "New_messages": "Ny melding", + "No": "Nei", + "Options": "Egenskaper", + "Send": "Send", + "Skip": "Hopp over", + "Start_Chat": "Start samtale", + "Survey": "Undersøkelse", + "Type_your_message": "Skriv inn din beskjed", + "Type_your_name": "Skriv inn ditt navn", + "Yes": "Ja", + "You": "Deg", + "You_must_complete_all_fields": "Du må fylle inn alle feltene" } \ No newline at end of file diff --git a/packages/rocketchat-livechat/app/i18n/pt.i18n.json b/packages/rocketchat-livechat/app/i18n/pt.i18n.json index 0c533fac233c8d0f930abee3a8da93764f6ba632..aade2c0e02147c1e4434f85bcd342a9ad93d18d7 100644 --- a/packages/rocketchat-livechat/app/i18n/pt.i18n.json +++ b/packages/rocketchat-livechat/app/i18n/pt.i18n.json @@ -2,8 +2,11 @@ "Additional_Feedback": "Feedback Adicional", "Appearance": "Aparência", "Are_you_sure_do_you_want_end_this_chat": "Você tem certeza que deseja encerrar?", + "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "Tem a certeza que deseja terminar este chat e mudar o departamento?", "Cancel": "Cancelar", + "Change": "Alterar", "Chat_ended": "Chat encerrado!", + "Choose_a_new_department": "Escolha um novo departamento", "Close_menu": "Fechar menu", "Conversation_finished": "Chat encerrado", "End_chat": "Encerrar chat", @@ -16,10 +19,13 @@ "No": "Não", "Options": "Opções", "Please_answer_survey": "Por favor nos dê um momento para responder uma rápida pesquisa sobre este chat", + "Please_choose_a_department": "Por favor escolha um departamento", "Please_fill_name_and_email": "Por favor preencha nome e email", "Powered_by": "Distribuído por", "Request_video_chat": "Solicitar vídeoconferência", "Select_a_department": "Selecione um departamento", + "Switch_department": "Mudar departamento", + "Department_switched": "Departamento alterado", "Send": "Enviar", "Skip": "Pular", "Start_Chat": "Iniciar bate-papo", diff --git a/packages/rocketchat-livechat/app/i18n/ru.i18n.json b/packages/rocketchat-livechat/app/i18n/ru.i18n.json index 1e3ece962134d7a4adbde41f00b450fed3ebed19..1397a579a870bcdae5fe7c17e9fff3422ecb9f79 100644 --- a/packages/rocketchat-livechat/app/i18n/ru.i18n.json +++ b/packages/rocketchat-livechat/app/i18n/ru.i18n.json @@ -2,6 +2,7 @@ "Additional_Feedback": "Дополнительная обратная связь", "Appearance": "Внешний вид", "Are_you_sure_do_you_want_end_this_chat": "Вы уверены что хотите завершить этот чат?", + "Are_you_sure_do_you_want_end_this_chat_and_switch_department": "Вы действительно хотите завершить этот чат и сменить отдел?", "Cancel": "Отмена", "Chat_ended": "Чат закончен!", "Close_menu": "Закрыть меню", @@ -16,6 +17,7 @@ "No": "Нет", "Options": "Параметры", "Please_answer_survey": "Пожалуйста, уделите время, чтобы ответить на несколько вопросов об этом чате", + "Please_choose_a_department": "Пожалуйста, выберете отдел", "Please_fill_name_and_email": "Введите имя и электронный адрес", "Powered_by": "Представлен", "Request_video_chat": "Запрос чата с видео", diff --git a/packages/rocketchat-livechat/roomType.js b/packages/rocketchat-livechat/roomType.js index c59fdf9a803443e3d4cc0f3ab53a041cbd147e73..de542583e11993f8b64192381d25a985f033d929 100644 --- a/packages/rocketchat-livechat/roomType.js +++ b/packages/rocketchat-livechat/roomType.js @@ -1,8 +1,8 @@ /* globals openRoom, LivechatInquiry */ RocketChat.roomTypes.add('l', 5, { - template: 'livechat', icon: 'icon-chat-empty', + label: 'Livechat', route: { name: 'live', path: '/live/:code(\\d+)', diff --git a/packages/rocketchat-markdown/markdown.js b/packages/rocketchat-markdown/markdown.js index 365a0126dd391d04abcab1db347532f1e7e982c8..5f0269f99face6b6b19162e5b170960b87f5e116 100644 --- a/packages/rocketchat-markdown/markdown.js +++ b/packages/rocketchat-markdown/markdown.js @@ -11,24 +11,6 @@ class MarkdownClass { parseNotEscaped(msg) { const schemes = RocketChat.settings.get('Markdown_SupportSchemesForLink').split(',').join('|'); - // Support ![alt text](http://image url) - msg = msg.replace(new RegExp(`!\\[([^\\]]+)\\]\\(((?:${ schemes }):\\/\\/[^\\)]+)\\)`, 'gm'), function(match, title, url) { - const target = url.indexOf(Meteor.absoluteUrl()) === 0 ? '' : '_blank'; - return `
`; - }); - - // Support [Text](http://link) - msg = msg.replace(new RegExp(`\\[([^\\]]+)\\]\\(((?:${ schemes }):\\/\\/[^\\)]+)\\)`, 'gm'), function(match, title, url) { - const target = url.indexOf(Meteor.absoluteUrl()) === 0 ? '' : '_blank'; - return `${ _.escapeHTML(title) }`; - }); - - // Support - msg = msg.replace(new RegExp(`(?:<|<)((?:${ schemes }):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)`, 'gm'), (match, url, title) => { - const target = url.indexOf(Meteor.absoluteUrl()) === 0 ? '' : '_blank'; - return `${ _.escapeHTML(title) }`; - }); - if (RocketChat.settings.get('Markdown_Headers')) { // Support # Text for h1 msg = msg.replace(/^# (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm, '

$1

'); @@ -68,6 +50,24 @@ class MarkdownClass { // Remove new-line between blockquotes. msg = msg.replace(/<\/blockquote>\n
`; + }); + + // Support [Text](http://link) + msg = msg.replace(new RegExp(`\\[([^\\]]+)\\]\\(((?:${ schemes }):\\/\\/[^\\)]+)\\)`, 'gm'), function(match, title, url) { + const target = url.indexOf(Meteor.absoluteUrl()) === 0 ? '' : '_blank'; + return `${ _.escapeHTML(title) }`; + }); + + // Support + msg = msg.replace(new RegExp(`(?:<|<)((?:${ schemes }):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)`, 'gm'), (match, url, title) => { + const target = url.indexOf(Meteor.absoluteUrl()) === 0 ? '' : '_blank'; + return `${ _.escapeHTML(title) }`; + }); + if (typeof window !== 'undefined' && window !== null ? window.rocketDebug : undefined) { console.log('Markdown', msg); } return msg; @@ -90,4 +90,5 @@ RocketChat.callbacks.add('renderMessage', MarkdownMessage, RocketChat.callbacks. if (Meteor.isClient) { Blaze.registerHelper('RocketChatMarkdown', text => Markdown.parse(text)); + Blaze.registerHelper('RocketChatMarkdownUnescape', text => Markdown.parseNotEscaped(text)); } diff --git a/packages/rocketchat-markdown/markdowncode.js b/packages/rocketchat-markdown/markdowncode.js index 1ff5ca80f7876458d7a8e0cb9972cafd18c3594c..e00b030d01bce7792c52762f4c2681e32e9a08e5 100644 --- a/packages/rocketchat-markdown/markdowncode.js +++ b/packages/rocketchat-markdown/markdowncode.js @@ -51,38 +51,25 @@ class MarkdownCode { } // Separate text in code blocks and non code blocks - const msgParts = message.html.split(/(^.*)(```(?:[a-zA-Z]+)?(?:(?:.|\n)*?)```)(.*\n?)$/gm); + const msgParts = message.html.split(/(^.*)(```(?:[a-zA-Z]+)?(?:(?:.|\r|\n)*?)```)(.*\n?)$/gm); for (let index = 0; index < msgParts.length; index++) { // Verify if this part is code const part = msgParts[index]; - const codeMatch = part.match(/^```(.*[\n\ ]?)([\s\S]*?)```+?$/); + const codeMatch = part.match(/^```(.*[\r\n\ ]?)([\s\S]*?)```+?$/); if (codeMatch != null) { // Process highlight if this part is code - let code; - let lang; - let result; const singleLine = codeMatch[0].indexOf('\n') === -1; - - if (singleLine) { - lang = ''; - code = _.unescapeHTML(codeMatch[1] + codeMatch[2]); - } else { - lang = codeMatch[1]; - code = _.unescapeHTML(codeMatch[2]); - } - - if (s.trim(lang) === '') { - lang = ''; - } - - if (!Array.from(hljs.listLanguages()).includes(s.trim(lang))) { - result = hljs.highlightAuto((lang + code)); - } else { - result = hljs.highlight(s.trim(lang), code); - } - + const lang = !singleLine && Array.from(hljs.listLanguages()).includes(s.trim(codeMatch[1])) ? s.trim(codeMatch[1]) : ''; + const code = + singleLine ? + _.unescapeHTML(codeMatch[1]) : + lang === '' ? + _.unescapeHTML(codeMatch[1] + codeMatch[2]) : + _.unescapeHTML(codeMatch[2]); + + const result = lang === '' ? hljs.highlightAuto((lang + code)) : hljs.highlight(lang, code); const token = `=!=${ Random.id() }=!=`; message.tokens.push({ diff --git a/packages/rocketchat-mentions/Mentions.js b/packages/rocketchat-mentions/Mentions.js new file mode 100644 index 0000000000000000000000000000000000000000..c9a744a9ac402e0f2a2b41ae1b048197038341ee --- /dev/null +++ b/packages/rocketchat-mentions/Mentions.js @@ -0,0 +1,77 @@ +/* +* Mentions is a named function that will process Mentions +* @param {Object} message - The message object +*/ +import _ from 'underscore'; +export default class { + constructor({pattern, useRealName, me}) { + this.pattern = pattern; + this.useRealName = useRealName; + this.me = me; + } + set me(m) { + this._me = m; + } + get me() { + return typeof this._me === 'function' ? this._me() : this._me; + } + set pattern(p) { + this._pattern = p; + } + get pattern() { + return typeof this._pattern === 'function' ? this._pattern() : this._pattern; + } + set useRealName(s) { + this._useRealName = s; + } + get useRealName() { + return typeof this._useRealName === 'function' ? this._useRealName() : this._useRealName; + } + get userMentionRegex() { + return new RegExp(`@(${ this.pattern })`, 'gm'); + } + get channelMentionRegex() { + return new RegExp(`#(${ this.pattern })`, 'gm'); + } + replaceUsers(str, message, me) { + return str.replace(this.userMentionRegex, (match, username) => { + if (['all', 'here'].includes(username)) { + return `${ match }`; + } + + const mentionObj = _.findWhere(message.mentions, {username}); + if (message.temp == null && mentionObj == null) { + return match; + } + const name = this.useRealName && mentionObj && mentionObj.name; + console.log({name}); + + return `${ name || match }`; + }); + } + replaceChannels(str, message) { + //since apostrophe escaped contains # we need to unescape it + return str.replace(/'/g, '\'').replace(this.channelMentionRegex, (match, name) => { + if (message.temp == null && _.findWhere(message.channels, {name}) == null) { + return match; + } + return `${ match }`; + }); + } + getUserMentions(str) { + return str.match(this.userMentionRegex) || []; + } + getChannelMentions(str) { + return str.match(this.channelMentionRegex) || []; + } + parse(message) { + let msg = (message && message.html) || ''; + if (!msg.trim()) { + return message; + } + msg = this.replaceUsers(msg, message, this.me); + msg = this.replaceChannels(msg, message, this.me); + message.html = msg; + return message; + } +} diff --git a/packages/rocketchat-mentions/MentionsServer.js b/packages/rocketchat-mentions/MentionsServer.js new file mode 100644 index 0000000000000000000000000000000000000000..40132d9432e94b12d3dad90a845d2817a710d1e2 --- /dev/null +++ b/packages/rocketchat-mentions/MentionsServer.js @@ -0,0 +1,76 @@ +/* +* Mentions is a named function that will process Mentions +* @param {Object} message - The message object +*/ +import Mentions from './Mentions'; +export default class MentionsServer extends Mentions { + constructor(args) { + super(args); + this.messageMaxAll = args.messageMaxAll; + this.getChannel = args.getChannel; + this.getChannels = args.getChannels; + this.getUsers = args.getUsers; + } + set getUsers(m) { + this._getUsers = m; + } + get getUsers() { + return typeof this._getUsers === 'function' ? this._getUsers : () => this._getUsers; + } + set getChannels(m) { + this._getChannels = m; + } + get getChannels() { + return typeof this._getChannels === 'function' ? this._getChannels : () => this._getChannels; + } + set getChannel(m) { + this._getChannel = m; + } + get getChannel() { + return typeof this._getChannel === 'function' ? this._getChannel : () => this._getChannel; + } + set messageMaxAll(m) { + this._messageMaxAll = m; + } + get messageMaxAll() { + return typeof this._messageMaxAll === 'function' ? this._messageMaxAll() : this._messageMaxAll; + } + getUsersByMentions({msg, rid}) { + let mentions = this.getUserMentions(msg); + const mentionsAll = []; + const userMentions = []; + + mentions.forEach((m) => { + const mention = m.trim().substr(1); + if (mention !== 'all' && mention !== 'here') { + return userMentions.push(mention); + } + if (mention === 'all') { + const messageMaxAll = this.messageMaxAll; + const allChannel = this.getChannel(rid); + if (messageMaxAll !== 0 && allChannel.usernames.length >= messageMaxAll) { + return; + } + } + mentionsAll.push({ + _id: mention, + username: mention + }); + }); + mentions = userMentions.length ? this.getUsers(userMentions) : []; + return [...mentionsAll, ...mentions]; + } + getChannelbyMentions({msg}) { + const channels = this.getChannelMentions(msg); + return this.getChannels(channels.map(c => c.trim().substr(1))); + } + execute(message) { + const mentionsAll = this.getUsersByMentions(message); + const channels = this.getChannelbyMentions(message); + + message.mentions = mentionsAll; + + message.channels = channels; + return message; + } +} diff --git a/packages/rocketchat-mentions/client.js b/packages/rocketchat-mentions/client.js index 041eb0bfb123e95c5a4ac2bf6145d87446775c01..b95e9bb320e9486fc829ec79b47efc1055128944 100644 --- a/packages/rocketchat-mentions/client.js +++ b/packages/rocketchat-mentions/client.js @@ -1,41 +1,16 @@ -/* - * Mentions is a named function that will process Mentions - * @param {Object} message - The message object - */ - -function MentionsClient(message) { - let msg = (message && message.html) || ''; - if (!msg.trim()) { - return message; +import Mentions from './Mentions'; +const MentionsClient = new Mentions({ + pattern() { + return RocketChat.settings.get('UTF8_Names_Validation'); + }, + useRealName() { + return RocketChat.settings.get('UI_Use_Real_Name'); + }, + me() { + const me = Meteor.user(); + return me && me.username; } - const msgMentionRegex = new RegExp(`(?:^|\\s|\\n)(@(${ RocketChat.settings.get('UTF8_Names_Validation') }):?)[:.,\s]?`, 'g'); - - let me = Meteor.user(); - me = me ? me.username : null; - - msg = msg.replace(msgMentionRegex, function(match, mention, username) { - if (['all', 'here'].includes(username)) { - return match.replace(mention, `${ mention }`); - } - if (message.temp == null && _.findWhere(message.mentions, {username}) == null) { - return match; - } - return match.replace(mention, `${ mention }`); - }); - - const msgChannelRegex = new RegExp(`(?:^|\\s|\\n)(#(${ RocketChat.settings.get('UTF8_Names_Validation') }))[:.,\s]?`, 'g'); - - msg = msg.replace(msgChannelRegex, function(match, mention, name) { - if (message.temp == null && _.findWhere(message.channels, {name}) == null) { - return match; - } - return match.replace(mention, `${ mention }`); - }); - message.html = msg; - return message; -} - - -RocketChat.callbacks.add('renderMessage', MentionsClient, RocketChat.callbacks.priority.MEDIUM, 'mentions-message'); +}); -RocketChat.callbacks.add('renderMentions', MentionsClient, RocketChat.callbacks.priority.MEDIUM, 'mentions-mentions'); +RocketChat.callbacks.add('renderMessage', (message) => MentionsClient.parse(message), RocketChat.callbacks.priority.MEDIUM, 'mentions-message'); +RocketChat.callbacks.add('renderMentions', (message) => MentionsClient.parse(message), RocketChat.callbacks.priority.MEDIUM, 'mentions-mentions'); diff --git a/packages/rocketchat-mentions/package.js b/packages/rocketchat-mentions/package.js index 80be96108096c1ac0a66686db59b7cfd5f669f94..ccc3cf51ab1c2e5ee9de0c3f9b435852a4492e07 100644 --- a/packages/rocketchat-mentions/package.js +++ b/packages/rocketchat-mentions/package.js @@ -8,9 +8,11 @@ Package.describe({ Package.onUse(function(api) { api.use([ 'ecmascript', - 'rocketchat:lib' + 'rocketchat:lib', + 'underscore' ]); api.addFiles('server.js', 'server'); api.addFiles('client.js', 'client'); + // api.('mentions.js', 'client'); }); diff --git a/packages/rocketchat-mentions/server.js b/packages/rocketchat-mentions/server.js index 66aa59c086616df9568abb2cdcb5a7f06a30874d..f04030f231ddd4924150d3e7b9f750c81056af6f 100644 --- a/packages/rocketchat-mentions/server.js +++ b/packages/rocketchat-mentions/server.js @@ -1,49 +1,9 @@ -/* -* Mentions is a named function that will process Mentions -* @param {Object} message - The message object -*/ - -function MentionsServer(message) { - const msgMentionRegex = new RegExp(`(?:^|\\s|\\n)(?:@)(${ RocketChat.settings.get('UTF8_Names_Validation') })`, 'g'); - const mentionsAll = []; - const userMentions = []; - let mentions = message.msg.match(msgMentionRegex); - if (mentions) { - mentions.forEach((m) => { - const mention = m.trim().substr(1); - if (mention !== 'all' && mention !== 'here') { - return userMentions.push(mention); - } - if (mention === 'all') { - const messageMaxAll = RocketChat.settings.get('Message_MaxAll'); - const allChannel = RocketChat.models.Rooms.findOneById(message.rid); - if (messageMaxAll !== 0 && allChannel.usernames.length >= messageMaxAll) { - return; - } - } - mentionsAll.push({ - _id: mention, - username: mention - }); - }); - mentions = userMentions.length ? Meteor.users.find({ username: {$in: _.unique(userMentions)}}, { fields: {_id: true, username: true }}).fetch() : []; - - const verifiedMentions = [...mentionsAll, ...mentions]; - if (verifiedMentions.length !== 0) { - message.mentions = verifiedMentions; - } - } - - const msgChannelRegex = new RegExp(`(?:^|\\s|\\n)(?:#)(${ RocketChat.settings.get('UTF8_Names_Validation') })`, 'g'); - let channels = message.msg.match(msgChannelRegex); - if (channels) { - channels = channels.map(c => c.trim().substr(1)); - const verifiedChannels = RocketChat.models.Rooms.find({ name: {$in: _.unique(channels)}, t: 'c' }, { fields: {_id: 1, name: 1 }}).fetch(); - if (verifiedChannels.length !== 0) { - message.channels = verifiedChannels; - } - } - return message; -} - -RocketChat.callbacks.add('beforeSaveMessage', MentionsServer, RocketChat.callbacks.priority.HIGH, 'mentions'); +import MentionsServer from './MentionsServer'; +const mention = new MentionsServer({ + pattern: () => RocketChat.settings.get('UTF8_Names_Validation'), + messageMaxAll: () => RocketChat.settings.get('Message_MaxAll'), + getUsers: (usernames) => Meteor.users.find({ username: {$in: _.unique(usernames)}}, { fields: {_id: true, username: true }}).fetch(), + getChannel: (rid) => RocketChat.models.Rooms.findOneById(rid), + getChannels: (channels) => RocketChat.models.Rooms.find({ name: {$in: _.unique(channels)}, t: 'c' }, { fields: {_id: 1, name: 1 }}).fetch() +}); +RocketChat.callbacks.add('beforeSaveMessage', (message) => mention.execute(message), RocketChat.callbacks.priority.HIGH, 'mentions'); diff --git a/packages/rocketchat-mentions/tests/client.tests.js b/packages/rocketchat-mentions/tests/client.tests.js new file mode 100644 index 0000000000000000000000000000000000000000..c6ef427d14f2ac0f5fbffad952abc22bdba6e3c0 --- /dev/null +++ b/packages/rocketchat-mentions/tests/client.tests.js @@ -0,0 +1,294 @@ +/* eslint-env mocha */ +import 'babel-polyfill'; +import assert from 'assert'; + +import Mentions from '../Mentions'; +let mention; +beforeEach(function functionName() { + mention = new Mentions({ + pattern: '[0-9a-zA-Z-_.]+', + me: () => 'me' + }); +}); +describe('Mention', function() { + describe('get pattern', () => { + const regexp = '[0-9a-zA-Z-_.]+'; + beforeEach(() => mention.pattern = () => regexp); + describe('by function', function functionName() { + it(`should be equal to ${ regexp }`, ()=> { + assert.equal(regexp, mention.pattern); + }); + }); + describe('by const', function functionName() { + it(`should be equal to ${ regexp }`, ()=> { + assert.equal(regexp, mention.pattern); + }); + }); + }); + describe('get useRealName', () => { + beforeEach(() => mention.useRealName = () => true); + describe('by function', function functionName() { + it('should be true', () => { + assert.equal(true, mention.useRealName); + }); + }); + describe('by const', function functionName() { + it('should be true', () => { + assert.equal(true, mention.useRealName); + }); + }); + }); + describe('get me', () => { + const me = 'me'; + describe('by function', function functionName() { + beforeEach(() => mention.me = () => me); + it(`should be equal to ${ me }`, ()=> { + assert.equal(me, mention.me); + }); + }); + describe('by const', function functionName() { + beforeEach(() => mention.me = me); + it(`should be equal to ${ me }`, ()=> { + assert.equal(me, mention.me); + }); + }); + }); + describe('getUserMentions', function functionName() { + describe('for simple text, no mentions', () => { + const result = []; + [ + '#rocket.cat', + 'hello rocket.cat how are you?' + ] + .forEach(text => { + it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { + assert.deepEqual(result, mention.getUserMentions(text)); + }); + }); + }); + describe('for one user', () => { + const result = ['@rocket.cat']; + [ + '@rocket.cat', + ' @rocket.cat ', + 'hello @rocket.cat', + 'hello,@rocket.cat', + '@rocket.cat, hello', + '@rocket.cat,hello', + 'hello @rocket.cat how are you?' + ] + .forEach(text => { + it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { + assert.deepEqual(result, mention.getUserMentions(text)); + }); + }); + it.skip('shoud return without the "." from "@rocket.cat."', () => { + assert.deepEqual(result, mention.getUserMentions('@rocket.cat.')); + }); + it.skip('shoud return without the "_" from "@rocket.cat_"', () => { + assert.deepEqual(result, mention.getUserMentions('@rocket.cat_')); + }); + it.skip('shoud return without the "-" from "@rocket.cat."', () => { + assert.deepEqual(result, mention.getUserMentions('@rocket.cat-')); + }); + }); + describe('for two users', () => { + const result = ['@rocket.cat', '@all']; + [ + '@rocket.cat @all', + ' @rocket.cat @all ', + 'hello @rocket.cat and @all', + '@rocket.cat, hello @all', + 'hello @rocket.cat and @all how are you?' + ] + .forEach(text => { + it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { + assert.deepEqual(result, mention.getUserMentions(text)); + }); + }); + }); + }); + + describe('getChannelMentions', function functionName() { + describe('for simple text, no mentions', () => { + const result = []; + [ + '@rocket.cat', + 'hello rocket.cat how are you?' + ] + .forEach(text => { + it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { + assert.deepEqual(result, mention.getChannelMentions(text)); + }); + }); + }); + describe('for one channel', () => { + const result = ['#general']; + [ + '#general', + ' #general ', + 'hello #general', + '#general, hello', + 'hello #general, how are you?' + ].forEach(text => { + it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { + assert.deepEqual(result, mention.getChannelMentions(text)); + }); + }); + it.skip('shoud return without the "." from "#general."', () => { + assert.deepEqual(result, mention.getUserMentions('#general.')); + }); + it.skip('shoud return without the "_" from "#general_"', () => { + assert.deepEqual(result, mention.getUserMentions('#general_')); + }); + it.skip('shoud return without the "-" from "#general."', () => { + assert.deepEqual(result, mention.getUserMentions('#general-')); + }); + }); + describe('for two channels', () => { + const result = ['#general', '#other']; + [ + '#general #other', + ' #general #other', + 'hello #general and #other', + '#general, hello #other', + 'hello #general #other, how are you?' + ].forEach(text => { + it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { + assert.deepEqual(result, mention.getChannelMentions(text)); + }); + }); + }); + + }); + +}); +const message = { + mentions:[{username:'rocket.cat', name: 'Rocket.Cat'}, {username:'admin', name: 'Admin'}, {username: 'me', name: 'Me'}], + channels: [{name: 'general'}, {name: 'rocket.cat'}] +}; +describe('replace methods', function() { + describe('replaceUsers', () => { + it('shoud render for @all', () => { + const result = mention.replaceUsers('@all', message, 'me'); + assert.equal('@all', result); + }); + const str2 = '@rocket.cat'; + it(`shoud render for ${ str2 }`, () => { + const result = mention.replaceUsers('@rocket.cat', message, 'me'); + assert.equal(result, `${ str2 }`); + }); + + it(`shoud render for "hello ${ str2 }"`, () => { + const result = mention.replaceUsers(`hello ${ str2 }`, message, 'me'); + assert.equal(result, `hello ${ str2 }`); + }); + it('shoud render for unknow/private user "hello @unknow"', () => { + const result = mention.replaceUsers('hello @unknow', message, 'me'); + assert.equal(result, 'hello @unknow'); + }); + it('shoud render for me', () => { + const result = mention.replaceUsers('hello @me', message, 'me'); + assert.equal(result, 'hello @me'); + }); + }); + + describe('replaceUsers (RealNames)', () => { + beforeEach(() => { + mention.useRealName = () => true; + }); + it('shoud render for @all', () => { + const result = mention.replaceUsers('@all', message, 'me'); + assert.equal('@all', result); + }); + + const str2 = '@rocket.cat'; + const str2Name = 'Rocket.Cat'; + it(`shoud render for ${ str2 }`, () => { + const result = mention.replaceUsers('@rocket.cat', message, 'me'); + assert.equal(result, `${ str2Name }`); + }); + + it(`shoud render for "hello ${ str2 }"`, () => { + const result = mention.replaceUsers(`hello ${ str2 }`, message, 'me'); + assert.equal(result, `hello ${ str2Name }`); + }); + it('shoud render for unknow/private user "hello @unknow"', () => { + const result = mention.replaceUsers('hello @unknow', message, 'me'); + assert.equal(result, 'hello @unknow'); + }); + it('shoud render for me', () => { + const result = mention.replaceUsers('hello @me', message, 'me'); + assert.equal(result, 'hello Me'); + }); + }); + + describe('replaceChannels', () => { + it('shoud render for #general', () => { + const result = mention.replaceChannels('#general', message, 'me'); + assert.equal('#general', result); + }); + const str2 = '#rocket.cat'; + it(`shoud render for ${ str2 }`, () => { + const result = mention.replaceChannels(str2, message, 'me'); + assert.equal(result, `${ str2 }`); + }); + it(`shoud render for "hello ${ str2 }"`, () => { + const result = mention.replaceChannels(`hello ${ str2 }`, message, 'me'); + assert.equal(result, `hello ${ str2 }`); + }); + it('shoud render for unknow/private channel "hello #unknow"', () => { + const result = mention.replaceChannels('hello #unknow', message, 'me'); + assert.equal(result, 'hello #unknow'); + }); + }); + + describe('parse all', () => { + it('shoud render for #general', () => { + message.html = '#general'; + const result = mention.parse(message, 'me'); + assert.equal('#general', result.html); + }); + it('shoud render for "#general and @rocket.cat', () => { + message.html = '#general and @rocket.cat'; + const result = mention.parse(message, 'me'); + assert.equal('#general and @rocket.cat', result.html); + }); + it('shoud render for "', () => { + message.html = ''; + const result = mention.parse(message, 'me'); + assert.equal('', result.html); + }); + it('shoud render for "simple text', () => { + message.html = 'simple text'; + const result = mention.parse(message, 'me'); + assert.equal('simple text', result.html); + }); + }); + + describe('parse all (RealNames)', () => { + beforeEach(() => { + mention.useRealName = () => true; + }); + it('shoud render for #general', () => { + message.html = '#general'; + const result = mention.parse(message, 'me'); + assert.equal('#general', result.html); + }); + it('shoud render for "#general and @rocket.cat', () => { + message.html = '#general and @rocket.cat'; + const result = mention.parse(message, 'me'); + assert.equal('#general and Rocket.Cat', result.html); + }); + it('shoud render for "', () => { + message.html = ''; + const result = mention.parse(message, 'me'); + assert.equal('', result.html); + }); + it('shoud render for "simple text', () => { + message.html = 'simple text'; + const result = mention.parse(message, 'me'); + assert.equal('simple text', result.html); + }); + }); +}); diff --git a/packages/rocketchat-mentions/tests/server.tests.js b/packages/rocketchat-mentions/tests/server.tests.js new file mode 100644 index 0000000000000000000000000000000000000000..ae0a38521fdbdd4062ce28d0ebbc71e8aa6d1160 --- /dev/null +++ b/packages/rocketchat-mentions/tests/server.tests.js @@ -0,0 +1,299 @@ +/* eslint-env mocha */ +import 'babel-polyfill'; +import assert from 'assert'; + +import MentionsServer from '../MentionsServer'; + + +let mention; + +beforeEach(function() { + mention = new MentionsServer({ + pattern: '[0-9a-zA-Z-_.]+', + messageMaxAll: () => 4, //|| RocketChat.settings.get('Message_MaxAll') + getUsers: (usernames) => { + return [{ + _id: 1, + username: 'rocket.cat' + }, { + _id: 2, + username: 'jon' + }].filter(user => usernames.includes(user.username));//Meteor.users.find({ username: {$in: _.unique(usernames)}}, { fields: {_id: true, username: true }}).fetch(); + }, + getChannel: () => { + return { + usernames: [{ + _id: 1, + username: 'rocket.cat' + }, { + _id: 2, + username: 'jon' + }] + }; + // RocketChat.models.Rooms.findOneById(message.rid);, + }, + getChannels(channels) { + return [{ + _id: 1, + name: 'general' + }].filter(channel => channels.includes(channel.name)); + // return RocketChat.models.Rooms.find({ name: {$in: _.unique(channels)}, t: 'c' }, { fields: {_id: 1, name: 1 }}).fetch(); + } + }); +}); + +describe('Mention Server', () => { + describe('getUsersByMentions', () => { + describe('for @all but the number of users is greater than messageMaxAll', () => { + beforeEach(() => { + mention.getChannel = () => { + return { + usernames:[{ + _id: 1, + username: 'rocket.cat' + }, { + _id: 2, + username: 'jon' + }, { + _id: 3, + username: 'jon1' + }, { + _id: 4, + username: 'jon2' + }, { + _id: 5, + username: 'jon3' + }] + }; + //Meteor.users.find({ username: {$in: _.unique(usernames)}}, { fields: {_id: true, username: true }}).fetch(); + }; + }); + it('should return nothing', () => { + const message = { + msg: '@all' + }; + const expected = []; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); + }); + describe('for one user', () => { + beforeEach(() => { + mention.getChannel = () => { + return { + usernames:[{ + _id: 1, + username: 'rocket.cat' + }, { + _id: 2, + username: 'jon' + }] + }; + //Meteor.users.find({ username: {$in: _.unique(usernames)}}, { fields: {_id: true, username: true }}).fetch(); + }; + }); + it('should return "all"', () => { + const message = { + msg: '@all' + }; + const expected = [{ + _id: 'all', + username: 'all' + }]; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); + it('should return "here"', () => { + const message = { + msg: '@here' + }; + const expected = [{ + _id: 'here', + username: 'here' + }]; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); + it('should return "rocket.cat"', () => { + const message = { + msg: '@rocket.cat' + }; + const expected = [{ + _id: 1, + username: 'rocket.cat' + }]; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); + }); + describe('for two user', () => { + it('should return "all and here"', () => { + const message = { + msg: '@all @here' + }; + const expected = [{ + _id: 'all', + username: 'all' + }, { + _id: 'here', + username: 'here' + }]; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); + it('should return "here and rocket.cat"', () => { + const message = { + msg: '@here @rocket.cat' + }; + const expected = [{ + _id: 'here', + username: 'here' + }, { + _id: 1, + username: 'rocket.cat' + }]; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); + + it('should return "here, rocket.cat, jon"', () => { + const message = { + msg: '@here @rocket.cat @jon' + }; + const expected = [{ + _id: 'here', + username: 'here' + }, { + _id: 1, + username: 'rocket.cat' + }, { + _id: 2, + username: 'jon' + }]; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); + }); + + describe('for an unknow user', () => { + it('should return "nothing"', () => { + const message = { + msg: '@unknow' + }; + const expected = []; + const result = mention.getUsersByMentions(message); + assert.deepEqual(expected, result); + }); + }); + + }); + describe('getChannelbyMentions', () => { + it('should return the channel "general"', () => { + const message = { + msg: '#general' + }; + const expected = [{ + _id: 1, + name: 'general' + }]; + const result = mention.getChannelbyMentions(message); + assert.deepEqual(result, expected); + }); + it('should return nothing"', () => { + const message = { + msg: '#unknow' + }; + const expected = []; + const result = mention.getChannelbyMentions(message); + assert.deepEqual(result, expected); + }); + }); + describe('execute', () => { + it('should return the channel "general"', () => { + const message = { + msg: '#general' + }; + const expected = [{ + _id: 1, + name: 'general' + }]; + const result = mention.getChannelbyMentions(message); + assert.deepEqual(result, expected); + }); + it('should return nothing"', () => { + const message = { + msg: '#unknow' + }; + const expected = { + msg: '#unknow', + mentions: [], + channels: [] + }; + const result = mention.execute(message); + assert.deepEqual(result, expected); + }); + }); + + describe('getters and setters', ()=> { + describe('messageMaxAll', ()=> { + const mention = new MentionsServer({}); + describe('constant', ()=> { + it('should return the informed value', () => { + mention.messageMaxAll = 4; + assert.deepEqual(mention.messageMaxAll, 4); + }); + }); + describe('function', ()=> { + it('should return the informed value', () => { + mention.messageMaxAll = () => 4; + assert.deepEqual(mention.messageMaxAll, 4); + }); + }); + }); + describe('getUsers', ()=> { + const mention = new MentionsServer({}); + describe('constant', ()=> { + it('should return the informed value', () => { + mention.getUsers = 4; + assert.deepEqual(mention.getUsers(), 4); + }); + }); + describe('function', ()=> { + it('should return the informed value', () => { + mention.getUsers = () => 4; + assert.deepEqual(mention.getUsers(), 4); + }); + }); + }); + describe('getChannels', ()=> { + const mention = new MentionsServer({}); + describe('constant', ()=> { + it('should return the informed value', () => { + mention.getChannels = 4; + assert.deepEqual(mention.getChannels(), 4); + }); + }); + describe('function', ()=> { + it('should return the informed value', () => { + mention.getChannels = () => 4; + assert.deepEqual(mention.getChannels(), 4); + }); + }); + }); + describe('getChannel', ()=> { + const mention = new MentionsServer({}); + describe('constant', ()=> { + it('should return the informed value', () => { + mention.getChannel = true; + assert.deepEqual(mention.getChannel(), true); + }); + }); + describe('function', ()=> { + it('should return the informed value', () => { + mention.getChannel = () => true; + assert.deepEqual(mention.getChannel(), true); + }); + }); + }); + }); +}); diff --git a/packages/rocketchat-message-attachments/package.js b/packages/rocketchat-message-attachments/package.js index 22563380d4c68d7d6518b21e6819f4a01e9af49d..099b85a74640b8bfff39fee42acd3c7653203140 100644 --- a/packages/rocketchat-message-attachments/package.js +++ b/packages/rocketchat-message-attachments/package.js @@ -9,7 +9,6 @@ Package.onUse(function(api) { api.use([ 'templating', 'ecmascript', - 'coffeescript', 'underscore', 'rocketchat:lib', 'less' diff --git a/packages/rocketchat-message-pin/client/actionButton.js b/packages/rocketchat-message-pin/client/actionButton.js index 826f33f9fbe0eda0bb4af1b5ee4e8778c8de1d1b..5e77c3fb7308cf93d5be9840b94bd65d58140100 100644 --- a/packages/rocketchat-message-pin/client/actionButton.js +++ b/packages/rocketchat-message-pin/client/actionButton.js @@ -79,7 +79,7 @@ Meteor.startup(function() { i18nLabel: 'Permalink', classes: 'clipboard', context: ['pinned'], - action() { + action(event) { const message = this._arguments[1]; RocketChat.MessageAction.hideDropDown(); $(event.currentTarget).attr('data-clipboard-text', RocketChat.MessageAction.getPermaLink(message._id)); diff --git a/packages/rocketchat-message-star/client/actionButton.js b/packages/rocketchat-message-star/client/actionButton.js index 64faa7b24bad0a06d5a4ed31201cabe92d651393..cbcbc64f15224a47020fb3c55b63d6c7ef27c8a7 100644 --- a/packages/rocketchat-message-star/client/actionButton.js +++ b/packages/rocketchat-message-star/client/actionButton.js @@ -70,7 +70,7 @@ Meteor.startup(function() { i18nLabel: 'Permalink', classes: 'clipboard', context: ['starred'], - action() { + action(event) { const message = this._arguments[1]; RocketChat.MessageAction.hideDropDown(); $(event.currentTarget).attr('data-clipboard-text', RocketChat.MessageAction.getPermaLink(message._id)); diff --git a/packages/rocketchat-migrations/migrations.js b/packages/rocketchat-migrations/migrations.js index ee54c145e6960a2c84009cabde2795b52845aa63..82885469da8b0fbf1b0fa823d94bc29fedac6e9f 100644 --- a/packages/rocketchat-migrations/migrations.js +++ b/packages/rocketchat-migrations/migrations.js @@ -239,7 +239,7 @@ Migrations._migrateTo = function(version, rerun) { if (rerun) { log.info('Rerunning version ' + version); - migrate('up', version); + migrate('up', this._findIndexByVersion(version)); log.info('Finished migrating.'); unlock(); return true; diff --git a/packages/rocketchat-oauth2-server-config/admin/client/collection.coffee b/packages/rocketchat-oauth2-server-config/admin/client/collection.coffee deleted file mode 100644 index a0f53700c27c8cb28ecc7ec1eadcff8387f53220..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oauth2-server-config/admin/client/collection.coffee +++ /dev/null @@ -1 +0,0 @@ -@ChatOAuthApps = new Mongo.Collection 'rocketchat_oauth_apps' diff --git a/packages/rocketchat-oauth2-server-config/admin/client/collection.js b/packages/rocketchat-oauth2-server-config/admin/client/collection.js new file mode 100644 index 0000000000000000000000000000000000000000..f196d7dc17ebd168e0cb33379cfdf449be5fcedc --- /dev/null +++ b/packages/rocketchat-oauth2-server-config/admin/client/collection.js @@ -0,0 +1 @@ +this.ChatOAuthApps = new Mongo.Collection('rocketchat_oauth_apps'); diff --git a/packages/rocketchat-oauth2-server-config/admin/client/route.coffee b/packages/rocketchat-oauth2-server-config/admin/client/route.coffee deleted file mode 100644 index f2fcc3b836596dea446f0a2906346fada821c5df..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oauth2-server-config/admin/client/route.coffee +++ /dev/null @@ -1,17 +0,0 @@ -FlowRouter.route '/admin/oauth-apps', - name: 'admin-oauth-apps' - action: (params) -> - BlazeLayout.render 'main', - center: 'pageSettingsContainer' - pageTitle: t('OAuth_Applications') - pageTemplate: 'oauthApps' - - -FlowRouter.route '/admin/oauth-app/:id?', - name: 'admin-oauth-app' - action: (params) -> - BlazeLayout.render 'main', - center: 'pageSettingsContainer' - pageTitle: t('OAuth_Application') - pageTemplate: 'oauthApp' - params: params diff --git a/packages/rocketchat-oauth2-server-config/admin/client/route.js b/packages/rocketchat-oauth2-server-config/admin/client/route.js new file mode 100644 index 0000000000000000000000000000000000000000..7b0b7f469f2c6e897a18babd5c38685de0bb8aff --- /dev/null +++ b/packages/rocketchat-oauth2-server-config/admin/client/route.js @@ -0,0 +1,22 @@ +FlowRouter.route('/admin/oauth-apps', { + name: 'admin-oauth-apps', + action() { + return BlazeLayout.render('main', { + center: 'pageSettingsContainer', + pageTitle: t('OAuth_Applications'), + pageTemplate: 'oauthApps' + }); + } +}); + +FlowRouter.route('/admin/oauth-app/:id?', { + name: 'admin-oauth-app', + action(params) { + return BlazeLayout.render('main', { + center: 'pageSettingsContainer', + pageTitle: t('OAuth_Application'), + pageTemplate: 'oauthApp', + params + }); + } +}); diff --git a/packages/rocketchat-oauth2-server-config/admin/client/startup.coffee b/packages/rocketchat-oauth2-server-config/admin/client/startup.coffee deleted file mode 100644 index 0e4c45616177a4211c57a8b43f91d7da27cf3509..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oauth2-server-config/admin/client/startup.coffee +++ /dev/null @@ -1,5 +0,0 @@ -RocketChat.AdminBox.addOption - href: 'admin-oauth-apps' - i18nLabel: 'OAuth Apps' - permissionGranted: -> - return RocketChat.authz.hasAllPermission('manage-oauth-apps') diff --git a/packages/rocketchat-oauth2-server-config/admin/client/startup.js b/packages/rocketchat-oauth2-server-config/admin/client/startup.js new file mode 100644 index 0000000000000000000000000000000000000000..27cee4bd5597190716cb93045ba236e1195df972 --- /dev/null +++ b/packages/rocketchat-oauth2-server-config/admin/client/startup.js @@ -0,0 +1,7 @@ +RocketChat.AdminBox.addOption({ + href: 'admin-oauth-apps', + i18nLabel: 'OAuth Apps', + permissionGranted() { + return RocketChat.authz.hasAllPermission('manage-oauth-apps'); + } +}); diff --git a/packages/rocketchat-oauth2-server-config/admin/client/views/oauthApp.coffee b/packages/rocketchat-oauth2-server-config/admin/client/views/oauthApp.coffee deleted file mode 100644 index f8da9f6ce692d301e7f1f85f5ef6c340d604ae5a..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oauth2-server-config/admin/client/views/oauthApp.coffee +++ /dev/null @@ -1,81 +0,0 @@ -import toastr from 'toastr' -Template.oauthApp.onCreated -> - @subscribe 'oauthApps' - @record = new ReactiveVar - active: true - - -Template.oauthApp.helpers - hasPermission: -> - return RocketChat.authz.hasAllPermission 'manage-oauth-apps' - - data: -> - params = Template.instance().data.params?() - - if params?.id? - data = ChatOAuthApps.findOne({_id: params.id}) - if data? - data.authorization_url = Meteor.absoluteUrl("oauth/authorize") - data.access_token_url = Meteor.absoluteUrl("oauth/token") - - Template.instance().record.set data - return data - - return Template.instance().record.curValue - - -Template.oauthApp.events - "click .submit > .delete": -> - params = Template.instance().data.params() - - swal - title: t('Are_you_sure') - text: t('You_will_not_be_able_to_recover') - type: 'warning' - showCancelButton: true - confirmButtonColor: '#DD6B55' - confirmButtonText: t('Yes_delete_it') - cancelButtonText: t('Cancel') - closeOnConfirm: false - html: false - , -> - Meteor.call "deleteOAuthApp", params.id, (err, data) -> - swal - title: t('Deleted') - text: t('Your_entry_has_been_deleted') - type: 'success' - timer: 1000 - showConfirmButton: false - - FlowRouter.go "admin-oauth-apps" - - "click .submit > .save": -> - name = $('[name=name]').val().trim() - active = $('[name=active]:checked').val().trim() is "1" - redirectUri = $('[name=redirectUri]').val().trim() - - if name is '' - return toastr.error TAPi18n.__("The_application_name_is_required") - - if redirectUri is '' - return toastr.error TAPi18n.__("The_redirectUri_is_required") - - app = - name: name - active: active - redirectUri: redirectUri - - params = Template.instance().data.params?() - if params?.id? - Meteor.call "updateOAuthApp", params.id, app, (err, data) -> - if err? - return handleError(err) - - toastr.success TAPi18n.__("Application_updated") - else - Meteor.call "addOAuthApp", app, (err, data) -> - if err? - return handleError(err) - - toastr.success TAPi18n.__("Application_added") - FlowRouter.go "admin-oauth-app", {id: data._id} diff --git a/packages/rocketchat-oauth2-server-config/admin/client/views/oauthApp.js b/packages/rocketchat-oauth2-server-config/admin/client/views/oauthApp.js new file mode 100644 index 0000000000000000000000000000000000000000..0bf2d9e0e52b68dac40a9a854699f57c44581ac6 --- /dev/null +++ b/packages/rocketchat-oauth2-server-config/admin/client/views/oauthApp.js @@ -0,0 +1,94 @@ +/* globals ChatOAuthApps */ +import toastr from 'toastr'; + +Template.oauthApp.onCreated(function() { + this.subscribe('oauthApps'); + this.record = new ReactiveVar({ + active: true + }); +}); + +Template.oauthApp.helpers({ + hasPermission() { + return RocketChat.authz.hasAllPermission('manage-oauth-apps'); + }, + data() { + const instance = Template.instance(); + if (typeof instance.data.params === 'function') { + const params = instance.data.params(); + if (params && params.id) { + const data = ChatOAuthApps.findOne({ _id: params.id }); + if (data) { + data.authorization_url = Meteor.absoluteUrl('oauth/authorize'); + data.access_token_url = Meteor.absoluteUrl('oauth/token'); + Template.instance().record.set(data); + return data; + } + } + } + return Template.instance().record.curValue; + } +}); + +Template.oauthApp.events({ + 'click .submit > .delete'() { + const params = Template.instance().data.params(); + swal({ + title: t('Are_you_sure'), + text: t('You_will_not_be_able_to_recover'), + type: 'warning', + showCancelButton: true, + confirmButtonColor: '#DD6B55', + confirmButtonText: t('Yes_delete_it'), + cancelButtonText: t('Cancel'), + closeOnConfirm: false, + html: false + }, function() { + Meteor.call('deleteOAuthApp', params.id, function() { + swal({ + title: t('Deleted'), + text: t('Your_entry_has_been_deleted'), + type: 'success', + timer: 1000, + showConfirmButton: false + }); + FlowRouter.go('admin-oauth-apps'); + }); + }); + }, + 'click .submit > .save'() { + const instance = Template.instance(); + const name = $('[name=name]').val().trim(); + const active = $('[name=active]:checked').val().trim() === '1'; + const redirectUri = $('[name=redirectUri]').val().trim(); + if (name === '') { + return toastr.error(TAPi18n.__('The_application_name_is_required')); + } + if (redirectUri === '') { + return toastr.error(TAPi18n.__('The_redirectUri_is_required')); + } + const app = { + name, + active, + redirectUri + }; + if (typeof instance.data.params === 'function') { + const params = instance.data.params(); + if (params && params.id) { + return Meteor.call('updateOAuthApp', params.id, app, function(err) { + if (err != null) { + return handleError(err); + } + toastr.success(TAPi18n.__('Application_updated')); + }); + } + } + Meteor.call('addOAuthApp', app, function(err, data) { + if (err != null) { + return handleError(err); + } + toastr.success(TAPi18n.__('Application_added')); + FlowRouter.go('admin-oauth-app', { id: data._id }); + }); + } +}); diff --git a/packages/rocketchat-oauth2-server-config/admin/client/views/oauthApps.coffee b/packages/rocketchat-oauth2-server-config/admin/client/views/oauthApps.coffee deleted file mode 100644 index 37e3df9d3ca00b7adb3f1f15db173804c602cb43..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oauth2-server-config/admin/client/views/oauthApps.coffee +++ /dev/null @@ -1,14 +0,0 @@ -import moment from 'moment' - -Template.oauthApps.onCreated -> - @subscribe 'oauthApps' - -Template.oauthApps.helpers - hasPermission: -> - return RocketChat.authz.hasAllPermission 'manage-oauth-apps' - - applications: -> - return ChatOAuthApps.find() - - dateFormated: (date) -> - return moment(date).format('L LT') diff --git a/packages/rocketchat-oauth2-server-config/admin/client/views/oauthApps.js b/packages/rocketchat-oauth2-server-config/admin/client/views/oauthApps.js new file mode 100644 index 0000000000000000000000000000000000000000..ddc3ce6c1539b77ccbcff6594320ba04e4c9cc0e --- /dev/null +++ b/packages/rocketchat-oauth2-server-config/admin/client/views/oauthApps.js @@ -0,0 +1,18 @@ +/* globals ChatOAuthApps */ +import moment from 'moment'; + +Template.oauthApps.onCreated(function() { + this.subscribe('oauthApps'); +}); + +Template.oauthApps.helpers({ + hasPermission() { + return RocketChat.authz.hasAllPermission('manage-oauth-apps'); + }, + applications() { + return ChatOAuthApps.find(); + }, + dateFormated(date) { + return moment(date).format('L LT'); + } +}); diff --git a/packages/rocketchat-oauth2-server-config/admin/server/methods/addOAuthApp.coffee b/packages/rocketchat-oauth2-server-config/admin/server/methods/addOAuthApp.coffee deleted file mode 100644 index fca6a47f1c28993588969ac112c7d41b5b6111fd..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oauth2-server-config/admin/server/methods/addOAuthApp.coffee +++ /dev/null @@ -1,22 +0,0 @@ -Meteor.methods - addOAuthApp: (application) -> - if not RocketChat.authz.hasPermission @userId, 'manage-oauth-apps' - throw new Meteor.Error 'error-not-allowed', 'Not allowed', { method: 'addOAuthApp' } - - if not _.isString(application.name) or application.name.trim() is '' - throw new Meteor.Error 'error-invalid-name', 'Invalid name', { method: 'addOAuthApp' } - - if not _.isString(application.redirectUri) or application.redirectUri.trim() is '' - throw new Meteor.Error 'error-invalid-redirectUri', 'Invalid redirectUri', { method: 'addOAuthApp' } - - if not _.isBoolean(application.active) - throw new Meteor.Error 'error-invalid-arguments', 'Invalid arguments', { method: 'addOAuthApp' } - - application.clientId = Random.id() - application.clientSecret = Random.secret() - application._createdAt = new Date - application._createdBy = RocketChat.models.Users.findOne @userId, {fields: {username: 1}} - - application._id = RocketChat.models.OAuthApps.insert application - - return application diff --git a/packages/rocketchat-oauth2-server-config/admin/server/methods/addOAuthApp.js b/packages/rocketchat-oauth2-server-config/admin/server/methods/addOAuthApp.js new file mode 100644 index 0000000000000000000000000000000000000000..80ddecef58fc307a3113f76c6f77842490b4ac70 --- /dev/null +++ b/packages/rocketchat-oauth2-server-config/admin/server/methods/addOAuthApp.js @@ -0,0 +1,22 @@ +Meteor.methods({ + addOAuthApp(application) { + if (!RocketChat.authz.hasPermission(this.userId, 'manage-oauth-apps')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'addOAuthApp' }); + } + if (!_.isString(application.name) || application.name.trim() === '') { + throw new Meteor.Error('error-invalid-name', 'Invalid name', { method: 'addOAuthApp' }); + } + if (!_.isString(application.redirectUri) || application.redirectUri.trim() === '') { + throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', { method: 'addOAuthApp' }); + } + if (!_.isBoolean(application.active)) { + throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', { method: 'addOAuthApp' }); + } + application.clientId = Random.id(); + application.clientSecret = Random.secret(); + application._createdAt = new Date; + application._createdBy = RocketChat.models.Users.findOne(this.userId, { fields: { username: 1 } }); + application._id = RocketChat.models.OAuthApps.insert(application); + return application; + } +}); diff --git a/packages/rocketchat-oauth2-server-config/admin/server/methods/deleteOAuthApp.coffee b/packages/rocketchat-oauth2-server-config/admin/server/methods/deleteOAuthApp.coffee deleted file mode 100644 index f0367d88ca42ee348abbc86b4d4b277141fb61a4..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oauth2-server-config/admin/server/methods/deleteOAuthApp.coffee +++ /dev/null @@ -1,14 +0,0 @@ -Meteor.methods - deleteOAuthApp: (applicationId) -> - if not RocketChat.authz.hasPermission @userId, 'manage-oauth-apps' - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'deleteOAuthApp' }); - - application = RocketChat.models.OAuthApps.findOne(applicationId) - - if not application? - throw new Meteor.Error('error-application-not-found', 'Application not found', { method: 'deleteOAuthApp' }); - - - RocketChat.models.OAuthApps.remove _id: applicationId - - return true diff --git a/packages/rocketchat-oauth2-server-config/admin/server/methods/deleteOAuthApp.js b/packages/rocketchat-oauth2-server-config/admin/server/methods/deleteOAuthApp.js new file mode 100644 index 0000000000000000000000000000000000000000..1426496ed407248127d525b98d02e19a72f3e630 --- /dev/null +++ b/packages/rocketchat-oauth2-server-config/admin/server/methods/deleteOAuthApp.js @@ -0,0 +1,13 @@ +Meteor.methods({ + deleteOAuthApp(applicationId) { + if (!RocketChat.authz.hasPermission(this.userId, 'manage-oauth-apps')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'deleteOAuthApp' }); + } + const application = RocketChat.models.OAuthApps.findOne(applicationId); + if (application == null) { + throw new Meteor.Error('error-application-not-found', 'Application not found', { method: 'deleteOAuthApp' }); + } + RocketChat.models.OAuthApps.remove({ _id: applicationId }); + return true; + } +}); diff --git a/packages/rocketchat-oauth2-server-config/admin/server/methods/updateOAuthApp.coffee b/packages/rocketchat-oauth2-server-config/admin/server/methods/updateOAuthApp.coffee deleted file mode 100644 index ea9dba6cdca9c26b32def23b8fbbc5fd4c182d57..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oauth2-server-config/admin/server/methods/updateOAuthApp.coffee +++ /dev/null @@ -1,27 +0,0 @@ -Meteor.methods - updateOAuthApp: (applicationId, application) -> - if not RocketChat.authz.hasPermission @userId, 'manage-oauth-apps' - throw new Meteor.Error 'error-not-allowed', 'Not allowed', { method: 'updateOAuthApp' } - - if not _.isString(application.name) or application.name.trim() is '' - throw new Meteor.Error 'error-invalid-name', 'Invalid name', { method: 'updateOAuthApp' } - - if not _.isString(application.redirectUri) or application.redirectUri.trim() is '' - throw new Meteor.Error 'error-invalid-redirectUri', 'Invalid redirectUri', { method: 'updateOAuthApp' } - - if not _.isBoolean(application.active) - throw new Meteor.Error 'error-invalid-arguments', 'Invalid arguments', { method: 'updateOAuthApp' } - - currentApplication = RocketChat.models.OAuthApps.findOne(applicationId) - if not currentApplication? - throw new Meteor.Error 'error-application-not-found', 'Application not found', { method: 'updateOAuthApp' } - - RocketChat.models.OAuthApps.update applicationId, - $set: - name: application.name - active: application.active - redirectUri: application.redirectUri - _updatedAt: new Date - _updatedBy: RocketChat.models.Users.findOne @userId, {fields: {username: 1}} - - return RocketChat.models.OAuthApps.findOne(applicationId) diff --git a/packages/rocketchat-oauth2-server-config/admin/server/methods/updateOAuthApp.js b/packages/rocketchat-oauth2-server-config/admin/server/methods/updateOAuthApp.js new file mode 100644 index 0000000000000000000000000000000000000000..78a742b726f04f94e4427eda5833f5bc22584bcf --- /dev/null +++ b/packages/rocketchat-oauth2-server-config/admin/server/methods/updateOAuthApp.js @@ -0,0 +1,34 @@ +Meteor.methods({ + updateOAuthApp(applicationId, application) { + if (!RocketChat.authz.hasPermission(this.userId, 'manage-oauth-apps')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'updateOAuthApp' }); + } + if (!_.isString(application.name) || application.name.trim() === '') { + throw new Meteor.Error('error-invalid-name', 'Invalid name', { method: 'updateOAuthApp' }); + } + if (!_.isString(application.redirectUri) || application.redirectUri.trim() === '') { + throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', { method: 'updateOAuthApp' }); + } + if (!_.isBoolean(application.active)) { + throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', { method: 'updateOAuthApp' }); + } + const currentApplication = RocketChat.models.OAuthApps.findOne(applicationId); + if (currentApplication == null) { + throw new Meteor.Error('error-application-not-found', 'Application not found', { method: 'updateOAuthApp' }); + } + RocketChat.models.OAuthApps.update(applicationId, { + $set: { + name: application.name, + active: application.active, + redirectUri: application.redirectUri, + _updatedAt: new Date, + _updatedBy: RocketChat.models.Users.findOne(this.userId, { + fields: { + username: 1 + } + }) + } + }); + return RocketChat.models.OAuthApps.findOne(applicationId); + } +}); diff --git a/packages/rocketchat-oauth2-server-config/admin/server/publications/oauthApps.coffee b/packages/rocketchat-oauth2-server-config/admin/server/publications/oauthApps.coffee deleted file mode 100644 index ef3d48a4f49c9efd0a9cb588b6194e699a9df3fb..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oauth2-server-config/admin/server/publications/oauthApps.coffee +++ /dev/null @@ -1,8 +0,0 @@ -Meteor.publish 'oauthApps', -> - unless @userId - return @ready() - - if not RocketChat.authz.hasPermission @userId, 'manage-oauth-apps' - @error Meteor.Error "error-not-allowed", "Not allowed", { publish: 'oauthApps' } - - return RocketChat.models.OAuthApps.find() diff --git a/packages/rocketchat-oauth2-server-config/admin/server/publications/oauthApps.js b/packages/rocketchat-oauth2-server-config/admin/server/publications/oauthApps.js new file mode 100644 index 0000000000000000000000000000000000000000..33cc5f5ff9550560b32234d1a7ce96616f7142fb --- /dev/null +++ b/packages/rocketchat-oauth2-server-config/admin/server/publications/oauthApps.js @@ -0,0 +1,9 @@ +Meteor.publish('oauthApps', function() { + if (!this.userId) { + return this.ready(); + } + if (!RocketChat.authz.hasPermission(this.userId, 'manage-oauth-apps')) { + this.error(Meteor.Error('error-not-allowed', 'Not allowed', { publish: 'oauthApps' })); + } + return RocketChat.models.OAuthApps.find(); +}); diff --git a/packages/rocketchat-oauth2-server-config/oauth/client/oauth2-client.coffee b/packages/rocketchat-oauth2-server-config/oauth/client/oauth2-client.coffee deleted file mode 100644 index 52c098f3b407f839513f48a8c6807480a42798fe..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oauth2-server-config/oauth/client/oauth2-client.coffee +++ /dev/null @@ -1,47 +0,0 @@ -# @ChatOAuthApps = new Mongo.Collection 'rocketchat_oauth_apps' - -FlowRouter.route '/oauth/authorize', - action: (params, queryParams) -> - BlazeLayout.render 'main', - center: 'authorize' - modal: true - client_id: queryParams.client_id - redirect_uri: queryParams.redirect_uri - response_type: queryParams.response_type - state: queryParams.state - - -FlowRouter.route '/oauth/error/:error', - action: (params, queryParams) -> - BlazeLayout.render 'main', - center: 'oauth404' - modal: true - error: params.error - - -Template.authorize.onCreated -> - @subscribe 'authorizedOAuth' - @subscribe 'oauthClient', @data.client_id() - - -Template.authorize.helpers - getToken: -> - return localStorage.getItem('Meteor.loginToken') - - getClient: -> - return ChatOAuthApps.findOne() - - -Template.authorize.events - 'click #logout-oauth': -> - return Meteor.logout() - - 'click #cancel-oauth': -> - return window.close() - - -Template.authorize.onRendered -> - @autorun (c) => - if Meteor.user()?.oauth?.authorizedClients?.indexOf(@data.client_id()) > -1 - c.stop() - $('button[type=submit]').click() diff --git a/packages/rocketchat-oauth2-server-config/oauth/client/oauth2-client.js b/packages/rocketchat-oauth2-server-config/oauth/client/oauth2-client.js new file mode 100644 index 0000000000000000000000000000000000000000..afdc2a3cd879cea043747fc733e7d17233b2ddea --- /dev/null +++ b/packages/rocketchat-oauth2-server-config/oauth/client/oauth2-client.js @@ -0,0 +1,57 @@ +// @ChatOAuthApps = new Mongo.Collection 'rocketchat_oauth_apps' +/*globals ChatOAuthApps */ +FlowRouter.route('/oauth/authorize', { + action(params, queryParams) { + BlazeLayout.render('main', { + center: 'authorize', + modal: true, + client_id: queryParams.client_id, + redirect_uri: queryParams.redirect_uri, + response_type: queryParams.response_type, + state: queryParams.state + }); + } +}); + +FlowRouter.route('/oauth/error/:error', { + action(params) { + BlazeLayout.render('main', { + center: 'oauth404', + modal: true, + error: params.error + }); + } +}); + +Template.authorize.onCreated(function() { + this.subscribe('authorizedOAuth'); + this.subscribe('oauthClient', this.data.client_id()); +}); + +Template.authorize.helpers({ + getToken() { + return localStorage.getItem('Meteor.loginToken'); + }, + getClient() { + return ChatOAuthApps.findOne(); + } +}); + +Template.authorize.events({ + 'click #logout-oauth'() { + return Meteor.logout(); + }, + 'click #cancel-oauth'() { + return window.close(); + } +}); + +Template.authorize.onRendered(function() { + this.autorun(c => { + const user = Meteor.user(); + if (user && user.oauth && user.oauth.authorizedClients && user.oauth.authorizedClients.includes(this.data.client_id())) { + c.stop(); + $('button[type=submit]').click(); + } + }); +}); diff --git a/packages/rocketchat-oauth2-server-config/oauth/server/default-services.coffee b/packages/rocketchat-oauth2-server-config/oauth/server/default-services.coffee deleted file mode 100644 index fa457c95e66d5aa4e8037f101f4fa4b560a6c55e..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oauth2-server-config/oauth/server/default-services.coffee +++ /dev/null @@ -1,12 +0,0 @@ -if not RocketChat.models.OAuthApps.findOne('zapier') - RocketChat.models.OAuthApps.insert - _id: 'zapier' - name: 'Zapier' - active: true - clientId: 'zapier' - clientSecret: 'RTK6TlndaCIolhQhZ7_KHIGOKj41RnlaOq_o-7JKwLr' - redirectUri: 'https://zapier.com/dashboard/auth/oauth/return/App32270API/' - _createdAt: new Date - _createdBy: - _id: 'system' - username: 'system' diff --git a/packages/rocketchat-oauth2-server-config/oauth/server/default-services.js b/packages/rocketchat-oauth2-server-config/oauth/server/default-services.js new file mode 100644 index 0000000000000000000000000000000000000000..329acb3c19a191bf3915bdf64ec77cb845f8ec6f --- /dev/null +++ b/packages/rocketchat-oauth2-server-config/oauth/server/default-services.js @@ -0,0 +1,15 @@ +if (!RocketChat.models.OAuthApps.findOne('zapier')) { + RocketChat.models.OAuthApps.insert({ + _id: 'zapier', + name: 'Zapier', + active: true, + clientId: 'zapier', + clientSecret: 'RTK6TlndaCIolhQhZ7_KHIGOKj41RnlaOq_o-7JKwLr', + redirectUri: 'https://zapier.com/dashboard/auth/oauth/return/RocketChatDevAPI/', + _createdAt: new Date, + _createdBy: { + _id: 'system', + username: 'system' + } + }); +} diff --git a/packages/rocketchat-oauth2-server-config/oauth/server/oauth2-server.coffee b/packages/rocketchat-oauth2-server-config/oauth/server/oauth2-server.coffee deleted file mode 100644 index 2a5abda75c9baa7037730d88c5d82842d0a9ba27..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oauth2-server-config/oauth/server/oauth2-server.coffee +++ /dev/null @@ -1,82 +0,0 @@ -oauth2server = new OAuth2Server - accessTokensCollectionName: 'rocketchat_oauth_access_tokens' - refreshTokensCollectionName: 'rocketchat_oauth_refresh_tokens' - authCodesCollectionName: 'rocketchat_oauth_auth_codes' - clientsCollection: RocketChat.models.OAuthApps.model - debug: true - - -WebApp.connectHandlers.use oauth2server.app - -oauth2server.routes.get '/oauth/userinfo', (req, res, next) -> - if not req.headers.authorization? - return res.sendStatus(401).send('No token') - - accessToken = req.headers.authorization.replace('Bearer ', '') - - token = oauth2server.oauth.model.AccessTokens.findOne accessToken: accessToken - - if not token? - return res.sendStatus(401).send('Invalid Token') - - user = RocketChat.models.Users.findOneById(token.userId); - - if not user? - return res.sendStatus(401).send('Invalid Token') - - res.send - sub: user._id - name: user.name - email: user.emails[0].address - email_verified: user.emails[0].verified - department: "" - birthdate: "" - preffered_username: user.username - updated_at: user._updatedAt - picture: "#{Meteor.absoluteUrl()}avatar/#{user.username}" - - -Meteor.publish 'oauthClient', (clientId) -> - unless @userId - return @ready() - - return RocketChat.models.OAuthApps.find {clientId: clientId, active: true}, - fields: - name: 1 - - -RocketChat.API.v1.addAuthMethod -> - headerToken = @request.headers['authorization'] - getToken = @request.query.access_token - - if headerToken? - if matches = headerToken.match(/Bearer\s(\S+)/) - headerToken = matches[1] - else - headerToken = undefined - - bearerToken = headerToken or getToken - - if not bearerToken? - # console.log 'token not found'.red - return - - # console.log 'bearerToken', bearerToken - - getAccessToken = Meteor.wrapAsync oauth2server.oauth.model.getAccessToken, oauth2server.oauth.model - accessToken = getAccessToken bearerToken - - if not accessToken? - # console.log 'accessToken not found'.red - return - - if accessToken.expires? and accessToken.expires isnt 0 and accessToken.expires < new Date() - # console.log 'accessToken expired'.red - return - - user = RocketChat.models.Users.findOne(accessToken.userId) - if not user? - # console.log 'user not found'.red - return - - return user: _.omit(user, '$loki') diff --git a/packages/rocketchat-oauth2-server-config/oauth/server/oauth2-server.js b/packages/rocketchat-oauth2-server-config/oauth/server/oauth2-server.js new file mode 100644 index 0000000000000000000000000000000000000000..a93f9ae4baf04d62f4f01b69164637e7f00844b0 --- /dev/null +++ b/packages/rocketchat-oauth2-server-config/oauth/server/oauth2-server.js @@ -0,0 +1,83 @@ +/*global OAuth2Server */ + +const oauth2server = new OAuth2Server({ + accessTokensCollectionName: 'rocketchat_oauth_access_tokens', + refreshTokensCollectionName: 'rocketchat_oauth_refresh_tokens', + authCodesCollectionName: 'rocketchat_oauth_auth_codes', + clientsCollection: RocketChat.models.OAuthApps.model, + debug: true +}); + +WebApp.connectHandlers.use(oauth2server.app); + +oauth2server.routes.get('/oauth/userinfo', function(req, res) { + if (req.headers.authorization == null) { + return res.sendStatus(401).send('No token'); + } + const accessToken = req.headers.authorization.replace('Bearer ', ''); + const token = oauth2server.oauth.model.AccessTokens.findOne({ + accessToken + }); + if (token == null) { + return res.sendStatus(401).send('Invalid Token'); + } + const user = RocketChat.models.Users.findOneById(token.userId); + if (user == null) { + return res.sendStatus(401).send('Invalid Token'); + } + return res.send({ + sub: user._id, + name: user.name, + email: user.emails[0].address, + email_verified: user.emails[0].verified, + department: '', + birthdate: '', + preffered_username: user.username, + updated_at: user._updatedAt, + picture: `${ Meteor.absoluteUrl() }avatar/${ user.username }` + }); +}); + +Meteor.publish('oauthClient', function(clientId) { + if (!this.userId) { + return this.ready(); + } + return RocketChat.models.OAuthApps.find({ + clientId, + active: true + }, { + fields: { + name: 1 + } + }); +}); + +RocketChat.API.v1.addAuthMethod(function() { + let headerToken = this.request.headers['authorization']; + const getToken = this.request.query.access_token; + if (headerToken != null) { + const matches = headerToken.match(/Bearer\s(\S+)/); + if (matches) { + headerToken = matches[1]; + } else { + headerToken = undefined; + } + } + const bearerToken = headerToken || getToken; + if (bearerToken == null) { + return; + } + const getAccessToken = Meteor.wrapAsync(oauth2server.oauth.model.getAccessToken, oauth2server.oauth.model); + const accessToken = getAccessToken(bearerToken); + if (accessToken == null) { + return; + } + if ((accessToken.expires != null) && accessToken.expires !== 0 && accessToken.expires < new Date()) { + return; + } + const user = RocketChat.models.Users.findOne(accessToken.userId); + if (user == null) { + return; + } + return { user: _.omit(user, '$loki') }; +}); diff --git a/packages/rocketchat-oauth2-server-config/package.js b/packages/rocketchat-oauth2-server-config/package.js index 1123513df50055a6637073513b43ae55d1a675d8..6fa2af8525d2447fef53e01b327395a5647f16fc 100644 --- a/packages/rocketchat-oauth2-server-config/package.js +++ b/packages/rocketchat-oauth2-server-config/package.js @@ -6,7 +6,6 @@ Package.describe({ Package.onUse(function(api) { api.use('webapp'); - api.use('coffeescript'); api.use('mongo'); api.use('ecmascript'); api.use('rocketchat:lib'); @@ -20,33 +19,33 @@ Package.onUse(function(api) { //// General // // Server - api.addFiles('server/models/OAuthApps.coffee', 'server'); + api.addFiles('server/models/OAuthApps.js', 'server'); //// OAuth // // Server - api.addFiles('oauth/server/oauth2-server.coffee', 'server'); - api.addFiles('oauth/server/default-services.coffee', 'server'); + api.addFiles('oauth/server/oauth2-server.js', 'server'); + api.addFiles('oauth/server/default-services.js', 'server'); api.addFiles('oauth/client/stylesheets/oauth2.less', 'client'); // Client api.addFiles('oauth/client/oauth2-client.html', 'client'); - api.addFiles('oauth/client/oauth2-client.coffee', 'client'); + api.addFiles('oauth/client/oauth2-client.js', 'client'); //// Admin // // Client - api.addFiles('admin/client/startup.coffee', 'client'); - api.addFiles('admin/client/collection.coffee', 'client'); - api.addFiles('admin/client/route.coffee', 'client'); + api.addFiles('admin/client/startup.js', 'client'); + api.addFiles('admin/client/collection.js', 'client'); + api.addFiles('admin/client/route.js', 'client'); api.addFiles('admin/client/views/oauthApp.html', 'client'); - api.addFiles('admin/client/views/oauthApp.coffee', 'client'); + api.addFiles('admin/client/views/oauthApp.js', 'client'); api.addFiles('admin/client/views/oauthApps.html', 'client'); - api.addFiles('admin/client/views/oauthApps.coffee', 'client'); + api.addFiles('admin/client/views/oauthApps.js', 'client'); // Server - api.addFiles('admin/server/publications/oauthApps.coffee', 'server'); - api.addFiles('admin/server/methods/addOAuthApp.coffee', 'server'); - api.addFiles('admin/server/methods/updateOAuthApp.coffee', 'server'); - api.addFiles('admin/server/methods/deleteOAuthApp.coffee', 'server'); + api.addFiles('admin/server/publications/oauthApps.js', 'server'); + api.addFiles('admin/server/methods/addOAuthApp.js', 'server'); + api.addFiles('admin/server/methods/updateOAuthApp.js', 'server'); + api.addFiles('admin/server/methods/deleteOAuthApp.js', 'server'); }); diff --git a/packages/rocketchat-oauth2-server-config/server/models/OAuthApps.coffee b/packages/rocketchat-oauth2-server-config/server/models/OAuthApps.coffee deleted file mode 100644 index 62bc4b7db342114733bb19c3c666e03d7e122bd9..0000000000000000000000000000000000000000 --- a/packages/rocketchat-oauth2-server-config/server/models/OAuthApps.coffee +++ /dev/null @@ -1,13 +0,0 @@ -RocketChat.models.OAuthApps = new class extends RocketChat.models._Base - constructor: -> - super('oauth_apps') - - - # FIND - # findByRole: (role, options) -> - # query = - # roles: role - - # return @find query, options - - # CREATE diff --git a/packages/rocketchat-oauth2-server-config/server/models/OAuthApps.js b/packages/rocketchat-oauth2-server-config/server/models/OAuthApps.js new file mode 100644 index 0000000000000000000000000000000000000000..46c70d55f9eb7346abffebbd45d377ee6bb36e9b --- /dev/null +++ b/packages/rocketchat-oauth2-server-config/server/models/OAuthApps.js @@ -0,0 +1,17 @@ +RocketChat.models.OAuthApps = new class extends RocketChat.models._Base { + constructor() { + super('oauth_apps'); + } +}; + + + + + // FIND + // findByRole: (role, options) -> + // query = + // roles: role + + // return @find query, options + + // CREATE diff --git a/packages/rocketchat-postcss/.npm/plugin/minifier-postcss/.gitignore b/packages/rocketchat-postcss/.npm/plugin/minifier-postcss/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3c3629e647f5ddf82548912e337bea9826b434af --- /dev/null +++ b/packages/rocketchat-postcss/.npm/plugin/minifier-postcss/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/rocketchat-postcss/.npm/plugin/minifier-postcss/README b/packages/rocketchat-postcss/.npm/plugin/minifier-postcss/README new file mode 100644 index 0000000000000000000000000000000000000000..3d492553a438e46facd411cd3e206a648395a38c --- /dev/null +++ b/packages/rocketchat-postcss/.npm/plugin/minifier-postcss/README @@ -0,0 +1,7 @@ +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/rocketchat-postcss/.npm/plugin/minifier-postcss/npm-shrinkwrap.json b/packages/rocketchat-postcss/.npm/plugin/minifier-postcss/npm-shrinkwrap.json new file mode 100644 index 0000000000000000000000000000000000000000..7f67f9efde11044465af6b8296f526030c5c3835 --- /dev/null +++ b/packages/rocketchat-postcss/.npm/plugin/minifier-postcss/npm-shrinkwrap.json @@ -0,0 +1,71 @@ +{ + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "from": "ansi-regex@>=2.0.0 <3.0.0" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "from": "ansi-styles@>=2.2.1 <3.0.0" + }, + "app-module-path": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/app-module-path/-/app-module-path-2.2.0.tgz", + "from": "app-module-path@2.2.0" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "from": "chalk@>=1.1.3 <2.0.0", + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "from": "supports-color@>=2.0.0 <3.0.0" + } + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "from": "escape-string-regexp@>=1.0.2 <2.0.0" + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "from": "has-ansi@>=2.0.0 <3.0.0" + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "from": "has-flag@>=1.0.0 <2.0.0" + }, + "js-base64": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.1.9.tgz", + "from": "js-base64@>=2.1.9 <3.0.0" + }, + "postcss": { + "version": "5.2.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", + "from": "postcss@5.2.17" + }, + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "from": "source-map@0.5.6" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "from": "strip-ansi@>=3.0.0 <4.0.0" + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "from": "supports-color@>=3.2.3 <4.0.0" + } + } +} diff --git a/packages/rocketchat-postcss/package.js b/packages/rocketchat-postcss/package.js new file mode 100644 index 0000000000000000000000000000000000000000..181c5f0a48f5e77600b86f228314bf665e0d2cf7 --- /dev/null +++ b/packages/rocketchat-postcss/package.js @@ -0,0 +1,25 @@ +Package.describe({ + summary: 'Minifier for Meteor with PostCSS processing', + version: '1.0.0', + name: 'rocketchat:postcss' +}); + +Package.registerBuildPlugin({ + name: 'minifier-postcss', + use: [ + 'ecmascript', + 'minifier-css' + ], + npmDependencies: { + 'app-module-path': '2.2.0', + 'postcss': '5.2.17', + 'source-map': '0.5.6' + }, + sources: [ + 'plugin/minify-css.js' + ] +}); + +Package.onUse(function(api) { + api.use('isobuild:minifier-plugin'); +}); diff --git a/packages/rocketchat-postcss/plugin/minify-css.js b/packages/rocketchat-postcss/plugin/minify-css.js new file mode 100644 index 0000000000000000000000000000000000000000..e102bfe1d96a28edf01b17f07cc883f41babf096 --- /dev/null +++ b/packages/rocketchat-postcss/plugin/minify-css.js @@ -0,0 +1,213 @@ +/* global CssTools */ + +import appModulePath from 'app-module-path'; +import Future from 'fibers/future'; +import fs from 'fs'; +import path from 'path'; +import postCSS from 'postcss'; +import sourcemap from 'source-map'; + +appModulePath.addPath(`${ process.cwd() }/node_modules/`); + +const postCSSConfigFile = path.resolve(process.cwd(), '.postcssrc'); + +const postCSSConfig = JSON.parse(fs.readFileSync(postCSSConfigFile)); + +const getPostCSSPlugins = () => { + const plugins = []; + if (postCSSConfig.plugins) { + Object.keys(postCSSConfig.plugins).forEach((pluginName) => { + const postCSSPlugin = Npm.require(pluginName); + if (postCSSPlugin && postCSSPlugin.name === 'creator' && postCSSPlugin().postcssPlugin) { + plugins.push(postCSSPlugin(postCSSConfig.plugins ? postCSSConfig.plugins[pluginName] : {})); + } + }); + } + + return plugins; +}; + +const getPostCSSParser = () => { + if (postCSSConfig.parser) { + return Npm.require(postCSSConfig.parser); + } + + return false; +}; + +const getExcludedPackages = () => { + if (postCSSConfig.excludedPackages && postCSSConfig.excludedPackages instanceof Array) { + return postCSSConfig.excludedPackages; + } + + return false; +}; + +const isNotInExcludedPackages = (excludedPackages, pathInBundle) => { + let exclArr = []; + if (excludedPackages && excludedPackages instanceof Array) { + exclArr = excludedPackages.map(packageName => { + return pathInBundle && pathInBundle.indexOf(`packages/${ packageName.replace(':', '_') }`) > -1; + }); + } + + return exclArr.indexOf(true) === -1; +}; + +const isNotImport = inputFileUrl => !(/\.import\.css$/.test(inputFileUrl) || /(?:^|\/)imports\//.test(inputFileUrl)); + +const mergeCss = css => { + const originals = {}; + const excludedPackagesArr = getExcludedPackages(); + + const cssAsts = css.map(file => { + const filename = file.getPathInBundle(); + originals[filename] = file; + + const f = new Future; + + let css; + let postres; + const isFileForPostCSS = isNotInExcludedPackages(excludedPackagesArr, file.getPathInBundle()); + + postCSS(isFileForPostCSS ? getPostCSSPlugins() : []) + .process(file.getContentsAsString(), { + from: process.cwd() + file._source.url.replace('_', '-'), + parser: getPostCSSParser() + }) + .then(result => { + result.warnings().forEach(warn => { + process.stderr.write(warn.toString()); + }); + f.return(result); + }) + .catch(error => { + if (error.name === 'CssSyntaxError') { + error.message = `${ error.message }\n\nCss Syntax Error.\n\n${ error.message }${ error.showSourceCode() }`; + } + f.return(error); + }); + + try { + const parseOptions = { + source: filename, + position: true + }; + + postres = f.wait(); + + if (postres.name === 'CssSyntaxError') { + throw postres; + } + + css = postres.css; + + const ast = CssTools.parseCss(css, parseOptions); + ast.filename = filename; + + return ast; + } catch (e) { + if (e.name === 'CssSyntaxError') { + file.error({ + message: e.message, + line: e.line, + column: e.column + }); + } else if (e.reason) { + file.error({ + message: e.reason, + line: e.line, + column: e.column + }); + } else { + file.error({ + message: e.message + }); + } + + return { + type: 'stylesheet', + stylesheet: { rules: [] }, + filename + }; + } + }); + + const mergedCssAst = CssTools.mergeCssAsts(cssAsts, (filename, msg) => { + console.log(`${ filename }: warn: ${ msg }`); + }); + + const stringifiedCss = CssTools.stringifyCss(mergedCssAst, { + sourcemap: true, + inputSourcemaps: false + }); + + if (!stringifiedCss.code) { + return { code: '' }; + } + + stringifiedCss.map.sourcesContent = + stringifiedCss.map.sources.map(filename => { +