diff --git a/.changeset/all-baths-cry.md b/.changeset/all-baths-cry.md new file mode 100644 index 0000000000000..8dc92088b6b5f --- /dev/null +++ b/.changeset/all-baths-cry.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/model-typings': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/models': minor +'@rocket.chat/meteor': minor +--- + +Adds `current` field to `DeviceManagementSession` type and `currentLoginToken` parameter to `aggregateSessionsByUserId`, allowing the sessions endpoint to identify and flag the caller's active session. diff --git a/.changeset/atomic-model-operations.md b/.changeset/atomic-model-operations.md new file mode 100644 index 0000000000000..4e05f12719360 --- /dev/null +++ b/.changeset/atomic-model-operations.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/meteor': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/model-typings': patch +'@rocket.chat/models': patch +--- + +Fixes race conditions in several check-then-write database flows by collapsing them into single atomic operations: CAS login tokens can no longer be consumed by two concurrent logins, revoking a room invite no longer emits duplicate removal notifications, and deleting an integration now enforces the creator-only permission scope in the delete itself diff --git a/.changeset/better-results-press.md b/.changeset/better-results-press.md new file mode 100644 index 0000000000000..c4e2d5b7d97ba --- /dev/null +++ b/.changeset/better-results-press.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": major +--- + +Removes `/ufs` legacy endpoint for downloading files diff --git a/.changeset/block-fallback-rendering.md b/.changeset/block-fallback-rendering.md new file mode 100644 index 0000000000000..4b6ef1733355d --- /dev/null +++ b/.changeset/block-fallback-rendering.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/gazzodown": patch +--- + +Degrades blocks without a dedicated renderer to their raw markup instead of dropping them. When a block carries a `fallback` `[start, end]` offset span, `Markup`/`PreviewMarkup` slice the original message source (passed via the new optional `source` prop) and render that text. This avoids duplicating the markup into the AST while keeping unsupported blocks visible. diff --git a/.changeset/breezy-moons-search.md b/.changeset/breezy-moons-search.md new file mode 100644 index 0000000000000..b73abc9211ff8 --- /dev/null +++ b/.changeset/breezy-moons-search.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes own account showing twice in navbar room search when searching by username diff --git a/.changeset/breezy-parts-kiss.md b/.changeset/breezy-parts-kiss.md new file mode 100644 index 0000000000000..082079b663c60 --- /dev/null +++ b/.changeset/breezy-parts-kiss.md @@ -0,0 +1,31 @@ +--- +'@rocket.chat/web-ui-registration': major +'@rocket.chat/model-typings': major +'@rocket.chat/core-typings': major +'@rocket.chat/rest-typings': major +'@rocket.chat/passport-x': major +'@rocket.chat/models': major +'@rocket.chat/i18n': major +'@rocket.chat/meteor': major +--- + +## Phishing-Resistant Multi-Factor Authentication + +Introduces a more secure and reliable server-side OAuth authentication flow. + +### Whatโ€™s New + +- **Improved OAuth login security** + OAuth authentication now happens fully on the server, reducing the risk of token theft, phishing attacks, and client-side credential interception. + +- **Built-in CSRF, state validation, and PKCE protection** + OAuth logins now include stronger protection against CSRF attacks, request tampering, and authorization code interception through secure state validation and PKCE support. + +- **Improved two-step verification with OAuth logins** + Users with email or TOTP two-factor authentication enabled will now be asked to complete 2FA even when signing in with providers like Google, GitHub, GitLab, and others. + +- **Improved mobile & desktop app login** + Mobile and desktop apps now support a smoother and more secure deep-link OAuth login flow. + +- **A new setting to enable/disable new OAuth Flow** + Enable this new setting `Accounts_OAuth_Use_Modern_Flow` to use all of the above mentioned features. diff --git a/.changeset/code-fence-trailing-backtick.md b/.changeset/code-fence-trailing-backtick.md new file mode 100644 index 0000000000000..4101b46273bcd --- /dev/null +++ b/.changeset/code-fence-trailing-backtick.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/message-parser": patch +--- + +Fixes code fences failing to render when a line inside them ends with an inline-code backtick (e.g. `` - **Node**: `22.22.3` ``). A trailing backtick immediately before a line break could not be consumed as content, causing the whole ```` ``` ```` block to fall back to markdown parsing and split apart. Trailing 1-2 backticks before a line end (or EOF) are now treated as code content. diff --git a/.changeset/deep-ways-poke.md b/.changeset/deep-ways-poke.md new file mode 100644 index 0000000000000..918b70379880e --- /dev/null +++ b/.changeset/deep-ways-poke.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/core-typings': patch +'@rocket.chat/models': patch +'@rocket.chat/meteor': patch +--- + +Fixes the Slack importer storing shared files as raw URLs in the message body. Imported file messages now stay hidden until "Download Pending Files" button fetches them, then display as native attachments with image previews. Failed downloads (e.g. invalidated export links) are no longer silently saved as the file's content โ€” they are counted as errors and can be retried. diff --git a/.changeset/desktop-set-user-roles.md b/.changeset/desktop-set-user-roles.md new file mode 100644 index 0000000000000..369f18c00b4de --- /dev/null +++ b/.changeset/desktop-set-user-roles.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/desktop-api': minor +'@rocket.chat/meteor': patch +--- + +Added a `setUserRoles` bridge method to the desktop API and pushed the logged-in user's roles to the desktop app. This lets the desktop client restrict supportedVersions messages (such as version-expiration warnings) to specific roles like admins, instead of showing them to every user. The push is reactive to role changes; desktop builds without the bridge method fall back to their own role lookup. diff --git a/.changeset/emoji-zwj-tag-sequences.md b/.changeset/emoji-zwj-tag-sequences.md new file mode 100644 index 0000000000000..e649ef534bd12 --- /dev/null +++ b/.changeset/emoji-zwj-tag-sequences.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/message-parser': patch +--- + +Fixes an issue in which some combined emojis like ๐Ÿ˜ถโ€๐ŸŒซ๏ธ, ๐Ÿ˜ฎโ€๐Ÿ’จ and ๐Ÿ˜ตโ€๐Ÿ’ซ were being displayed as two separate emojis, and the flags of some countries like England ๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ, Scotland ๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ and Wales ๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ were being displayed as a plain black flag diff --git a/.changeset/empty-boxes-cross.md b/.changeset/empty-boxes-cross.md new file mode 100644 index 0000000000000..532d9a9e287cc --- /dev/null +++ b/.changeset/empty-boxes-cross.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes engagement dashboard loading unnecessary data into memory on startup diff --git a/.changeset/empty-garlics-reply.md b/.changeset/empty-garlics-reply.md new file mode 100644 index 0000000000000..6233cf9bf525e --- /dev/null +++ b/.changeset/empty-garlics-reply.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/core-typings': patch +'@rocket.chat/meteor': patch +--- + +Fixes the password policy allowing a maximum length lower than the minimum length to be saved โ€” a combination that made it impossible to set any valid password. The server now rejects such configurations when password policy settings are saved and shows an error explaining the constraint. diff --git a/.changeset/fancy-days-knock.md b/.changeset/fancy-days-knock.md new file mode 100644 index 0000000000000..eacb88108a0f7 --- /dev/null +++ b/.changeset/fancy-days-knock.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/.changeset/fancy-deserts-mate.md b/.changeset/fancy-deserts-mate.md new file mode 100644 index 0000000000000..7f400fd7cfcca --- /dev/null +++ b/.changeset/fancy-deserts-mate.md @@ -0,0 +1,10 @@ +--- +'@rocket.chat/core-services': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/ai-search': minor +'@rocket.chat/ui-client': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Adds AI Search with semantic message results, optional OpenAI-compatible answers, and AI Center configuration. diff --git a/.changeset/fifty-candies-heal.md b/.changeset/fifty-candies-heal.md new file mode 100644 index 0000000000000..a67e4d5f376e5 --- /dev/null +++ b/.changeset/fifty-candies-heal.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: Buttons from emoji picker misbehaving on clicks + +An infinite render loop was preventing proper behavior when clicking on the emoji picker buttons. It was fixed by removing the unnecessary state update that was causing the loop and replacing multiple fires of the same mouseover event (when a mouseenter event was the right one to use). There is a chance this pre-existing bug was hidden by React 18's event delegation. diff --git a/.changeset/fips-mode-support.md b/.changeset/fips-mode-support.md new file mode 100644 index 0000000000000..56c45fc870e88 --- /dev/null +++ b/.changeset/fips-mode-support.md @@ -0,0 +1,13 @@ +--- +'@rocket.chat/meteor': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/federation-matrix': minor +'@rocket.chat/account-service': minor +'@rocket.chat/authorization-service': minor +'@rocket.chat/ddp-streamer': minor +'@rocket.chat/omnichannel-transcript': minor +'@rocket.chat/presence-service': minor +'@rocket.chat/queue-worker': minor +--- + +Adds support for running Rocket.Chat in FIPS mode. The monolith and all microservices (ddp-streamer, account-service, authorization-service, presence-service, queue-worker, omnichannel-transcript) can now enforce FIPS-compliant cryptography via Node.js/OpenSSL FIPS, with dedicated FIPS Docker images. Running in FIPS mode requires a license including the new `fips` module, and FIPS status is now reported in server logs and statistics. diff --git a/.changeset/fix-business-hour-agents-availability.md b/.changeset/fix-business-hour-agents-availability.md new file mode 100644 index 0000000000000..ce800f8acaabe --- /dev/null +++ b/.changeset/fix-business-hour-agents-availability.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes agents' business-hour availability not being updated when they are removed from a department linked to a business hour while multiple business hours are enabled. The recomputation step always failed, leaving removed agents available (or unavailable) according to a business hour that no longer applied to them โ€” and, on deployments running with `EXIT_UNHANDLEDPROMISEREJECTION` (or in development/test mode), the unhandled rejection crashed the server process. diff --git a/.changeset/fix-business-hour-dst-department-unlink.md b/.changeset/fix-business-hour-dst-department-unlink.md new file mode 100644 index 0000000000000..b5eb7acbfe962 --- /dev/null +++ b/.changeset/fix-business-hour-dst-department-unlink.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes multiple business hours losing their linked departments after a daylight saving time change or a server restart. The automatic timezone adjustment re-saved business hours without their department associations, causing business hours configured with timezones to silently stop applying to agents. + +Also fixes agents keeping a business hour's availability after their department was removed from it: saving a business hour with a smaller department list unlinked the departments but never cleared the business hour from the removed departments' agents. diff --git a/.changeset/fix-deno-runtime-symlink.md b/.changeset/fix-deno-runtime-symlink.md new file mode 100644 index 0000000000000..b59e781c3c4ea --- /dev/null +++ b/.changeset/fix-deno-runtime-symlink.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/apps': patch +--- + +Fixes an issue that would cause apps to fail with 'Module not found' errors in some cases diff --git a/.changeset/fix-license-env-precedence.md b/.changeset/fix-license-env-precedence.md new file mode 100644 index 0000000000000..c2cc0f1ee51c9 --- /dev/null +++ b/.changeset/fix-license-env-precedence.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/meteor': patch +'@rocket.chat/license': patch +--- + +Fixes the license provided via the `ROCKETCHAT_LICENSE` environment variable not being applied when it is newer than the one persisted in the workspace. diff --git a/.changeset/flat-poets-cheat.md b/.changeset/flat-poets-cheat.md new file mode 100644 index 0000000000000..2560e2d3ee172 --- /dev/null +++ b/.changeset/flat-poets-cheat.md @@ -0,0 +1,28 @@ +--- +'@rocket.chat/web-ui-registration': minor +'@rocket.chat/model-typings': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/desktop-api': minor +'@rocket.chat/models': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +## Phishing-Resistant Multi-Factor Authentication + +Introduces a more secure and reliable server-side OAuth authentication flow. + +### Whatโ€™s New + +- **Improved OAuth login security** + OAuth authentication now happens fully on the server, reducing the risk of token theft, phishing attacks, and client-side credential interception. + +- **Built-in CSRF, state validation, and PKCE protection** + OAuth logins now include stronger protection against CSRF attacks, request tampering, and authorization code interception through secure state validation and PKCE support. + +- **Improved two-step verification with OAuth logins** + Users with email or TOTP two-factor authentication enabled will now be asked to complete 2FA even when signing in with providers like Google, GitHub, GitLab, and others. + +- **Improved mobile & desktop app login** + Mobile and desktop apps now support a smoother and more secure deep-link OAuth login flow. diff --git a/.changeset/forty-stars-bathe.md b/.changeset/forty-stars-bathe.md new file mode 100644 index 0000000000000..00f21597c8a50 --- /dev/null +++ b/.changeset/forty-stars-bathe.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes error message being shown when logging out current device via Device Management despite successful logout. diff --git a/.changeset/four-tigers-clap.md b/.changeset/four-tigers-clap.md new file mode 100644 index 0000000000000..aa6555d3a0d6c --- /dev/null +++ b/.changeset/four-tigers-clap.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/models': patch +'@rocket.chat/model-typings': patch +'@rocket.chat/meteor': patch +--- + +Fixes a race condition that left messages permanently undecryptable ("incorrect encryption key") in rooms created with encryption enabled. When several members opened such a room at the same time, each client could independently generate and distribute a different group key. Establishing the room key is now atomic (first-write-wins) on the server, and a client that loses the race discards its locally generated key and adopts the established one instead of encrypting with a divergent key. diff --git a/.changeset/fruity-items-fix.md b/.changeset/fruity-items-fix.md new file mode 100644 index 0000000000000..dd9b9d290b81a --- /dev/null +++ b/.changeset/fruity-items-fix.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/apps': minor +'@rocket.chat/meteor': minor +--- + +Adds an alternative runtime runner for apps. It can be enabled via environment variable `APPS_ENGINE_RUNTIME_BACKEND='node'` diff --git a/.changeset/gentle-cats-change.md b/.changeset/gentle-cats-change.md new file mode 100644 index 0000000000000..da4f32ad30a64 --- /dev/null +++ b/.changeset/gentle-cats-change.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/meteor': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/model-typings': patch +'@rocket.chat/models': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/.changeset/integration-scripts-no-babel.md b/.changeset/integration-scripts-no-babel.md new file mode 100644 index 0000000000000..0e962b99bd059 --- /dev/null +++ b/.changeset/integration-scripts-no-babel.md @@ -0,0 +1,13 @@ +--- +'@rocket.chat/meteor': major +--- + +**Breaking:** Stopped transpiling webhook integration scripts with Babel. Scripts now run as-is inside `isolated-vm` (modern V8). + +Class method bodies are now in strict mode per the ES2015 spec. Scripts that relied on sloppy-mode behaviors provided by the previous Babel transpilation must be updated: + +- **Implicit globals** โ€” `msg = buildMessage(...)` inside a class method now throws `ReferenceError`. Add `let`, `const`, or `var`. +- **`this` in nested regular functions** โ€” `function helper() { this.JSON.stringify(...) }` now has `this === undefined` instead of `globalThis`. Use arrow functions or pass the dependency explicitly. +- **`arguments.callee`** โ€” Throws `TypeError`. Use a named function expression instead. +- **Octal literals** โ€” `0777` is now a `SyntaxError`. Use `0o777`. +- **Duplicate parameter names** โ€” `function(a, a) {}` is now a `SyntaxError`. diff --git a/.changeset/json-setting-validation-feedback.md b/.changeset/json-setting-validation-feedback.md new file mode 100644 index 0000000000000..acbf24c507007 --- /dev/null +++ b/.changeset/json-setting-validation-feedback.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Adds inline JSON validation feedback to admin settings that hold JSON (`code: application/json`), showing an error in the editor and blocking save while the value is malformed diff --git a/.changeset/ldap-channel-sync-removal-abort.md b/.changeset/ldap-channel-sync-removal-abort.md new file mode 100644 index 0000000000000..36b86b01bd171 --- /dev/null +++ b/.changeset/ldap-channel-sync-removal-abort.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes LDAP channel sync aborting the entire add/removal pass when a mapped channel could not be resolved, which prevented users from being removed from channels when "Auto Remove Users from Channels" was enabled. diff --git a/.changeset/ldap-merge-email-lookup.md b/.changeset/ldap-merge-email-lookup.md new file mode 100644 index 0000000000000..58f788eb95e70 --- /dev/null +++ b/.changeset/ldap-merge-email-lookup.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/models': patch +'@rocket.chat/meteor': patch +--- + +Fixes LDAP sync failing to merge an existing user matched by email, which caused a `Username already exists` error when the user's username differed from the directory. diff --git a/.changeset/ldap-sync-crash-invalid-filter.md b/.changeset/ldap-sync-crash-invalid-filter.md new file mode 100644 index 0000000000000..cecdbba85e822 --- /dev/null +++ b/.changeset/ldap-sync-crash-invalid-filter.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the server crashing during LDAP login or sync when the configured search settings produce an invalid LDAP filter (for example, an empty User Search Field). The operation now fails gracefully with a logged error instead of terminating the process. diff --git a/.changeset/license-validate-preview.md b/.changeset/license-validate-preview.md new file mode 100644 index 0000000000000..e3dd66c0eab86 --- /dev/null +++ b/.changeset/license-validate-preview.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/license': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Adds a new `licenses.validate` REST endpoint that validates a Rocket.Chat license (V2 or V3 JWT) against the current workspace without applying it, so a license can be previewed before it is applied from the UI. A valid license responds with success; an invalid one responds with the validation behaviors that rejected it. diff --git a/.changeset/light-geckos-start.md b/.changeset/light-geckos-start.md new file mode 100644 index 0000000000000..4730b2b67b0d0 --- /dev/null +++ b/.changeset/light-geckos-start.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Disables more actions on message composer during public channel preview. diff --git a/.changeset/livechat-forward-ul-message.md b/.changeset/livechat-forward-ul-message.md new file mode 100644 index 0000000000000..afe2bee7c0222 --- /dev/null +++ b/.changeset/livechat-forward-ul-message.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where the "user left" system message could be added to an Omnichannel conversation only after the forwarding process had already finished, causing it to appear out of order in the conversation history diff --git a/.changeset/message-content-body-source.md b/.changeset/message-content-body-source.md new file mode 100644 index 0000000000000..439b1a1d6ada2 --- /dev/null +++ b/.changeset/message-content-body-source.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Passes the original message text to the message renderer so blocks without a dedicated renderer (e.g. tables on clients that don't render them yet) can degrade to their raw markup via the parser's `fallback` source offsets, instead of disappearing. diff --git a/.changeset/moody-eggs-juggle.md b/.changeset/moody-eggs-juggle.md new file mode 100644 index 0000000000000..1324f520d0d2c --- /dev/null +++ b/.changeset/moody-eggs-juggle.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where the cursor jumped to the wrong position after inserting a mention at the start or middle of a message. diff --git a/.changeset/nice-fans-cover.md b/.changeset/nice-fans-cover.md new file mode 100644 index 0000000000000..8ac90e87c5a87 --- /dev/null +++ b/.changeset/nice-fans-cover.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Replaces proprietary emojis with native (unicode) emojis and increases available emoji set diff --git a/.changeset/offline-license-no-egress.md b/.changeset/offline-license-no-egress.md new file mode 100644 index 0000000000000..43b8259ea417f --- /dev/null +++ b/.changeset/offline-license-no-egress.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/meteor': minor +'@rocket.chat/license': minor +--- + +Adds support for the `offline` license flag, suppressing every outbound connection to Rocket.Chat Cloud services and the Push Gateway at its source, so air-gapped workspaces never initiate calls that would violate their security compliance. diff --git a/.changeset/old-bats-sniff.md b/.changeset/old-bats-sniff.md new file mode 100644 index 0000000000000..56d55d7b08f99 --- /dev/null +++ b/.changeset/old-bats-sniff.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/apps-engine': patch +'@rocket.chat/meteor': patch +--- + +Fixes wrong FederationLookup type assigned to IUser in apps. The correct data is there, but the type does not represent it. diff --git a/.changeset/olive-nails-fix.md b/.changeset/olive-nails-fix.md new file mode 100644 index 0000000000000..7a4cf2c364f76 --- /dev/null +++ b/.changeset/olive-nails-fix.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Enables keyboard activation for Display menu radio buttons and checkboxes diff --git a/.changeset/omnichannel-contact-center-date-relative-time.md b/.changeset/omnichannel-contact-center-date-relative-time.md new file mode 100644 index 0000000000000..3da10de7b1caf --- /dev/null +++ b/.changeset/omnichannel-contact-center-date-relative-time.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Changes the date columns in the Omnichannel Contact Center to show both the formatted date and the relative time: the **Last Chat** column in the contacts list, and the **Started at** and **Last Message** columns in the chats list diff --git a/.changeset/parser-horizontal-rule.md b/.changeset/parser-horizontal-rule.md new file mode 100644 index 0000000000000..366209c9b7788 --- /dev/null +++ b/.changeset/parser-horizontal-rule.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/message-parser": minor +"@rocket.chat/gazzodown": minor +--- + +Adds support for horizontal rules (thematic breaks) in the message parser. A line of 3 or more contiguous dashes (`---`, with nothing else on the line) is parsed into a new `HORIZONTAL_RULE` block node and rendered with Fuselage's `Divider`. The node carries an optional `fallback` โ€” a `[start, end]` offset span into the original source โ€” so renderers without horizontal-rule support can slice the source to show the raw markup instead of dropping it, without duplicating the text into the AST. Only `-` is accepted: CommonMark also allows `*` and `_`, but those collide with emphasis and with censored words (bad-words masks a term as a run of `*`), so a bare `***` / `_______` line stays text/emphasis instead of becoming a divider. diff --git a/.changeset/persistent-audio-player.md b/.changeset/persistent-audio-player.md new file mode 100644 index 0000000000000..15d68178c43ac --- /dev/null +++ b/.changeset/persistent-audio-player.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/meteor': minor +'@rocket.chat/i18n': minor +--- + +Added a persistent audio player. Playing an audio attachment now continues across room navigation: the audio keeps playing when you switch or close the conversation, and a "Now playing" card appears at the top of the sidebar with play/pause, seek, playback speed (1x/1.5x/2x), and a shortcut back to the originating conversation. diff --git a/.changeset/push-reenable-crash.md b/.changeset/push-reenable-crash.md new file mode 100644 index 0000000000000..28c67860a2598 --- /dev/null +++ b/.changeset/push-reenable-crash.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes a server crash when re-enabling the "Enable Push" setting after it was disabled. Push notifications are now fully reconfigured on re-enable, so settings changed while push was disabled are picked up as well. diff --git a/.changeset/quiet-lizards-jump.md b/.changeset/quiet-lizards-jump.md new file mode 100644 index 0000000000000..f9858cac0d1f9 --- /dev/null +++ b/.changeset/quiet-lizards-jump.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Ensures the `users.CreateToken` endpoint checks for the `user-generate-access-token` permission when generating a login token for another user diff --git a/.changeset/quiet-teams-guard.md b/.changeset/quiet-teams-guard.md new file mode 100644 index 0000000000000..ac9a8af178b0a --- /dev/null +++ b/.changeset/quiet-teams-guard.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Ensures room permission checks are applied consistently regardless of how the room is identified when converting a channel to a team or creating a team from an existing room diff --git a/.changeset/ready-taxis-join.md b/.changeset/ready-taxis-join.md new file mode 100644 index 0000000000000..cc8fcdb1db16c --- /dev/null +++ b/.changeset/ready-taxis-join.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Restores Away as a selectable preset in the quick status menu, custom status modal, and account profile page. diff --git a/.changeset/remove-webdav-integration.md b/.changeset/remove-webdav-integration.md new file mode 100644 index 0000000000000..f7afb3f115ee6 --- /dev/null +++ b/.changeset/remove-webdav-integration.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes the WebDAV integration. All WebDAV accounts, settings, and file-picker functionality have been removed, and existing WebDAV settings are cleared from the database on upgrade diff --git a/.changeset/rest-batch4-logout-cleanup.md b/.changeset/rest-batch4-logout-cleanup.md new file mode 100644 index 0000000000000..3382f4b694321 --- /dev/null +++ b/.changeset/rest-batch4-logout-cleanup.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Deprecates the `logoutCleanUp` DDP method and stops the client from calling it. The post-logout side effects (`afterLogoutCleanUpCallback` + `Apps.IPostUserLoggedOut`) now run server-side via a new `Accounts.onLogout` handler and from `POST /v1/users.logout`, so both DDP and REST logout paths fire them without a client round-trip. The DDP method keeps its original implementation and registration with a deprecation log pointing at `/v1/users.logout` until 9.0.0. diff --git a/.changeset/rest-cloud-connect-workspace.md b/.changeset/rest-cloud-connect-workspace.md new file mode 100644 index 0000000000000..63f5ac3751f7a --- /dev/null +++ b/.changeset/rest-cloud-connect-workspace.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Added `POST /v1/cloud.connectWorkspace` (replaces the deprecated `cloud:connectWorkspace` DDP method). Body is `{ token }`; auth-gated with `manage-cloud` permission. The legacy DDP method remains registered with a deprecation log pointing at the new route. diff --git a/.changeset/rest-integrations-history-clear-replay.md b/.changeset/rest-integrations-history-clear-replay.md new file mode 100644 index 0000000000000..4c71e723add01 --- /dev/null +++ b/.changeset/rest-integrations-history-clear-replay.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Added `POST /v1/integrations.clearHistory` and `POST /v1/integrations.replayOutgoing` (replace the deprecated `clearIntegrationHistory` and `replayOutgoingIntegration` DDP methods). Bodies `{ integrationId }` and `{ integrationId, historyId }` respectively. Permissions (`manage-outgoing-integrations` or `manage-own-outgoing-integrations`) are enforced the same way the DDP methods did. Legacy DDP methods remain registered with deprecation logs pointing at the new routes. diff --git a/.changeset/rest-permissions-add-remove-role.md b/.changeset/rest-permissions-add-remove-role.md new file mode 100644 index 0000000000000..f4a6b916a2724 --- /dev/null +++ b/.changeset/rest-permissions-add-remove-role.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Added `POST /v1/permissions.addRole` and `POST /v1/permissions.removeRole` (replace the deprecated `authorization:addPermissionToRole` and `authorization:removeRoleFromPermission` DDP methods). Body is `{ permissionId, role }` on both. The same per-user permission checks (`access-permissions`, `access-setting-permissions`) the DDP methods enforced are reused. Legacy DDP methods remain registered with deprecation logs pointing at the new routes. diff --git a/.changeset/rest-users-setavatar-service.md b/.changeset/rest-users-setavatar-service.md new file mode 100644 index 0000000000000..7d4ef483f395f --- /dev/null +++ b/.changeset/rest-users-setavatar-service.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Extended `POST /v1/users.setAvatar` to accept an optional `service` multipart field. When provided, the value is stored as the user's `avatarOrigin`, matching what the deprecated `setAvatarFromService` DDP method did. The legacy DDP method remains registered with a deprecation log pointing at the new route. diff --git a/.changeset/rest-users-verify-email.md b/.changeset/rest-users-verify-email.md new file mode 100644 index 0000000000000..ffca4eac98bff --- /dev/null +++ b/.changeset/rest-users-verify-email.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Added `POST /v1/users.verifyEmail` (replaces the two-call DDP flow of `verifyEmail` + `afterVerifyEmail`). Body is `{ token }`; the server resolves the user, marks the email verified, and runs the anonymousโ†’user role swap in a single request. The deprecated `afterVerifyEmail` DDP method keeps its registration with a deprecation log pointing at the new route. diff --git a/.changeset/saml-slo-relaystate.md b/.changeset/saml-slo-relaystate.md new file mode 100644 index 0000000000000..50b174a928786 --- /dev/null +++ b/.changeset/saml-slo-relaystate.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the SAML Single Logout response so the `RelayState` matches the exact value received on the logout request, as required by the SAML specification, instead of using Rocket.Chat's own URL diff --git a/.changeset/setup-wizard-env-override-upgrade.md b/.changeset/setup-wizard-env-override-upgrade.md new file mode 100644 index 0000000000000..5b42fd6ceb472 --- /dev/null +++ b/.changeset/setup-wizard-env-override-upgrade.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the setup wizard being forced back into the registration step on the first start after an upgrade when `OVERWRITE_SETTING_Show_Setup_Wizard=completed` is set, which affected air-gapped workspaces running offline licenses without cloud registration. diff --git a/.changeset/shaggy-hats-behave.md b/.changeset/shaggy-hats-behave.md new file mode 100644 index 0000000000000..d3b1045c1d4ac --- /dev/null +++ b/.changeset/shaggy-hats-behave.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes deleteCustomSound Meteor method diff --git a/.changeset/shaggy-pets-return.md b/.changeset/shaggy-pets-return.md new file mode 100644 index 0000000000000..96cfa3050bc17 --- /dev/null +++ b/.changeset/shaggy-pets-return.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Adds keyboard navigation support in Room members list diff --git a/.changeset/shy-actors-draw.md b/.changeset/shy-actors-draw.md new file mode 100644 index 0000000000000..c2dbc530dba55 --- /dev/null +++ b/.changeset/shy-actors-draw.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-typings": patch +--- + +Returns `custom fields` on admin rooms detail view diff --git a/.changeset/silver-cars-kneel.md b/.changeset/silver-cars-kneel.md new file mode 100644 index 0000000000000..13a3c43ef087b --- /dev/null +++ b/.changeset/silver-cars-kneel.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Adds a manage license flow to the subscription admin page, allowing license verification before applying it and an option to remove the license. Note: From this point license management should be made in subscription page instead of the Enterprise settings page. diff --git a/.changeset/spotlight-perf.md b/.changeset/spotlight-perf.md new file mode 100644 index 0000000000000..330443ed00493 --- /dev/null +++ b/.changeset/spotlight-perf.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/meteor': patch +'@rocket.chat/models': patch +--- + +Improved `/v1/spotlight` search performance: results return faster, and room searches now read from secondary database replicas when available, reducing load on the primary. diff --git a/.changeset/stale-meals-check.md b/.changeset/stale-meals-check.md new file mode 100644 index 0000000000000..650848aa1bb5e --- /dev/null +++ b/.changeset/stale-meals-check.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': major +--- + +Removes insertOrUpdateSound and uploadCustomSound Meteor methods diff --git a/.changeset/strict-find-options.md b/.changeset/strict-find-options.md new file mode 100644 index 0000000000000..97e5552c67a61 --- /dev/null +++ b/.changeset/strict-find-options.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/meteor': patch +'@rocket.chat/model-typings': patch +--- + +Fixes broken pagination on `rooms.bannedUsers` and on omnichannel department listing endpoints, which ignored the `offset` parameter and always returned results from the first page. Also reduces payload over-fetching on several endpoints that unintentionally loaded full documents (`rooms.hide`, private group lookups, direct email replies, omnichannel auto-transfer), and removes the permissive model query typings that allowed these invalid find options to compile unnoticed. diff --git a/.changeset/swift-ants-appear.md b/.changeset/swift-ants-appear.md new file mode 100644 index 0000000000000..f60a41dd957ae --- /dev/null +++ b/.changeset/swift-ants-appear.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes dates showing one day earlier for users in negative UTC-offset timezones diff --git a/.changeset/tame-tables-render.md b/.changeset/tame-tables-render.md new file mode 100644 index 0000000000000..b72369d084616 --- /dev/null +++ b/.changeset/tame-tables-render.md @@ -0,0 +1,10 @@ +--- +"@rocket.chat/message-parser": minor +"@rocket.chat/gazzodown": minor +--- + +Adds GFM-style table support to the message parser and renders it in gazzodown. + +Parser: tables require a leading and trailing pipe on every row, support column alignment via the delimiter row (`:---`, `:--:`, `---:`), and allow inline markup inside cells (a literal pipe must be escaped as `\|`). New `TABLE`, `TABLE_ROW`, and `TABLE_CELL` AST nodes are emitted. The `TABLE` node also carries an optional `fallback` โ€” a `[start, end]` offset span into the original source โ€” so renderers without table support can slice the source to show the raw markup instead of dropping it, without duplicating the text into the AST. + +Rendering: gazzodown renders these tables using Fuselage's `Table` components with per-column alignment, and shows a compact single-row preview of the table header in message previews. diff --git a/.changeset/tidy-snails-smile.md b/.changeset/tidy-snails-smile.md new file mode 100644 index 0000000000000..546ec86bd8e63 --- /dev/null +++ b/.changeset/tidy-snails-smile.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/apps": patch +--- + +Fixes app startup failing with EACCES errors in docker deployments running with specified UIDs diff --git a/.changeset/timestamp-fallback-offsets.md b/.changeset/timestamp-fallback-offsets.md new file mode 100644 index 0000000000000..59bf60364fbff --- /dev/null +++ b/.changeset/timestamp-fallback-offsets.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/message-parser": minor +--- + +Normalizes the `Timestamp` node's `fallback` to the same `[start, end]` source-offset span used by other blocks, instead of a reconstructed plain-text node. The type still allows the previous `Plain` form so already-persisted data keeps type-checking and is safely ignored at render time. diff --git a/.changeset/voip-provider-app-remount.md b/.changeset/voip-provider-app-remount.md new file mode 100644 index 0000000000000..d1081143b9e66 --- /dev/null +++ b/.changeset/voip-provider-app-remount.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/ui-voip': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue where the whole app remounts when VoIP availability changes \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 699185914302b..dfdf2c04e0810 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,6 @@ /packages/* @RocketChat/Architecture /packages/apps-engine/ @RocketChat/apps +/packages/apps/ @RocketChat/apps /packages/core-typings/ @RocketChat/Architecture /packages/rest-typings/ @RocketChat/Architecture @RocketChat/backend /packages/ui-contexts/ @RocketChat/frontend @@ -12,23 +13,30 @@ /apps/meteor/tests/e2e @RocketChat/frontend /apps/meteor/tests/end-to-end @RocketChat/backend /apps/meteor/tests/unit/app @RocketChat/backend -/apps/meteor/tests/unit/app/ui-utils @RocketChat/frontend /apps/meteor/tests/unit/client @RocketChat/frontend /apps/meteor/tests/unit/server @RocketChat/backend /apps/meteor/app/apps/ @RocketChat/apps /apps/meteor/app/livechat @RocketChat/omnichannel -/apps/meteor/app/sms @RocketChat/omnichannel -/apps/meteor/app/api @RocketChat/backend -/apps/meteor/app/federation @RocketChat/backend /apps/meteor/app/file-upload @RocketChat/backend /apps/meteor/app/integrations @RocketChat/backend /apps/meteor/server @RocketChat/backend +/apps/meteor/server/lib/omnichannel @RocketChat/omnichannel +/apps/meteor/server/hooks/omnichannel @RocketChat/omnichannel +/apps/meteor/server/api/v1/omnichannel @RocketChat/omnichannel +/apps/meteor/server/meteor-methods/omnichannel @RocketChat/omnichannel /packages/models @RocketChat/Architecture apps/meteor/server/startup/migrations @RocketChat/Architecture /apps/meteor/packages/rocketchat-livechat @RocketChat/omnichannel /apps/meteor/server/features/EmailInbox @RocketChat/omnichannel -/apps/meteor/ee/app/canned-responses @RocketChat/omnichannel -/apps/meteor/ee/app/livechat @RocketChat/omnichannel -/apps/meteor/ee/app/livechat-enterprise @RocketChat/omnichannel +/apps/meteor/ee/server/apps/ @RocketChat/apps +/apps/meteor/ee/tests/unit/server/apps/ @RocketChat/apps +/apps/meteor/ee/server/lib/canned-responses @RocketChat/omnichannel +/apps/meteor/ee/server/hooks/canned-responses @RocketChat/omnichannel +/apps/meteor/ee/server/api/v1/canned-responses.ts @RocketChat/omnichannel +/apps/meteor/ee/server/lib/omnichannel @RocketChat/omnichannel +/apps/meteor/ee/server/hooks/omnichannel @RocketChat/omnichannel +/apps/meteor/ee/server/api/v1/omnichannel @RocketChat/omnichannel +/apps/meteor/ee/server/meteor-methods/saveCannedResponse.ts @RocketChat/omnichannel +/apps/meteor/ee/server/meteor-methods/removeCannedResponse.ts @RocketChat/omnichannel /apps/meteor/client/omnichannel @RocketChat/omnichannel /apps/meteor/client/components/omnichannel @RocketChat/omnichannel diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index 2d7b1fa736a18..827264b4a6434 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -8,6 +8,12 @@ inputs: CR_PAT: required: true description: 'GitHub Container Registry Personal Access Token' + DOCKER_USER: + required: false + description: 'DockerHub username (required for FIPS builds to pull private base image)' + DOCKER_PASS: + required: false + description: 'DockerHub password (required for FIPS builds to pull private base image)' deno-version: required: true description: 'Deno version' @@ -28,7 +34,7 @@ inputs: default: 'true' type: required: false - description: 'production or coverage' + description: 'production, coverage, or fips' default: 'coverage' runs: @@ -43,11 +49,19 @@ runs: username: ${{ inputs.CR_USER }} password: ${{ inputs.CR_PAT }} + - name: Login to DockerHub + # FIPS base image (rocketchat/dhi-node) lives in a private DockerHub repo and requires auth to pull. + if: inputs.type == 'fips' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.event_name == 'merge_group' || github.ref == 'refs/heads/develop') + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + with: + username: ${{ inputs.DOCKER_USER }} + password: ${{ inputs.DOCKER_PASS }} + - name: Restore meteor build if: inputs.service == 'rocketchat' uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: - name: build-${{ inputs.type }} + name: build-${{ inputs.type == 'fips' && ((github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production') || inputs.type }} path: /tmp/build - name: Unpack meteor build @@ -70,6 +84,33 @@ runs: } } + # Microservice images build from a pruned, self-contained context (out/) + # produced by `turbo prune`, instead of a hand-maintained list of COPY lines. + # The rocketchat (Meteor) image is a prebuilt bundle and is staged separately above. + - name: Stage service build context + if: inputs.service != 'rocketchat' + shell: bash + env: + INPUT_SERVICE: ${{ inputs.service }} + run: | + set -o xtrace + + # The Docker SERVICE arg (folder under ee/apps) lives in the compose build args; + # the compose service name (INPUT_SERVICE) can differ (e.g. ddp-streamer-service). + SERVICE_DIR=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "$INPUT_SERVICE" '.services[$s].build.args.SERVICE') + PKG_NAME=$(node -p "require('./ee/apps/${SERVICE_DIR}/package.json').name") + TURBO_VERSION=$(node -p "require('./package.json').devDependencies.turbo") + + rm -rf "out/${SERVICE_DIR}" + + # `turbo prune` derives the exact workspace dependency closure for the service โ€” no + # hand-maintained list. --use-gitignore=false also captures the gitignored build output + # we need (dist/, generated definition/), at the cost of dragging in node_modules/.turbo, + # which we strip below; `yarn workspaces focus --production` rebuilds node_modules in-image. + npx -y "turbo@${TURBO_VERSION}" prune "$PKG_NAME" --skip-infer --use-gitignore=false --out-dir "out/${SERVICE_DIR}" + + find "out/${SERVICE_DIR}" \( -name node_modules -o -name .turbo \) -type d -prune -exec rm -rf {} + + - name: Build Docker images shell: bash env: @@ -85,6 +126,10 @@ runs: GITHUB_REF: ${{ github.ref }} run: | set -o xtrace + compose_fips_override='' + if [[ "$INPUT_TYPE" == 'fips' ]]; then + compose_fips_override='-f docker-compose-ci.fips.yml' + fi # Removes unnecessary swc cores and sharp binaries to reduce image size swc_arch='x64' @@ -109,10 +154,10 @@ runs: fi # Get image name from docker-compose-ci.yml since rocketchat image is different from service name (rocket.chat) - IMAGE=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "$INPUT_SERVICE" '.services[$s].image') + IMAGE=$(docker compose -f docker-compose-ci.yml $compose_fips_override config --format json 2>/dev/null | jq -r --arg s "$INPUT_SERVICE" '.services[$s].image') docker buildx bake \ - -f docker-compose-ci.yml \ + -f docker-compose-ci.yml $compose_fips_override \ ${LOAD_OR_PUSH} \ --allow=fs.read=/tmp/build \ --set "*.tags+=${IMAGE}-gha-run-${GITHUB_RUN_ID}" \ @@ -133,6 +178,8 @@ runs: SERVICE_SUFFIX='' if [[ "$INPUT_SERVICE" == 'rocketchat' && "$INPUT_TYPE" == 'coverage' ]] && [[ "$GITHUB_EVENT_NAME" == 'release' || "$GITHUB_REF" == 'refs/heads/develop' ]]; then SERVICE_SUFFIX='-cov' + elif [[ "$INPUT_TYPE" == 'fips' ]]; then + SERVICE_SUFFIX='-fips' fi mkdir -p "/tmp/manifests/${INPUT_SERVICE}${SERVICE_SUFFIX}/${INPUT_ARCH}" @@ -160,9 +207,13 @@ runs: TYPE: ${{ inputs.type }} run: | set -o xtrace + compose_fips_override='' + if [[ "$TYPE" == 'fips' ]]; then + compose_fips_override='-f docker-compose-ci.fips.yml' + fi # Get image name from docker-compose-ci.yml - IMAGE=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "$SERVICE" '.services[$s].image') + IMAGE=$(docker compose -f docker-compose-ci.yml $compose_fips_override config --format json 2>/dev/null | jq -r --arg s "$SERVICE" '.services[$s].image') # Create directory for image archives mkdir -p /tmp/docker-images diff --git a/.github/actions/docker-image-size-tracker/action.yml b/.github/actions/docker-image-size-tracker/action.yml index b2ce2339435e4..31d887ce40223 100644 --- a/.github/actions/docker-image-size-tracker/action.yml +++ b/.github/actions/docker-image-size-tracker/action.yml @@ -89,10 +89,11 @@ runs: # Read only the specified platform architecture manifest_file="$service_dir/$PLATFORM/manifest.json" if [[ -f "$manifest_file" ]]; then - # Docker manifest inspect -v returns SchemaV2Manifest with sizes - # Extract config size and layer sizes - config_size=$(jq -r '.SchemaV2Manifest.config.size // 0' "$manifest_file") - layers_size=$(jq '[.SchemaV2Manifest.layers[]?.size // 0] | add // 0' "$manifest_file") + # docker manifest inspect -v puts the manifest under SchemaV2Manifest + # (Docker schema2 images) or OCIManifest (OCI images, which buildx + # produces by default). Coalesce so both formats are read. + config_size=$(jq -r '(.SchemaV2Manifest // .OCIManifest).config.size // 0' "$manifest_file") + layers_size=$(jq '[(.SchemaV2Manifest // .OCIManifest).layers[]?.size // 0] | add // 0' "$manifest_file") size=$((config_size + layers_size)) echo " โ†’ Found $manifest_file: $size bytes (config: $config_size, layers: $layers_size)" diff --git a/.github/actions/update-version-durability/package-lock.json b/.github/actions/update-version-durability/package-lock.json index c48e783a33d09..f57288ebe7dd1 100644 --- a/.github/actions/update-version-durability/package-lock.json +++ b/.github/actions/update-version-durability/package-lock.json @@ -53,14 +53,6 @@ "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", "license": "MIT" }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "engines": { - "node": ">=14" - } - }, "node_modules/@octokit/auth-token": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", @@ -612,15 +604,12 @@ } }, "node_modules/undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, "engines": { - "node": ">=14.0" + "node": ">=18.17" } }, "node_modules/universal-user-agent": { diff --git a/.github/actions/update-version-durability/package.json b/.github/actions/update-version-durability/package.json index b7483014396c9..71b5850b671b5 100644 --- a/.github/actions/update-version-durability/package.json +++ b/.github/actions/update-version-durability/package.json @@ -17,5 +17,8 @@ "diff": "^5.1.0", "semver": "^7.5.4", "@xmldom/xmldom": "^0.8.13" + }, + "overrides": { + "undici": "6.27.0" } } diff --git a/.github/workflows/auto-close-duplicates.yml b/.github/workflows/auto-close-duplicates.yml index 73f43e3465177..c2fe7bf5f8ab1 100644 --- a/.github/workflows/auto-close-duplicates.yml +++ b/.github/workflows/auto-close-duplicates.yml @@ -5,6 +5,8 @@ on: - cron: '0 9 * * *' workflow_dispatch: +permissions: {} + jobs: auto-close-duplicates: runs-on: ubuntu-latest @@ -15,7 +17,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index 21ff95c641948..84863f7987c80 100644 --- a/.github/workflows/ci-code-check.yml +++ b/.github/workflows/ci-code-check.yml @@ -13,9 +13,13 @@ on: env: TOOL_NODE_FLAGS: ${{ vars.TOOL_NODE_FLAGS }} +permissions: {} + jobs: code-check: runs-on: ubuntu-24.04-arm + permissions: + contents: read name: ${{ matrix.check == 'ts' && 'TypeScript' || 'Code Lint' }} @@ -30,7 +34,7 @@ jobs: with: swap-size-gb: 4 - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup NodeJS uses: ./.github/actions/setup-node @@ -41,14 +45,14 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 + - uses: rharkor/caching-for-turbo@75f8ebf4a43d2c60b23bc2a27082cfea94ffdad9 # v2.5.0 - uses: ./.github/actions/restore-packages - name: Restore TypeScript incremental cache id: restore-typecheck if: matrix.check == 'ts' - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ./apps/meteor/tsconfig.typecheck.tsbuildinfo key: typecheck-cache-${{ runner.os }}-${{ hashFiles('yarn.lock') }} @@ -89,7 +93,7 @@ jobs: - name: Save TypeScript incremental cache if: matrix.check == 'ts' && github.ref == 'refs/heads/develop' && github.event_name == 'push' - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ./apps/meteor/tsconfig.typecheck.tsbuildinfo key: typecheck-cache-${{ runner.os }}-${{ hashFiles('yarn.lock') }} @@ -97,7 +101,7 @@ jobs: - name: Restore ESLint cache id: restore-eslint if: matrix.check == 'lint' - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ./apps/meteor/.eslintcache key: eslintcache-${{ runner.os }}-${{ hashFiles('yarn.lock') }} @@ -118,7 +122,7 @@ jobs: - name: Save ESLint cache if: matrix.check == 'lint' && github.ref == 'refs/heads/develop' && github.event_name == 'push' - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ./apps/meteor/.eslintcache key: eslintcache-${{ runner.os }}-${{ hashFiles('yarn.lock') }} diff --git a/.github/workflows/ci-deploy-gh-pages.yml b/.github/workflows/ci-deploy-gh-pages.yml index e1b8b81517da6..4ad5b16842d68 100644 --- a/.github/workflows/ci-deploy-gh-pages.yml +++ b/.github/workflows/ci-deploy-gh-pages.yml @@ -7,12 +7,17 @@ on: - main - master - develop + +permissions: {} + jobs: deploy-preview: runs-on: ubuntu-24.04-arm + permissions: + contents: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: rharkor/caching-for-turbo@75f8ebf4a43d2c60b23bc2a27082cfea94ffdad9 # v2.5.0 - name: Setup NodeJS uses: ./.github/actions/setup-node diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index c8421db957b60..b08bd2615cf94 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -26,6 +26,10 @@ on: release: required: true type: string + fips: + default: false + required: false + type: boolean shard: default: '[1]' required: false @@ -68,9 +72,14 @@ env: LOWERCASE_REPOSITORY: ${{ inputs.lowercase-repo }} DOCKER_TAG: ${{ inputs.gh-docker-tag }}-amd64 +permissions: {} + jobs: test: runs-on: ubuntu-24.04 + permissions: + actions: read + contents: read env: # if building for production on develop branch or release, add suffix for coverage images @@ -108,13 +117,13 @@ jobs: - name: Login to GitHub Container Registry if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') && github.actor != 'dependabot[bot]' - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ secrets.CR_USER }} password: ${{ secrets.CR_PAT }} - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup NodeJS uses: ./.github/actions/setup-node @@ -125,7 +134,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 + - uses: rharkor/caching-for-turbo@75f8ebf4a43d2c60b23bc2a27082cfea94ffdad9 # v2.5.0 - uses: ./.github/actions/restore-packages @@ -134,7 +143,7 @@ jobs: uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 if: github.event.pull_request.head.repo.full_name != github.repository && github.event_name != 'release' && github.ref != 'refs/heads/develop' with: - pattern: ${{ inputs.release == 'ce' && 'docker-image-rocketchat-amd64-coverage' || 'docker-image-*-amd64-coverage' }} + pattern: ${{ inputs.release == 'ce' && 'docker-image-rocketchat-amd64-coverage' || (inputs.fips && 'docker-image-*-amd64-fips' || 'docker-image-*-amd64-coverage') }} path: /tmp/docker-images merge-multiple: true @@ -162,7 +171,7 @@ jobs: run: echo "DEBUG_LOG_LEVEL=2" >> "$GITHUB_ENV" - name: Start httpbin container and wait for it to be ready - if: inputs.type == 'api' || inputs.type == 'api-livechat' + if: startsWith(inputs.type, 'api') run: | docker compose -f docker-compose-ci.yml up -d httpbin @@ -181,7 +190,7 @@ jobs: # behavior on (rate-limiter bypass, short cache TTLs) while letting # the deprecation logger log without throwing. Other suites use the # docker-compose default of TEST_MODE='true'. - TEST_MODE: ${{ (inputs.type == 'api' || inputs.type == 'api-livechat') && 'api' || 'true' }} + TEST_MODE: ${{ startsWith(inputs.type, 'api') && 'api' || 'true' }} run: | # when we are testing CE, we only need to start the rocketchat container DEBUG_LOG_LEVEL=${DEBUG_LOG_LEVEL:-0} docker compose -f docker-compose-ci.yml up -d rocketchat --wait @@ -192,9 +201,12 @@ jobs: ENTERPRISE_LICENSE: ${{ inputs.enterprise-license }} TRANSPORTER: ${{ inputs.transporter }} COMPOSE_PROFILES: ${{ inputs.type == 'api' && 'api' || '' }} - TEST_MODE: ${{ (inputs.type == 'api' || inputs.type == 'api-livechat') && 'api' || 'true' }} + TEST_MODE: ${{ startsWith(inputs.type, 'api') && 'api' || 'true' }} + APPS_ENGINE_RUNTIME_BACKEND: ${{ inputs.type == 'api-apps-node' && 'node' || '' }} + FIPS_OVERRIDE: ${{ inputs.fips && '-f docker-compose-ci.fips.yml' || '' }} run: | - DEBUG_LOG_LEVEL=${DEBUG_LOG_LEVEL:-0} docker compose -f docker-compose-ci.yml up -d --wait + read -r -a fips_override <<< "$FIPS_OVERRIDE" + DEBUG_LOG_LEVEL=${DEBUG_LOG_LEVEL:-0} docker compose -f docker-compose-ci.yml "${fips_override[@]}" up -d --wait - uses: ./.github/actions/setup-playwright if: inputs.type == 'ui' @@ -229,6 +241,23 @@ jobs: ls -la "$COVERAGE_DIR" exit "${s:-0}" + # This step should be temporary, only here until we remove the deno-runtime + - name: E2E Test API (apps + node-runtime) + if: (inputs.type == 'api-apps-node' && inputs.release == 'ee') + working-directory: ./apps/meteor + env: + WEBHOOK_TEST_URL: 'http://httpbin' + IS_EE: 'true' + run: | + set -o xtrace + + npm run testapi:apps || s=$? + + docker compose -f ../../docker-compose-ci.yml stop + + ls -la "$COVERAGE_DIR" + exit "${s:-0}" + - name: E2E Test API (Livechat) if: inputs.type == 'api-livechat' working-directory: ./apps/meteor @@ -285,7 +314,7 @@ jobs: if: inputs.type == 'ui' && always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: playwright-test-trace-${{ inputs.release }}-${{ matrix.mongodb-version }}-${{ matrix.shard }} + name: playwright-test-trace-${{ inputs.release }}${{ inputs.fips && '-fips' || '' }}-${{ matrix.mongodb-version }}-${{ matrix.shard }} path: ./apps/meteor/tests/e2e/.playwright* include-hidden-files: true @@ -297,6 +326,10 @@ jobs: if: failure() run: docker compose -f docker-compose-ci.yml logs mongo + - name: Show traefik logs if E2E test failed + if: failure() + run: docker compose -f docker-compose-ci.yml logs traefik + - name: Store coverage if: inputs.coverage == matrix.mongodb-version uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/ci-test-storybook.yml b/.github/workflows/ci-test-storybook.yml index 9c7732f88a48d..f277dd0b54b3b 100644 --- a/.github/workflows/ci-test-storybook.yml +++ b/.github/workflows/ci-test-storybook.yml @@ -19,14 +19,18 @@ on: env: TOOL_NODE_FLAGS: ${{ vars.TOOL_NODE_FLAGS }} +permissions: {} + jobs: test: runs-on: ubuntu-24.04-arm + permissions: + contents: read name: Test Storybook steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup NodeJS uses: ./.github/actions/setup-node @@ -37,7 +41,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 + - uses: rharkor/caching-for-turbo@75f8ebf4a43d2c60b23bc2a27082cfea94ffdad9 # v2.5.0 - uses: ./.github/actions/restore-packages @@ -48,8 +52,11 @@ jobs: env: STORYBOOK_DISABLE_TELEMETRY: 1 - - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 + - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: flags: unit verbose: true token: ${{ secrets.CODECOV_TOKEN }} + # tests don't re-run on the develop push, so the merge queue run (same SHA + # that lands on develop) provides the develop coverage baseline + override_branch: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_ref == 'refs/heads/develop' && 'develop' || '' }} diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index a13e1fe4834cd..abf9d12254295 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -23,14 +23,18 @@ env: TOOL_NODE_FLAGS: ${{ vars.TOOL_NODE_FLAGS }} ENTERPRISE_LICENSE: ${{ inputs.enterprise-license }} +permissions: {} + jobs: test: runs-on: ubuntu-24.04-arm + permissions: + contents: read name: Unit Tests steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup NodeJS uses: ./.github/actions/setup-node @@ -41,15 +45,18 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 + - uses: rharkor/caching-for-turbo@75f8ebf4a43d2c60b23bc2a27082cfea94ffdad9 # v2.5.0 - uses: ./.github/actions/restore-packages - name: Unit Test run: yarn testunit --concurrency=1 - - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 + - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: flags: unit verbose: true token: ${{ secrets.CODECOV_TOKEN }} + # tests don't re-run on the develop push, so the merge queue run (same SHA + # that lands on develop) provides the develop coverage baseline + override_branch: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_ref == 'refs/heads/develop' && 'develop' || '' }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77939861d1a42..00a7a3e0579e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,16 +16,25 @@ on: - '**.md' concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + # merge_group runs group by the merge-group ref so a recreated group cancels the stale run; + # push/release runs group by run_id on purpose (never cancel a deploy in progress) + group: ${{ github.workflow }}-${{ github.head_ref || (github.event_name == 'merge_group' && github.ref) || github.run_id }} cancel-in-progress: true env: TOOL_NODE_FLAGS: ${{ vars.TOOL_NODE_FLAGS }} +permissions: + contents: read + actions: read + jobs: release-versions: name: โš™๏ธ Variables Setup runs-on: ubuntu-24.04-arm + permissions: + contents: read + pull-requests: read outputs: release: ${{ steps.by-tag.outputs.release }} latest-release: ${{ steps.latest.outputs.latest-release }} @@ -51,7 +60,7 @@ jobs: echo "github.event_name: ${{ github.event_name }}" cat "$GITHUB_EVENT_PATH" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # with: # sparse-checkout: | # package.json @@ -144,12 +153,45 @@ jobs: echo "actions-changed=true" >> "$GITHUB_OUTPUT" fi + test-guard: + name: โš™๏ธ Test Guard + runs-on: ubuntu-24.04-arm + permissions: + checks: read + outputs: + skip-tests: ${{ steps.check.outputs.skip-tests }} + steps: + # Commits only reach develop through the merge queue, where the full test + # suite already ran on this exact SHA โ€” skip re-running it on the push event. + # Direct pushes (admin/revert) have no successful "Tests Done" check, so they + # still get the full suite. continue-on-error keeps this fail-open: if the + # API call fails, tests run. + - name: Check if this commit already passed tests + id: check + if: github.event_name == 'push' + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + run: | + # --paginate applies the jq filter per page, so emit names and count with + # wc -l to get a single integer across pages + passed=$(gh api "repos/${{ github.repository }}/commits/${{ github.sha }}/check-runs" --paginate \ + --jq '.check_runs[] | select(.name == "โœ… Tests Done" and .conclusion == "success") | .name' | wc -l) + + if [[ "$passed" -gt 0 ]]; then + echo "Tests already passed for ${{ github.sha }} in the merge queue, skipping test jobs" + echo "skip-tests=true" >> "$GITHUB_OUTPUT" + else + echo "skip-tests=false" >> "$GITHUB_OUTPUT" + fi + notify-draft-services: name: ๐Ÿš€ Notify external services - draft runs-on: ubuntu-24.04-arm needs: [release-versions] + if: github.event_name == 'release' steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: sparse-checkout: | package.json @@ -188,11 +230,11 @@ jobs: packages-build: name: ๐Ÿ“ฆ Build Packages - needs: [release-versions, notify-draft-services] + needs: [release-versions] runs-on: ubuntu-24.04-arm steps: - name: Cache build - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 id: packages-cache-build with: path: | @@ -213,7 +255,7 @@ jobs: with: swap-size-gb: 4 - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 if: steps.packages-cache-build.outputs.cache-hit != 'true' - name: Setup NodeJS @@ -238,7 +280,7 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" - name: Cache vite - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 if: steps.packages-cache-build.outputs.cache-hit != 'true' with: path: ./node_modules/.vite @@ -246,7 +288,7 @@ jobs: restore-keys: | vite-local-cache-${{ runner.arch }}-${{ runner.os }}- - - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 + - uses: rharkor/caching-for-turbo@75f8ebf4a43d2c60b23bc2a27082cfea94ffdad9 # v2.5.0 if: steps.packages-cache-build.outputs.cache-hit != 'true' - name: Build Rocket.Chat Packages @@ -289,7 +331,7 @@ jobs: - type: ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'production' || '' }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: ./.github/actions/meteor-build with: @@ -318,10 +360,12 @@ jobs: [rocketchat], ] type: - # if running in a PR build with coverage - ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }} + - fips + exclude: + - arch: arm64 + type: fips include: - # if not, build with coverage for tests - arch: amd64 service: [rocketchat] type: coverage @@ -330,7 +374,7 @@ jobs: type: coverage steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: ./.github/actions/restore-packages @@ -344,6 +388,8 @@ jobs: with: CR_USER: ${{ secrets.CR_USER }} CR_PAT: ${{ secrets.CR_PAT }} + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASS: ${{ secrets.DOCKER_PASS }} deno-version: ${{ needs.release-versions.outputs.deno-version }} arch: ${{ matrix.arch }} service: ${{ matrix.service[0] }} @@ -358,6 +404,8 @@ jobs: with: CR_USER: ${{ secrets.CR_USER }} CR_PAT: ${{ secrets.CR_PAT }} + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASS: ${{ secrets.DOCKER_PASS }} deno-version: ${{ needs.release-versions.outputs.deno-version }} arch: ${{ matrix.arch }} service: ${{ matrix.service[1] }} @@ -373,6 +421,8 @@ jobs: with: CR_USER: ${{ secrets.CR_USER }} CR_PAT: ${{ secrets.CR_PAT }} + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASS: ${{ secrets.DOCKER_PASS }} deno-version: ${{ needs.release-versions.outputs.deno-version }} arch: ${{ matrix.arch }} service: ${{ matrix.service[2] }} @@ -388,6 +438,8 @@ jobs: with: CR_USER: ${{ secrets.CR_USER }} CR_PAT: ${{ secrets.CR_PAT }} + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASS: ${{ secrets.DOCKER_PASS }} deno-version: ${{ needs.release-versions.outputs.deno-version }} arch: ${{ matrix.arch }} service: ${{ matrix.service[3] }} @@ -405,17 +457,18 @@ jobs: LOWERCASE_REPOSITORY: ${{ needs.release-versions.outputs.lowercase-repo }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') with: sparse-checkout: | docker-compose-ci.yml + docker-compose-ci.fips.yml sparse-checkout-cone-mode: false ref: ${{ github.ref }} - name: Login to GitHub Container Registry if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ secrets.CR_USER }} @@ -452,6 +505,9 @@ jobs: # Get image name from docker-compose-ci.yml since rocketchat image is different from service name (rocket.chat) if [ "$service" == "rocketchat-cov" ]; then IMAGE=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "rocketchat" '.services[$s].image')-cov + elif [[ "$service" == *"-fips" ]]; then + base_service="${service%-fips}" + IMAGE=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "$base_service" '.services[$s].image')-fips else IMAGE=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "$service" '.services[$s].image') fi @@ -476,7 +532,7 @@ jobs: contents: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Track Docker image sizes uses: ./.github/actions/docker-image-size-tracker @@ -498,7 +554,7 @@ jobs: env: ACTIONLINT_VERSION: 1.7.12 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Get actionlint shell: bash @@ -516,8 +572,11 @@ jobs: ./actionlint -color + # gating `checks` also skips every e2e test job (they all `need` it); + # test-storybook and test-unit don't, so they carry the same gate below checks: - needs: [release-versions, packages-build] + needs: [release-versions, packages-build, test-guard] + if: needs.test-guard.outputs.skip-tests != 'true' name: ๐Ÿ”Ž Code Check uses: ./.github/workflows/ci-code-check.yml @@ -527,7 +586,8 @@ jobs: test-storybook: name: ๐Ÿ”จ Test Storybook - needs: [packages-build, release-versions] + needs: [packages-build, release-versions, test-guard] + if: needs.test-guard.outputs.skip-tests != 'true' uses: ./.github/workflows/ci-test-storybook.yml with: @@ -538,7 +598,8 @@ jobs: test-unit: name: ๐Ÿ”จ Test Unit - needs: [packages-build, release-versions] + needs: [packages-build, release-versions, test-guard] + if: needs.test-guard.outputs.skip-tests != 'true' uses: ./.github/workflows/ci-test-unit.yml with: @@ -650,6 +711,27 @@ jobs: CR_PAT: ${{ secrets.CR_PAT }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + test-api-apps-node-ee: + name: ๐Ÿ”จ Test API Apps (node-runtime - EE) + needs: [checks, build-gh-docker-publish, release-versions] + + uses: ./.github/workflows/ci-test-e2e.yml + with: + type: api-apps-node + release: ee + transporter: 'nats://nats:4222' + enterprise-license: ${{ needs.release-versions.outputs.enterprise-license }} + mongodb-version: "['8.0']" + coverage: '8.0' + node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} + lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} + gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + secrets: + CR_USER: ${{ secrets.CR_USER }} + CR_PAT: ${{ secrets.CR_PAT }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + test-ui-ee: name: ๐Ÿ”จ Test UI (EE) needs: [checks, build-gh-docker-publish, release-versions] @@ -679,13 +761,83 @@ jobs: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} + test-api-fips: + name: ๐Ÿ”จ Test API (FIPS) + needs: [checks, build-gh-docker-publish, release-versions] + + uses: ./.github/workflows/ci-test-e2e.yml + with: + type: api + release: ee + fips: true + transporter: 'nats://nats:4222' + enterprise-license: ${{ needs.release-versions.outputs.enterprise-license }} + mongodb-version: "['8.0']" + node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} + lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} + gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + secrets: + CR_USER: ${{ secrets.CR_USER }} + CR_PAT: ${{ secrets.CR_PAT }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + test-api-livechat-fips: + name: ๐Ÿ”จ Test API Livechat (FIPS) + needs: [checks, build-gh-docker-publish, release-versions] + + uses: ./.github/workflows/ci-test-e2e.yml + with: + type: api-livechat + release: ee + fips: true + transporter: 'nats://nats:4222' + enterprise-license: ${{ needs.release-versions.outputs.enterprise-license }} + mongodb-version: "['8.0']" + node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} + lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} + gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + secrets: + CR_USER: ${{ secrets.CR_USER }} + CR_PAT: ${{ secrets.CR_PAT }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + test-ui-fips: + name: ๐Ÿ”จ Test UI (FIPS) + if: contains(github.event.pull_request.labels.*.name, 'fips') + needs: [checks, build-gh-docker-publish, release-versions] + uses: ./.github/workflows/ci-test-e2e.yml + with: + type: ui + release: ee + fips: true + transporter: 'nats://nats:4222' + enterprise-license: ${{ needs.release-versions.outputs.enterprise-license }} + shard: '[1, 2, 3, 4, 5]' + total-shard: 5 + mongodb-version: "['8.0']" + node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} + lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} + gh-docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + retries: ${{ (github.event_name == 'release' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') && 2 || 0 }} + secrets: + CR_USER: ${{ secrets.CR_USER }} + CR_PAT: ${{ secrets.CR_PAT }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + QASE_API_TOKEN: ${{ secrets.QASE_API_TOKEN }} + REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} + REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} + REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} + test-federation-matrix: name: ๐Ÿ”จ Test Federation Matrix needs: [checks, build-gh-docker-publish, packages-build, release-versions] runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup NodeJS uses: ./.github/actions/setup-node @@ -695,7 +847,7 @@ jobs: cache-modules: true install: true - - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 + - uses: rharkor/caching-for-turbo@75f8ebf4a43d2c60b23bc2a27082cfea94ffdad9 # v2.5.0 - name: Restore turbo build uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -709,7 +861,7 @@ jobs: - name: Login to GitHub Container Registry if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') && github.actor != 'dependabot[bot]' - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ secrets.CR_USER }} @@ -780,10 +932,10 @@ jobs: needs: [release-versions, test-api-ee, test-api-livechat-ee, test-ui-ee] steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Use Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: ${{ needs.release-versions.outputs.node-version }} @@ -809,28 +961,32 @@ jobs: include-hidden-files: true - name: Report API coverage - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: /tmp/coverage_report/api/lcov.info working-directory: . flags: e2e-api verbose: true token: ${{ secrets.CODECOV_TOKEN }} + # tests don't re-run on the develop push, so the merge queue run (same SHA + # that lands on develop) provides the develop coverage baseline + override_branch: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_ref == 'refs/heads/develop' && 'develop' || '' }} - name: Report UI coverage - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: /tmp/coverage_report/ui/lcov.info working-directory: . flags: e2e verbose: true token: ${{ secrets.CODECOV_TOKEN }} + override_branch: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_ref == 'refs/heads/develop' && 'develop' || '' }} tests-done: name: โœ… Tests Done runs-on: ubuntu-24.04-arm - needs: [checks, test-unit, test-api, test-ui, test-api-ee, test-ui-ee, test-api-livechat, test-api-livechat-ee, test-federation-matrix] - if: always() + needs: [test-guard, checks, test-unit, test-api, test-ui, test-api-ee, test-ui-ee, test-api-livechat, test-api-livechat-ee, test-api-apps-node-ee, test-api-fips, test-api-livechat-fips, test-ui-fips, test-federation-matrix] + if: always() && needs.test-guard.outputs.skip-tests != 'true' steps: - name: Test finish aggregation run: | @@ -866,6 +1022,22 @@ jobs: exit 1 fi + if [[ '${{ needs.test-api-apps-node-ee.result }}' != 'success' ]]; then + exit 1 + fi + + if [[ '${{ needs.test-api-fips.result }}' != 'success' ]]; then + exit 1 + fi + + if [[ '${{ needs.test-api-livechat-fips.result }}' != 'success' ]]; then + exit 1 + fi + + if [[ '${{ needs.test-ui-fips.result }}' != 'success' && '${{ needs.test-ui-fips.result }}' != 'skipped' ]]; then + exit 1 + fi + if [[ '${{ needs.test-federation-matrix.result }}' != 'success' ]]; then exit 1 fi @@ -879,7 +1051,7 @@ jobs: needs: [build-gh-docker-publish, release-versions] steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: sparse-checkout: | package.json @@ -934,21 +1106,22 @@ jobs: LOWERCASE_REPOSITORY: ${{ needs.release-versions.outputs.lowercase-repo }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: sparse-checkout: | docker-compose-ci.yml + docker-compose-ci.fips.yml sparse-checkout-cone-mode: false ref: ${{ github.ref }} - name: Login to DockerHub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASS }} - name: Login to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ secrets.CR_USER }} @@ -1014,28 +1187,39 @@ jobs: echo "Promoting $service" - if [[ "${service}" == 'rocketchat' ]]; then + # FIPS images publish to the same repo as their base service, distinguished by a + # '-fips' tag suffix (matching GHCR), not a separate '-fips' repository. + # Get image name from docker-compose-ci.yml since rocketchat image is different from service name (rocket.chat) + TAG_SUFFIX='' + if [[ "$service" == *"-fips" ]]; then + base_service="${service%-fips}" + TAG_SUFFIX='-fips' + SRC=$(docker compose -f docker-compose-ci.yml -f docker-compose-ci.fips.yml config --format json 2>/dev/null | jq -r --arg s "$base_service" '.services[$s].image') + else + base_service="$service" + SRC=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "$service" '.services[$s].image') + fi + + if [[ "${base_service}" == 'rocketchat' ]]; then IMAGE_NAME="${{ needs.release-versions.outputs.lowercase-repo }}/rocket.chat" else - IMAGE_NAME="${{ needs.release-versions.outputs.lowercase-repo }}/${service}" + IMAGE_NAME="${{ needs.release-versions.outputs.lowercase-repo }}/${base_service}" fi - # Get image name from docker-compose-ci.yml since rocketchat image is different from service name (rocket.chat) - SRC=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "${service}" '.services[$s].image') DEST_REPO="docker.io/${IMAGE_NAME}" - echo "Copying $SRC to ${DEST_REPO}:${PRIMARY}" + echo "Copying $SRC to ${DEST_REPO}:${PRIMARY}${TAG_SUFFIX}" skopeo copy --all \ "docker://${SRC}" \ - "docker://${DEST_REPO}:${PRIMARY}" + "docker://${DEST_REPO}:${PRIMARY}${TAG_SUFFIX}" # copy additional tags if (( ${#TAGS[@]} > 1 )); then for t in "${TAGS[@]:1}"; do - echo "Copying $SRC to ${DEST_REPO}:${t}" + echo "Copying $SRC to ${DEST_REPO}:${t}${TAG_SUFFIX}" skopeo copy --all \ "docker://${SRC}" \ - "docker://${DEST_REPO}:${t}" + "docker://${DEST_REPO}:${t}${TAG_SUFFIX}" done fi @@ -1048,7 +1232,7 @@ jobs: - docker-image-publish - release-versions steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: sparse-checkout: | package.json diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d2034fd0f6436..0f500b42ef28b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -12,10 +12,14 @@ jobs: CodeQL-Build: # CodeQL runs on ubuntu-latest and windows-latest runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. @@ -28,7 +32,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 # Override language selection by uncommenting this and choosing your languages with: languages: javascript @@ -36,7 +40,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/autobuild@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 # โ„น๏ธ Command-line programs to run using the OS shell. # ๐Ÿ“š https://git.io/JvXDl @@ -50,4 +54,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 diff --git a/.github/workflows/dedupe-issues.yml b/.github/workflows/dedupe-issues.yml index fe4565001a6e5..92a63eca34452 100644 --- a/.github/workflows/dedupe-issues.yml +++ b/.github/workflows/dedupe-issues.yml @@ -10,6 +10,8 @@ on: required: true type: string +permissions: {} + jobs: dedupe-issues: runs-on: ubuntu-latest @@ -20,7 +22,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Run Claude Code slash command uses: anthropics/claude-code-base-action@e8132bc5e637a42c27763fc757faa37e1ee43b34 # v0.0.63beta diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index 415e37e3c8f57..0a3c55db781f5 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -20,12 +20,16 @@ on: env: HUSKY: 0 +permissions: {} + jobs: new-release: runs-on: ubuntu-24.04 + permissions: + contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.inputs.base-ref }} fetch-depth: 0 @@ -38,7 +42,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 + - uses: rharkor/caching-for-turbo@75f8ebf4a43d2c60b23bc2a27082cfea94ffdad9 # v2.5.0 - name: Build packages run: yarn build diff --git a/.github/workflows/pr-update-description.yml b/.github/workflows/pr-update-description.yml index 58fa2d371e213..bbce3187cdcdb 100644 --- a/.github/workflows/pr-update-description.yml +++ b/.github/workflows/pr-update-description.yml @@ -7,13 +7,17 @@ on: concurrency: ${{ github.workflow }}-${{ github.ref }} +permissions: {} + jobs: update-pr: runs-on: ubuntu-24.04 if: startsWith(github.head_ref, 'release-') + permissions: + contents: read steps: - name: Checkout Repo - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 token: ${{ secrets.CI_PAT }} @@ -25,7 +29,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 + - uses: rharkor/caching-for-turbo@75f8ebf4a43d2c60b23bc2a27082cfea94ffdad9 # v2.5.0 - name: Build packages run: yarn build diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 7be1cf06211f2..8a02682383f9f 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -10,13 +10,17 @@ concurrency: ${{ github.workflow }}-${{ github.ref }} env: HUSKY: 0 +permissions: {} + jobs: release: name: Release runs-on: ubuntu-24.04 + permissions: + contents: read steps: - name: Checkout Repo - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 token: ${{ secrets.CI_PAT }} @@ -28,7 +32,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 + - uses: rharkor/caching-for-turbo@75f8ebf4a43d2c60b23bc2a27082cfea94ffdad9 # v2.5.0 - name: Build packages run: yarn build diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index a956df9f1f018..39e3578291bc0 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -2,11 +2,16 @@ name: Release candidate cut on: schedule: - cron: '28 21 20 * *' # run at minute 28 to avoid the chance of delay due to high load on GH + +permissions: {} + jobs: new-release: runs-on: ubuntu-24.04 + permissions: + contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.ref_name }} fetch-depth: 0 @@ -19,7 +24,7 @@ jobs: install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - - uses: rharkor/caching-for-turbo@5d14fba18e450c09393333cfd4242e8b3cb455a6 # v2.4.2 + - uses: rharkor/caching-for-turbo@75f8ebf4a43d2c60b23bc2a27082cfea94ffdad9 # v2.5.0 - name: Build packages run: yarn build diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a4e06de4a534c..d7294f102eeca 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -9,7 +9,7 @@ jobs: permissions: issues: write steps: - - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 + - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0 with: days-before-issue-stale: 14 days-before-issue-close: 14 diff --git a/.github/workflows/todo.yml b/.github/workflows/todo.yml index e1b72eeed170b..def0f2504b95b 100644 --- a/.github/workflows/todo.yml +++ b/.github/workflows/todo.yml @@ -33,7 +33,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 diff --git a/.github/workflows/update-version-durability.yml b/.github/workflows/update-version-durability.yml index 11fa82711ae1e..f543e14c92dd5 100644 --- a/.github/workflows/update-version-durability.yml +++ b/.github/workflows/update-version-durability.yml @@ -15,15 +15,19 @@ on: D360_USER_ID: required: true +permissions: {} + jobs: update-versions: runs-on: ubuntu-24.04 + permissions: + contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Use Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22.22.3 diff --git a/.yarn/patches/@react-pdf-layout-npm-4.4.2-6c2e3312fa.patch b/.yarn/patches/@react-pdf-layout-npm-4.4.2-6c2e3312fa.patch deleted file mode 100644 index a8271b2de103b..0000000000000 --- a/.yarn/patches/@react-pdf-layout-npm-4.4.2-6c2e3312fa.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/lib/index.js b/lib/index.js -index c64bdf2d5f7e704a65be4e9a7116c5ee6a582701..ce6641d0b63daf7c5d3f8a1de773f290c0e9d51c 100644 ---- a/lib/index.js -+++ b/lib/index.js -@@ -2,8 +2,8 @@ import { upperFirst, capitalize, parseFloat as parseFloat$1, without, pick, comp - import * as P from '@react-pdf/primitives'; - import resolveStyle, { transformColor, flatten } from '@react-pdf/stylesheet'; - import layoutEngine, { fontSubstitution, wordHyphenation, scriptItemizer, textDecoration, justification, linebreaker, bidi, fromFragments } from '@react-pdf/textkit'; --import * as Yoga from 'yoga-layout/load'; --import { loadYoga as loadYoga$1 } from 'yoga-layout/load'; -+import * as Yoga from 'yoga-layout/dist/src/load.js'; -+import { loadYoga as loadYoga$1 } from 'yoga-layout/dist/src/load.js'; - import emojiRegex from 'emoji-regex-xs'; - import resolveImage from '@react-pdf/image'; - diff --git a/.yarn/patches/@react-pdf-layout-npm-4.6.1-b12fdb71f4.patch b/.yarn/patches/@react-pdf-layout-npm-4.6.1-b12fdb71f4.patch new file mode 100644 index 0000000000000..740c1484bde8c --- /dev/null +++ b/.yarn/patches/@react-pdf-layout-npm-4.6.1-b12fdb71f4.patch @@ -0,0 +1,24 @@ +diff --git a/lib/index.js b/lib/index.js +index b7ea5e30fddb9bdef0034c00f28bbd4f0dee584a..47f7717632bd55d3c42ebb39f1ef5a23c14cfa3a 100644 +--- a/lib/index.js ++++ b/lib/index.js +@@ -2,8 +2,8 @@ import { upperFirst, capitalize, parseFloat as parseFloat$1, without, pick, comp + import * as P from '@react-pdf/primitives'; + import resolveStyle, { transformColor, flatten } from '@react-pdf/stylesheet'; + import layoutEngine, { fontSubstitution, wordHyphenation, scriptItemizer, textDecoration, justification, linebreaker, bidi, fromFragments } from '@react-pdf/textkit'; +-import * as Yoga from 'yoga-layout/load'; +-import { loadYoga as loadYoga$1 } from 'yoga-layout/load'; ++import * as Yoga from 'yoga-layout/dist/src/load.js'; ++import { loadYoga as loadYoga$1 } from 'yoga-layout/dist/src/load.js'; + import emojiRegex from 'emoji-regex-xs'; + import resolveImage from '@react-pdf/image'; + +@@ -2998,7 +2998,7 @@ const splitPage = (page, pageNumber, fontStore, yoga) => { + const dynamicPage = resolveDynamicPage({ pageNumber }, page, fontStore, yoga); + const height = page.style.height; + const [currentChilds, nextChilds] = splitNodes(wrapArea, contentArea, dynamicPage.children); +- const relayout = (node) => ++ const relayout = (node) => + // @ts-expect-error rework pagination + relayoutPage(node, fontStore, yoga); + const currentBox = { ...page.box, height }; diff --git a/.yarn/patches/file-type-npm-16.5.4-d653e66a40.patch b/.yarn/patches/file-type-npm-16.5.4-d653e66a40.patch new file mode 100644 index 0000000000000..a712347e8eea7 --- /dev/null +++ b/.yarn/patches/file-type-npm-16.5.4-d653e66a40.patch @@ -0,0 +1,23 @@ +diff --git a/core.js b/core.js +index d653e66a40..0f5d672dc4 100644 +--- a/core.js ++++ b/core.js +@@ -1074,6 +1074,7 @@ async function _fromTokenizer(tokenizer) { + await tokenizer.ignore(30); + // Search for header should be in first 1KB of file. + while (tokenizer.position + 24 < tokenizer.fileInfo.size) { ++ const previousPosition = tokenizer.position; + const header = await readHeader(); + let payload = header.size - 24; + if (_check(header.id, [0x91, 0x07, 0xDC, 0xB7, 0xB7, 0xA9, 0xCF, 0x11, 0x8E, 0xE6, 0x00, 0xC0, 0x0C, 0x20, 0x53, 0x65])) { +@@ -1101,6 +1102,10 @@ async function _fromTokenizer(tokenizer) { + } + + await tokenizer.ignore(payload); ++ ++ if (tokenizer.position <= previousPosition) { ++ break; ++ } + } + + // Default to ASF generic extension diff --git a/.yarn/patches/react-aria-npm-3.37.0-83959bd2fa.patch b/.yarn/patches/react-aria-npm-3.37.0-83959bd2fa.patch index 4cceb4611139a..bf3c5276f8ed2 100644 --- a/.yarn/patches/react-aria-npm-3.37.0-83959bd2fa.patch +++ b/.yarn/patches/react-aria-npm-3.37.0-83959bd2fa.patch @@ -1,5 +1,5 @@ diff --git a/dist/import.mjs b/dist/import.mjs -index a13ba8b37e27230ec01d4c31140127912208666c..97d6aa506f273263c2a99a9bc80731ac2c38eea6 100644 +index a13ba8b37e27230ec01d4c31140127912208666c..4208f8b7e8c3ff25b4eff8cce9017e8cfb8d3b4e 100644 --- a/dist/import.mjs +++ b/dist/import.mjs @@ -1,50 +1,14 @@ @@ -44,7 +44,7 @@ index a13ba8b37e27230ec01d4c31140127912208666c..97d6aa506f273263c2a99a9bc80731ac -export {useSearchField} from '@react-aria/searchfield'; -export {HiddenSelect, useHiddenSelect, useSelect} from '@react-aria/select'; -export {ListKeyboardDelegate} from '@react-aria/selection'; -+export {useMenu, useMenuItem, useMenuSection, useMenuTrigger} from '@react-aria/menu'; ++export {useMenu, useMenuItem, useMenuSection, useMenuTrigger, useSubmenuTrigger} from '@react-aria/menu'; +export {DismissButton, Overlay, useOverlayTrigger, usePopover} from '@react-aria/overlays'; +export {HiddenSelect, useSelect} from '@react-aria/select'; export {useSeparator} from '@react-aria/separator'; @@ -62,7 +62,7 @@ index a13ba8b37e27230ec01d4c31140127912208666c..97d6aa506f273263c2a99a9bc80731ac - +export {useDialog} from '@react-aria/dialog'; diff --git a/dist/main.js b/dist/main.js -index b6a2ce3a780a09aeb416d7330ffcf6fd0b36b218..98e720a4660bdcd0384509786d04681a8ba98886 100644 +index b6a2ce3a780a09aeb416d7330ffcf6fd0b36b218..c5e9d3e7ba3c8f1a4db5a8e2f7b9c4d6a1e3f2b5 100644 --- a/dist/main.js +++ b/dist/main.js @@ -1,172 +1,15 @@ @@ -243,7 +243,7 @@ index b6a2ce3a780a09aeb416d7330ffcf6fd0b36b218..98e720a4660bdcd0384509786d04681a +var _i18n = require("@react-aria/i18n"); exports.useNumberFormatter = _i18n.useNumberFormatter; +var _interactions = require("@react-aria/interactions"); exports.useFocus = _interactions.useFocus; exports.useFocusVisible = _interactions.useFocusVisible; exports.useFocusWithin = _interactions.useFocusWithin; exports.useHover = _interactions.useHover; exports.useInteractOutside = _interactions.useInteractOutside; exports.useKeyboard = _interactions.useKeyboard; exports.useMove = _interactions.useMove; exports.usePress = _interactions.usePress; exports.useLongPress = _interactions.useLongPress; +var _listbox = require("@react-aria/listbox"); exports.useListBox = _listbox.useListBox; exports.useListBoxSection = _listbox.useListBoxSection; exports.useOption = _listbox.useOption; -+var _menu = require("@react-aria/menu"); exports.useMenu = _menu.useMenu; exports.useMenuItem = _menu.useMenuItem; exports.useMenuSection = _menu.useMenuSection; exports.useMenuTrigger = _menu.useMenuTrigger; ++var _menu = require("@react-aria/menu"); exports.useMenu = _menu.useMenu; exports.useMenuItem = _menu.useMenuItem; exports.useMenuSection = _menu.useMenuSection; exports.useMenuTrigger = _menu.useMenuTrigger; exports.useSubmenuTrigger = _menu.useSubmenuTrigger; +var _overlays = require("@react-aria/overlays"); exports.DismissButton = _overlays.DismissButton; exports.Overlay = _overlays.Overlay; exports.useOverlayTrigger = _overlays.useOverlayTrigger; exports.usePopover = _overlays.usePopover; +var _select = require("@react-aria/select"); exports.HiddenSelect = _select.HiddenSelect; exports.useSelect = _select.useSelect; +var _separator = require("@react-aria/separator"); exports.useSeparator = _separator.useSeparator; @@ -252,7 +252,7 @@ index b6a2ce3a780a09aeb416d7330ffcf6fd0b36b218..98e720a4660bdcd0384509786d04681a +var _visuallyHidden = require("@react-aria/visually-hidden"); exports.VisuallyHidden = _visuallyHidden.VisuallyHidden; exports.useVisuallyHidden = _visuallyHidden.useVisuallyHidden; +var _dialog = require("@react-aria/dialog"); exports.useDialog = _dialog.useDialog; diff --git a/dist/module.js b/dist/module.js -index a13ba8b37e27230ec01d4c31140127912208666c..97d6aa506f273263c2a99a9bc80731ac2c38eea6 100644 +index a13ba8b37e27230ec01d4c31140127912208666c..4208f8b7e8c3ff25b4eff8cce9017e8cfb8d3b4e 100644 --- a/dist/module.js +++ b/dist/module.js @@ -1,50 +1,14 @@ @@ -297,7 +297,7 @@ index a13ba8b37e27230ec01d4c31140127912208666c..97d6aa506f273263c2a99a9bc80731ac -export {useSearchField} from '@react-aria/searchfield'; -export {HiddenSelect, useHiddenSelect, useSelect} from '@react-aria/select'; -export {ListKeyboardDelegate} from '@react-aria/selection'; -+export {useMenu, useMenuItem, useMenuSection, useMenuTrigger} from '@react-aria/menu'; ++export {useMenu, useMenuItem, useMenuSection, useMenuTrigger, useSubmenuTrigger} from '@react-aria/menu'; +export {DismissButton, Overlay, useOverlayTrigger, usePopover} from '@react-aria/overlays'; +export {HiddenSelect, useSelect} from '@react-aria/select'; export {useSeparator} from '@react-aria/separator'; diff --git a/.yarn/patches/react-stately-npm-3.17.0-264cc7a43c.patch b/.yarn/patches/react-stately-npm-3.17.0-264cc7a43c.patch deleted file mode 100644 index 53c82149f04aa..0000000000000 --- a/.yarn/patches/react-stately-npm-3.17.0-264cc7a43c.patch +++ /dev/null @@ -1,149 +0,0 @@ -diff --git a/dist/main.js b/dist/main.js -index 8e3aafa76ccbbc4491666e9593220378c3e79c18..2ed9b74c20b63fb1f9e8552d61343b26540f78d9 100644 ---- a/dist/main.js -+++ b/dist/main.js -@@ -1,98 +1,8 @@ - "use strict"; -- - exports.__esModule = true; --exports.useTreeState = exports.useTooltipTriggerState = exports.useToggleState = exports.useTabListState = exports.Cell = exports.Row = exports.Column = exports.TableBody = exports.TableHeader = exports.useTableState = exports.useMultipleSelectionState = exports.useSliderState = exports.useSelectState = exports.useSearchFieldState = exports.useRadioGroupState = exports.useOverlayTriggerState = exports.useNumberFieldState = exports.useMenuTriggerState = exports.useSingleSelectListState = exports.useListState = exports.useTreeData = exports.useListData = exports.useAsyncList = exports.useCollection = exports.Section = exports.Item = exports.useTimeFieldState = exports.useDateRangePickerState = exports.useDatePickerState = exports.useDateFieldState = exports.useComboBoxState = exports.useCheckboxGroupState = exports.useRangeCalendarState = exports.useCalendarState = void 0; -- --var _calendar = require("@react-stately/calendar"); -- --exports.useCalendarState = _calendar.useCalendarState; --exports.useRangeCalendarState = _calendar.useRangeCalendarState; -- --var _checkbox = require("@react-stately/checkbox"); -- --exports.useCheckboxGroupState = _checkbox.useCheckboxGroupState; -- --var _combobox = require("@react-stately/combobox"); -- --exports.useComboBoxState = _combobox.useComboBoxState; -- --var _datepicker = require("@react-stately/datepicker"); -- --exports.useDateFieldState = _datepicker.useDateFieldState; --exports.useDatePickerState = _datepicker.useDatePickerState; --exports.useDateRangePickerState = _datepicker.useDateRangePickerState; --exports.useTimeFieldState = _datepicker.useTimeFieldState; -- --var _collections = require("@react-stately/collections"); -- --exports.Item = _collections.Item; --exports.Section = _collections.Section; --exports.useCollection = _collections.useCollection; -- --var _data = require("@react-stately/data"); -- --exports.useAsyncList = _data.useAsyncList; --exports.useListData = _data.useListData; --exports.useTreeData = _data.useTreeData; -- --var _list = require("@react-stately/list"); -- --exports.useListState = _list.useListState; --exports.useSingleSelectListState = _list.useSingleSelectListState; -- --var _menu = require("@react-stately/menu"); -- --exports.useMenuTriggerState = _menu.useMenuTriggerState; -- --var _numberfield = require("@react-stately/numberfield"); -- --exports.useNumberFieldState = _numberfield.useNumberFieldState; -- --var _overlays = require("@react-stately/overlays"); -- --exports.useOverlayTriggerState = _overlays.useOverlayTriggerState; -- --var _radio = require("@react-stately/radio"); -- --exports.useRadioGroupState = _radio.useRadioGroupState; -- --var _searchfield = require("@react-stately/searchfield"); -- --exports.useSearchFieldState = _searchfield.useSearchFieldState; -- --var _select = require("@react-stately/select"); -- --exports.useSelectState = _select.useSelectState; -- --var _slider = require("@react-stately/slider"); -- --exports.useSliderState = _slider.useSliderState; -- --var _selection = require("@react-stately/selection"); -- --exports.useMultipleSelectionState = _selection.useMultipleSelectionState; -- --var _table = require("@react-stately/table"); -- --exports.useTableState = _table.useTableState; --exports.TableHeader = _table.TableHeader; --exports.TableBody = _table.TableBody; --exports.Column = _table.Column; --exports.Row = _table.Row; --exports.Cell = _table.Cell; -- --var _tabs = require("@react-stately/tabs"); -- --exports.useTabListState = _tabs.useTabListState; -- --var _toggle = require("@react-stately/toggle"); -- --exports.useToggleState = _toggle.useToggleState; -- --var _tooltip = require("@react-stately/tooltip"); -- --exports.useTooltipTriggerState = _tooltip.useTooltipTriggerState; -- --var _tree = require("@react-stately/tree"); -- --exports.useTreeState = _tree.useTreeState; -+var _collections = require("@react-stately/collections"); exports.Item = _collections.Item; exports.Section = _collections.Section; -+var _menu = require("@react-stately/menu"); exports.useMenuTriggerState = _menu.useMenuTriggerState; -+var _overlays = require("@react-stately/overlays"); exports.useOverlayTriggerState = _overlays.useOverlayTriggerState; -+var _select = require("@react-stately/select"); exports.useSelectState = _select.useSelectState; -+var _slider = require("@react-stately/slider"); exports.useSliderState = _slider.useSliderState; -+var _tree = require("@react-stately/tree"); exports.useTreeState = _tree.useTreeState; -diff --git a/dist/module.js b/dist/module.js -index 37bdb24af4a8310cc702b2d9c1effe6aac261424..c098533fc2bbeb6b203d4350c2dcdd6e6663afd3 100644 ---- a/dist/module.js -+++ b/dist/module.js -@@ -1,33 +1,7 @@ --/* -- * Copyright 2020 Adobe. All rights reserved. -- * This file is licensed to you under the Apache License, Version 2.0 (the "License"); -- * you may not use this file except in compliance with the License. You may obtain a copy -- * of the License at http://www.apache.org/licenses/LICENSE-2.0 -- * -- * Unless required by applicable law or agreed to in writing, software distributed under -- * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -- * OF ANY KIND, either express or implied. See the License for the specific language -- * governing permissions and limitations under the License. -- */ -- -- --export {useCalendarState, useRangeCalendarState} from '@react-stately/calendar'; --export {useCheckboxGroupState} from '@react-stately/checkbox'; --export {useComboBoxState} from '@react-stately/combobox'; --export {useDateFieldState, useDatePickerState, useDateRangePickerState, useTimeFieldState} from '@react-stately/datepicker'; --export {Item, Section, useCollection} from '@react-stately/collections'; --export {useAsyncList, useListData, useTreeData} from '@react-stately/data'; --export {useListState, useSingleSelectListState} from '@react-stately/list'; -+// Slim barrel: only exports actually used -+export {Item, Section} from '@react-stately/collections'; - export {useMenuTriggerState} from '@react-stately/menu'; --export {useNumberFieldState} from '@react-stately/numberfield'; - export {useOverlayTriggerState} from '@react-stately/overlays'; --export {useRadioGroupState} from '@react-stately/radio'; --export {useSearchFieldState} from '@react-stately/searchfield'; - export {useSelectState} from '@react-stately/select'; - export {useSliderState} from '@react-stately/slider'; --export {useMultipleSelectionState} from '@react-stately/selection'; --export {useTableState, TableHeader, TableBody, Column, Row, Cell} from '@react-stately/table'; --export {useTabListState} from '@react-stately/tabs'; --export {useToggleState} from '@react-stately/toggle'; --export {useTooltipTriggerState} from '@react-stately/tooltip'; - export {useTreeState} from '@react-stately/tree'; diff --git a/.yarn/patches/react-stately-npm-3.35.0-f77b012b54.patch b/.yarn/patches/react-stately-npm-3.35.0-f77b012b54.patch new file mode 100644 index 0000000000000..5694c887ce9ef --- /dev/null +++ b/.yarn/patches/react-stately-npm-3.35.0-f77b012b54.patch @@ -0,0 +1,133 @@ +diff --git a/dist/main.js b/dist/main.js +index fab7548..6d19514 100644 +--- a/dist/main.js ++++ b/dist/main.js +@@ -1,78 +1,8 @@ + "use strict"; +- + exports.__esModule = true; +-exports.useTreeState = exports.useTreeData = exports.useTooltipTriggerState = exports.useToggleState = exports.useToggleGroupState = exports.useTimeFieldState = exports.useTableState = exports.useTableColumnResizeState = exports.useTabListState = exports.useSubmenuTriggerState = exports.useSliderState = exports.useSingleSelectListState = exports.useSelectState = exports.useSearchFieldState = exports.useRangeCalendarState = exports.useRadioGroupState = exports.useOverlayTriggerState = exports.useNumberFieldState = exports.useMultipleSelectionState = exports.useMenuTriggerState = exports.useListState = exports.useListData = exports.useDroppableCollectionState = exports.useDraggableCollectionState = exports.useDisclosureState = exports.useDisclosureGroupState = exports.useDateRangePickerState = exports.useDatePickerState = exports.useDateFieldState = exports.useComboBoxState = exports.useColorWheelState = exports.useColorSliderState = exports.useColorPickerState = exports.useColorFieldState = exports.useColorChannelFieldState = exports.useColorAreaState = exports.useCollection = exports.useCheckboxGroupState = exports.useCalendarState = exports.useAsyncList = exports.parseColor = exports.getColorChannels = exports.TableHeader = exports.TableBody = exports.Section = exports.Row = exports.Item = exports.FormValidationContext = exports.Column = exports.Cell = void 0; +-var _calendar = require("@react-stately/calendar"); +-exports.useCalendarState = _calendar.useCalendarState; +-exports.useRangeCalendarState = _calendar.useRangeCalendarState; +-var _checkbox = require("@react-stately/checkbox"); +-exports.useCheckboxGroupState = _checkbox.useCheckboxGroupState; +-var _color = require("@react-stately/color"); +-exports.getColorChannels = _color.getColorChannels; +-exports.parseColor = _color.parseColor; +-exports.useColorAreaState = _color.useColorAreaState; +-exports.useColorChannelFieldState = _color.useColorChannelFieldState; +-exports.useColorFieldState = _color.useColorFieldState; +-exports.useColorPickerState = _color.useColorPickerState; +-exports.useColorSliderState = _color.useColorSliderState; +-exports.useColorWheelState = _color.useColorWheelState; +-var _combobox = require("@react-stately/combobox"); +-exports.useComboBoxState = _combobox.useComboBoxState; +-var _datepicker = require("@react-stately/datepicker"); +-exports.useDateFieldState = _datepicker.useDateFieldState; +-exports.useDatePickerState = _datepicker.useDatePickerState; +-exports.useDateRangePickerState = _datepicker.useDateRangePickerState; +-exports.useTimeFieldState = _datepicker.useTimeFieldState; +-var _disclosure = require("@react-stately/disclosure"); +-exports.useDisclosureState = _disclosure.useDisclosureState; +-exports.useDisclosureGroupState = _disclosure.useDisclosureGroupState; +-var _dnd = require("@react-stately/dnd"); +-exports.useDraggableCollectionState = _dnd.useDraggableCollectionState; +-exports.useDroppableCollectionState = _dnd.useDroppableCollectionState; +-var _collections = require("@react-stately/collections"); +-exports.Item = _collections.Item; +-exports.Section = _collections.Section; +-exports.useCollection = _collections.useCollection; +-var _data = require("@react-stately/data"); +-exports.useAsyncList = _data.useAsyncList; +-exports.useListData = _data.useListData; +-exports.useTreeData = _data.useTreeData; +-var _list = require("@react-stately/list"); +-exports.useListState = _list.useListState; +-exports.useSingleSelectListState = _list.useSingleSelectListState; +-var _menu = require("@react-stately/menu"); +-exports.useMenuTriggerState = _menu.useMenuTriggerState; +-exports.useSubmenuTriggerState = _menu.useSubmenuTriggerState; +-var _numberfield = require("@react-stately/numberfield"); +-exports.useNumberFieldState = _numberfield.useNumberFieldState; +-var _overlays = require("@react-stately/overlays"); +-exports.useOverlayTriggerState = _overlays.useOverlayTriggerState; +-var _radio = require("@react-stately/radio"); +-exports.useRadioGroupState = _radio.useRadioGroupState; +-var _searchfield = require("@react-stately/searchfield"); +-exports.useSearchFieldState = _searchfield.useSearchFieldState; +-var _select = require("@react-stately/select"); +-exports.useSelectState = _select.useSelectState; +-var _slider = require("@react-stately/slider"); +-exports.useSliderState = _slider.useSliderState; +-var _selection = require("@react-stately/selection"); +-exports.useMultipleSelectionState = _selection.useMultipleSelectionState; +-var _table = require("@react-stately/table"); +-exports.useTableState = _table.useTableState; +-exports.TableHeader = _table.TableHeader; +-exports.TableBody = _table.TableBody; +-exports.Column = _table.Column; +-exports.Row = _table.Row; +-exports.Cell = _table.Cell; +-exports.useTableColumnResizeState = _table.useTableColumnResizeState; +-var _tabs = require("@react-stately/tabs"); +-exports.useTabListState = _tabs.useTabListState; +-var _toggle = require("@react-stately/toggle"); +-exports.useToggleState = _toggle.useToggleState; +-exports.useToggleGroupState = _toggle.useToggleGroupState; +-var _tooltip = require("@react-stately/tooltip"); +-exports.useTooltipTriggerState = _tooltip.useTooltipTriggerState; +-var _tree = require("@react-stately/tree"); +-exports.useTreeState = _tree.useTreeState; +-var _form = require("@react-stately/form"); +-exports.FormValidationContext = _form.FormValidationContext; ++var _collections = require("@react-stately/collections"); exports.Item = _collections.Item; exports.Section = _collections.Section; ++var _menu = require("@react-stately/menu"); exports.useMenuTriggerState = _menu.useMenuTriggerState; exports.useSubmenuTriggerState = _menu.useSubmenuTriggerState; ++var _overlays = require("@react-stately/overlays"); exports.useOverlayTriggerState = _overlays.useOverlayTriggerState; ++var _select = require("@react-stately/select"); exports.useSelectState = _select.useSelectState; ++var _slider = require("@react-stately/slider"); exports.useSliderState = _slider.useSliderState; ++var _tree = require("@react-stately/tree"); exports.useTreeState = _tree.useTreeState; +diff --git a/dist/module.js b/dist/module.js +index fe89bbc..07bbd23 100644 +--- a/dist/module.js ++++ b/dist/module.js +@@ -1,37 +1,7 @@ +-/* +- * Copyright 2020 Adobe. All rights reserved. +- * This file is licensed to you under the Apache License, Version 2.0 (the "License"); +- * you may not use this file except in compliance with the License. You may obtain a copy +- * of the License at http://www.apache.org/licenses/LICENSE-2.0 +- * +- * Unless required by applicable law or agreed to in writing, software distributed under +- * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +- * OF ANY KIND, either express or implied. See the License for the specific language +- * governing permissions and limitations under the License. +- */ +- +- +-export {useCalendarState, useRangeCalendarState} from '@react-stately/calendar'; +-export {useCheckboxGroupState} from '@react-stately/checkbox'; +-export {getColorChannels, parseColor, useColorAreaState, useColorChannelFieldState, useColorFieldState, useColorPickerState, useColorSliderState, useColorWheelState} from '@react-stately/color'; +-export {useComboBoxState} from '@react-stately/combobox'; +-export {useDateFieldState, useDatePickerState, useDateRangePickerState, useTimeFieldState} from '@react-stately/datepicker'; +-export {useDisclosureState, useDisclosureGroupState} from '@react-stately/disclosure'; +-export {useDraggableCollectionState, useDroppableCollectionState} from '@react-stately/dnd'; +-export {Item, Section, useCollection} from '@react-stately/collections'; +-export {useAsyncList, useListData, useTreeData} from '@react-stately/data'; +-export {useListState, useSingleSelectListState} from '@react-stately/list'; ++// Slim barrel: only exports actually used ++export {Item, Section} from '@react-stately/collections'; + export {useMenuTriggerState, useSubmenuTriggerState} from '@react-stately/menu'; +-export {useNumberFieldState} from '@react-stately/numberfield'; + export {useOverlayTriggerState} from '@react-stately/overlays'; +-export {useRadioGroupState} from '@react-stately/radio'; +-export {useSearchFieldState} from '@react-stately/searchfield'; + export {useSelectState} from '@react-stately/select'; + export {useSliderState} from '@react-stately/slider'; +-export {useMultipleSelectionState} from '@react-stately/selection'; +-export {useTableState, TableHeader, TableBody, Column, Row, Cell, useTableColumnResizeState} from '@react-stately/table'; +-export {useTabListState} from '@react-stately/tabs'; +-export {useToggleState, useToggleGroupState} from '@react-stately/toggle'; +-export {useTooltipTriggerState} from '@react-stately/tooltip'; + export {useTreeState} from '@react-stately/tree'; +-export {FormValidationContext} from '@react-stately/form'; diff --git a/README.md b/README.md index 5c8f065f1849b..84109f3b1b39b 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,3 @@ We're hiring developers, technical support, and product managers all the time. C - [Facebook](https://www.facebook.com/RocketChatApp) - [LinkedIn](https://www.linkedin.com/company/rocket-chat) - [YouTube](https://www.youtube.com/channel/UCin9nv7mUjoqrRiwrzS5UVQ) - -# ๐Ÿ—’๏ธ Credits - -- Emoji provided graciously by [JoyPixels](https://www.joypixels.com). diff --git a/apps/meteor/.docker/Dockerfile.alpine b/apps/meteor/.docker/Dockerfile.alpine index 2bee7b45f36e3..307c801f261a0 100644 --- a/apps/meteor/.docker/Dockerfile.alpine +++ b/apps/meteor/.docker/Dockerfile.alpine @@ -20,7 +20,7 @@ RUN cd /app/bundle/programs/server \ && find /app/bundle/programs/server/npm/node_modules -type f -name '*.map' -delete \ && find /app/bundle/programs/web.browser -type f -name '*.map' -delete -FROM node:22.22.3-alpine3.23 +FROM node:22.22.3-alpine3.23 AS release-standard LABEL maintainer="buildmaster@rocket.chat" @@ -65,3 +65,29 @@ WORKDIR /app/bundle EXPOSE 3000 CMD ["node", "main.js"] + +FROM rocketchat/dhi-node:22.22.3-alpine3.23-fips AS release-fips + +LABEL maintainer="buildmaster@rocket.chat" + +# needs a mongo instance - defaults to container linking with alias 'mongo' +ENV DEPLOY_METHOD=docker \ + NODE_ENV=production \ + MONGO_URL=mongodb://mongo:27017/rocketchat \ + HOME=/tmp \ + PORT=3000 \ + ROOT_URL=http://localhost:3000 \ + Accounts_AvatarStorePath=/app/uploads \ + APPS_ENGINE_RUNTIME_BACKEND=node + +USER node + +COPY --from=builder --chown=node:node /app /app + +VOLUME /app/uploads + +WORKDIR /app/bundle + +EXPOSE 3000 + +CMD ["node", "--force-fips", "main.js"] diff --git a/apps/meteor/.mocharc.api.apps.js b/apps/meteor/.mocharc.api.apps.js new file mode 100644 index 0000000000000..7eea51b389f01 --- /dev/null +++ b/apps/meteor/.mocharc.api.apps.js @@ -0,0 +1,15 @@ +'use strict'; + +/* + * Mocha configuration for REST API integration tests. + */ + +module.exports = /** @satisfies {import('mocha').MochaOptions} */ ({ + ...require('./.mocharc.base.json'), // see https://github.com/mochajs/mocha/issues/3916 + timeout: 10000, + bail: false, + retries: 0, + file: 'tests/end-to-end/teardown.ts', + reporter: 'tests/end-to-end/reporter.ts', + spec: ['tests/end-to-end/apps/*'], +}); diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index bcbb789e3dd98..6837d59437853 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -9,9 +9,14 @@ const base = require('./.mocharc.base.json'); module.exports = { ...base, // see https://github.com/mochajs/mocha/issues/3916 exit: true, + // getUserInfo.spec.ts lives under server/api/lib but is a Jest test (run via jest.config.ts), + // so it must be excluded from the mocha `server/api/lib/**` glob below. Same for the + // business-hour spec under server/lib/omnichannel. + ignore: ['server/api/lib/getUserInfo.spec.ts', 'server/lib/omnichannel/business-hour/**/*.spec.ts'], spec: [ 'server/lib/callbacks.spec.ts', 'server/lib/cas/*.spec.ts', + 'server/lib/messages/**/*.spec.ts', 'server/lib/ldap/*.spec.ts', 'server/lib/ldap/**/*.spec.ts', 'server/lib/dataExport/**/*.spec.ts', @@ -25,14 +30,16 @@ module.exports = { 'tests/unit/lib/**/*.tests.ts', 'server/routes/avatar/**/*.spec.ts', 'tests/unit/lib/**/*.spec.ts', + 'tests/unit/server/**/*.tests.js', 'tests/unit/server/**/*.tests.ts', 'tests/unit/server/**/*.spec.ts', - 'app/2fa/server/**/*.spec.ts', - 'app/api/server/lib/**/*.spec.ts', - 'app/file-upload/server/**/*.spec.ts', - 'app/statistics/server/**/*.spec.ts', - 'app/livechat/server/lib/**/*.spec.ts', - 'app/push/server/**/*.spec.ts', - 'app/utils/server/**/*.spec.ts', + 'server/lib/2fa/**/*.spec.ts', + 'server/api/lib/**/*.spec.ts', + 'server/lib/media/**/*.spec.ts', + 'server/lib/statistics/**/*.spec.ts', + 'server/meteor-methods/**/*.spec.ts', + 'server/lib/omnichannel/**/*.spec.ts', + 'server/lib/notifications/push/**/*.spec.ts', + 'server/lib/utils/**/*.spec.ts', ], }; diff --git a/apps/meteor/.scripts/run-ha.ts b/apps/meteor/.scripts/run-ha.ts index a1a3775000690..2b53a960d7c06 100644 --- a/apps/meteor/.scripts/run-ha.ts +++ b/apps/meteor/.scripts/run-ha.ts @@ -56,7 +56,7 @@ async function runMain(config: IConfig): Promise { async function runInstance(config: IConfig): Promise { // Desctructuring the unused variables allows us to omit them in the `mainConfig` - // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { customEnv, parentEnv, ...mainConfig } = config; const env = { diff --git a/apps/meteor/.scripts/version.js b/apps/meteor/.scripts/version.js index 9ad130f449f4f..d33237361fbc1 100644 --- a/apps/meteor/.scripts/version.js +++ b/apps/meteor/.scripts/version.js @@ -3,7 +3,7 @@ const path = require('path'); let pkgJson = {}; try { - // eslint-disable-next-line import/no-dynamic-require + // eslint-disable-next-line import-x/no-dynamic-require pkgJson = require(path.resolve(process.cwd(), './package.json')); } catch (err) { console.error('no root package.json found'); diff --git a/apps/meteor/.stylelintignore b/apps/meteor/.stylelintignore index 242e9a4701c3a..edc9384135b98 100644 --- a/apps/meteor/.stylelintignore +++ b/apps/meteor/.stylelintignore @@ -1,3 +1,2 @@ app/meteor-autocomplete/client/autocomplete.css -app/emoji-emojione/client/*.css storybook-static diff --git a/apps/meteor/app/2fa/server/code/index.ts b/apps/meteor/app/2fa/server/code/index.ts deleted file mode 100644 index cbba0724214f3..0000000000000 --- a/apps/meteor/app/2fa/server/code/index.ts +++ /dev/null @@ -1,250 +0,0 @@ -import crypto from 'node:crypto'; - -import type { IUser, IMethodConnection } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; - -import { EmailCheck } from './EmailCheck'; -import type { ICodeCheck } from './ICodeCheck'; -import { PasswordCheckFallback } from './PasswordCheckFallback'; -import { TOTPCheck } from './TOTPCheck'; -import { normalizeHeaders } from '../../../lib/server/functions/getModifiedHttpHeaders'; -import { settings } from '../../../settings/server'; - -export interface ITwoFactorOptions { - disablePasswordFallback?: boolean; - disableRememberMe?: boolean; - requireSecondFactor?: boolean; // whether any two factor should be required -} - -const totpCheck = new TOTPCheck(); -export const emailCheck = new EmailCheck(); -const passwordCheckFallback = new PasswordCheckFallback(); - -const checkMethods = new Map(); - -checkMethods.set(totpCheck.name, totpCheck); -checkMethods.set(emailCheck.name, emailCheck); - -function getMethodByNameOrFirstActiveForUser(user: IUser, name?: string): ICodeCheck | undefined { - if (name && checkMethods.has(name)) { - return checkMethods.get(name); - } - - return Array.from(checkMethods.values()).find((method) => method.isEnabled(user)); -} - -function getAvailableMethodNames(user: IUser): string[] { - return ( - Array.from(checkMethods) - .filter(([, method]) => method.isEnabled(user)) - .map(([name]) => name) || [] - ); -} - -export async function getUserForCheck(userId: string): Promise { - return Users.findOneById(userId, { - projection: { - emails: 1, - language: 1, - createdAt: 1, - services: 1, - }, - }); -} - -export function getFingerprintFromConnection(connection: IMethodConnection): string { - const data = JSON.stringify({ - userAgent: connection.httpHeaders['user-agent'], - clientAddress: connection.clientAddress, - }); - - return crypto.createHash('md5').update(data).digest('hex'); -} - -function getRememberDate(from: Date = new Date()): Date | undefined { - const rememberFor = parseInt(settings.get('Accounts_TwoFactorAuthentication_RememberFor') as string, 10); - - if (rememberFor <= 0) { - return; - } - - const expires = new Date(from); - expires.setSeconds(expires.getSeconds() + rememberFor); - - return expires; -} - -function isAuthorizedForToken(connection: IMethodConnection, user: IUser, options: ITwoFactorOptions): boolean { - // Resolve the current login token from both transports: - // - DDP: the login flow registers it in `Accounts._accountData`, read via `_getLoginToken`. - // - REST: it is not registered in account data, so it is carried on `connection.token`. - // Both are needed; REST is the fallback that fixes `bypassTwoFactor` PATs (SUP-1064). - const currentToken = Accounts._getLoginToken(connection.id) || connection.token; - const tokenObject = user.services?.resume?.loginTokens?.find((i) => i.hashedToken === currentToken); - - if (!tokenObject) { - return false; - } - - // if any two factor is required, early abort - if (options.requireSecondFactor) { - return false; - } - - if ('bypassTwoFactor' in tokenObject && tokenObject.bypassTwoFactor === true) { - return true; - } - - // Skip 2FA for a freshly registered user who has not set up any 2FA method yet, - // until the grace period that starts at registration expires. - // (e.g. the Setup Wizard saving settings between steps before any 2FA is configured) - const rememberPeriodEnd = user.createdAt && getRememberDate(user.createdAt); - const isWithinRememberPeriod = rememberPeriodEnd && rememberPeriodEnd >= new Date(); - const hasNoTwoFactorMethod = getAvailableMethodNames(user).length === 0; - if (isWithinRememberPeriod && hasNoTwoFactorMethod) { - return true; - } - - if (options.disableRememberMe === true) { - return false; - } - - if (!tokenObject.twoFactorAuthorizedUntil || !tokenObject.twoFactorAuthorizedHash) { - return false; - } - - if (tokenObject.twoFactorAuthorizedUntil < new Date()) { - return false; - } - - if (tokenObject.twoFactorAuthorizedHash !== getFingerprintFromConnection(connection)) { - return false; - } - - return true; -} - -async function rememberAuthorization(connection: IMethodConnection, user: IUser): Promise { - // Same dual-transport resolution as `isAuthorizedForToken`: DDP reads from `Accounts._accountData` - // via `_getLoginToken`, REST falls back to the token carried on `connection.token`. - const currentToken = Accounts._getLoginToken(connection.id) || connection.token; - - const expires = getRememberDate(); - if (!expires) { - return; - } - - if (!currentToken) { - return; - } - - await Users.setTwoFactorAuthorizationHashAndUntilForUserIdAndToken( - user._id, - currentToken, - getFingerprintFromConnection(connection), - expires, - ); -} - -interface ICheckCodeForUser { - user: IUser | string; - code?: string; - method?: string; - options?: ITwoFactorOptions; - connection?: IMethodConnection; -} - -const getSecondFactorMethod = (user: IUser, method: string | undefined, options: ITwoFactorOptions): ICodeCheck | undefined => { - // try first getting one of the available methods or the one that was already provided - const selectedMethod = getMethodByNameOrFirstActiveForUser(user, method); - if (selectedMethod) { - return selectedMethod; - } - - // if none found but a second factor is required, chose the password check - if (options.requireSecondFactor) { - return passwordCheckFallback; - } - - // check if password fallback is enabled - if (!options.disablePasswordFallback && passwordCheckFallback.isEnabled(user, !!options.requireSecondFactor)) { - return passwordCheckFallback; - } -}; - -export async function checkCodeForUser({ user, code, method, options = {}, connection }: ICheckCodeForUser): Promise { - if (process.env.TEST_MODE && !options.requireSecondFactor) { - return true; - } - - if (!settings.get('Accounts_TwoFactorAuthentication_Enabled')) { - return true; - } - - let existingUser: IUser | null; - - if (typeof user === 'string') { - existingUser = await getUserForCheck(user); - } else { - existingUser = user; - } - - if (!existingUser) { - throw new Meteor.Error('totp-user-not-found', 'TOTP User not found'); - } - - const headers = normalizeHeaders(connection?.httpHeaders); - - if (!code && !method && headers?.['x-2fa-code'] && headers['x-2fa-method']) { - code = headers['x-2fa-code']; - method = headers['x-2fa-method']; - } - - if (connection && isAuthorizedForToken(connection, existingUser, options)) { - return true; - } - - // select a second factor method or return if none is found/available - const selectedMethod = getSecondFactorMethod(existingUser, method, options); - if (!selectedMethod) { - return true; - } - - const data = await selectedMethod.processInvalidCode(existingUser); - - const availableMethods = getAvailableMethodNames(existingUser); - - if (!code) { - throw new Meteor.Error('totp-required', 'TOTP Required', { - method: selectedMethod.name, - ...data, - availableMethods, - }); - } - - const valid = await selectedMethod.verify(existingUser, code, options.requireSecondFactor); - if (!valid) { - const tooManyFailedAttempts = await selectedMethod.maxFaildedAttemtpsReached(existingUser); - if (tooManyFailedAttempts) { - throw new Meteor.Error('totp-max-attempts', 'TOTP Maximun Failed Attempts Reached', { - method: selectedMethod.name, - ...data, - availableMethods, - }); - } - - throw new Meteor.Error('totp-invalid', 'TOTP Invalid', { - method: selectedMethod.name, - ...data, - availableMethods, - }); - } - - if (options.disableRememberMe !== true && connection) { - await rememberAuthorization(connection, existingUser); - } - - return true; -} diff --git a/apps/meteor/app/2fa/server/index.ts b/apps/meteor/app/2fa/server/index.ts deleted file mode 100644 index 783232a610eb2..0000000000000 --- a/apps/meteor/app/2fa/server/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import './MethodInvocationOverride'; -import './methods/checkCodesRemaining'; -import './methods/disable'; -import './methods/enable'; -import './methods/regenerateCodes'; -import './methods/validateTempToken'; -import './loginHandler'; diff --git a/apps/meteor/app/2fa/server/loginHandler.ts b/apps/meteor/app/2fa/server/loginHandler.ts deleted file mode 100644 index 772e3cff93696..0000000000000 --- a/apps/meteor/app/2fa/server/loginHandler.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Accounts } from 'meteor/accounts-base'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; -import { OAuth } from 'meteor/oauth'; - -import { checkCodeForUser } from './code/index'; -import { callbacks } from '../../../server/lib/callbacks'; - -const isMeteorError = (error: any): error is Meteor.Error => { - return error?.meteorError !== undefined; -}; - -const isCredentialWithError = (credential: any): credential is { error: Error } => { - return credential?.error !== undefined; -}; - -Accounts.registerLoginHandler('totp', function (options) { - if (!options.totp?.code) { - return; - } - - // @ts-expect-error - not sure how to type this yet - return Accounts._runLoginHandlers(this, options.totp.login); -}); - -callbacks.add( - 'onValidateLogin', - async (login) => { - if ( - !login.user || - login.type === 'resume' || - login.type === 'proxy' || - login.type === 'cas' || - (login.type === 'password' && login.methodName === 'resetPassword') || - login.methodName === 'verifyEmail' - ) { - return login; - } - - const [loginArgs] = login.methodArguments; - const { totp } = loginArgs; - - await checkCodeForUser({ - user: login.user, - code: totp?.code, - options: { disablePasswordFallback: true }, - }); - - return login; - }, - callbacks.priority.MEDIUM, - '2fa', -); - -const copyTo = (from: T, to: T): T => { - Object.getOwnPropertyNames(to).forEach((key) => { - const idx: keyof T = key as keyof T; - to[idx] = from[idx]; - }); - - return to; -}; - -const recreateError = (errorDoc: Error | Meteor.Error): Error | Meteor.Error => { - if (isMeteorError(errorDoc)) { - const error = new Meteor.Error(''); - return copyTo(errorDoc, error); - } - - const error = new Error(); - return copyTo(errorDoc, error); -}; - -OAuth._retrievePendingCredential = async function (key, ...args): Promise { - const credentialSecret = args.length > 0 && args[0] !== undefined ? args[0] : undefined; - check(key, String); - - const pendingCredential = await OAuth._pendingCredentials.findOneAsync({ - key, - credentialSecret, - }); - - if (!pendingCredential) { - return; - } - - if (isCredentialWithError(pendingCredential.credential)) { - await OAuth._pendingCredentials.removeAsync({ - _id: pendingCredential._id, - }); - return recreateError(pendingCredential.credential.error); - } - - // Work-around to make the credentials reusable for 2FA - const future = new Date(); - future.setMinutes(future.getMinutes() + 2); - - await OAuth._pendingCredentials.updateAsync( - { - _id: pendingCredential._id, - }, - { - $set: { - createdAt: future, - }, - }, - ); - - return OAuth.openSecret(pendingCredential.credential); -}; diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts deleted file mode 100644 index 1b5bac10a65df..0000000000000 --- a/apps/meteor/app/api/server/api.ts +++ /dev/null @@ -1,126 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import type { Router } from '@rocket.chat/http-router'; -import { Logger } from '@rocket.chat/logger'; -import type express from 'express'; -import { WebApp } from 'meteor/webapp'; - -import { APIClass } from './ApiClass'; -import { cors } from './middlewares/cors'; -import { loggerMiddleware } from './middlewares/logger'; -import { metricsMiddleware } from './middlewares/metrics'; -import { remoteAddressMiddleware } from './middlewares/remoteAddressMiddleware'; -import { tracerSpanMiddleware } from './middlewares/tracer'; -import { type APIActionHandler, RocketChatAPIRouter } from './router'; -import { metrics } from '../../metrics/server'; -import { settings } from '../../settings/server'; - -const logger = new Logger('API'); - -export type Prettify = { - [K in keyof T]: T[K]; -} & unknown; - -export type RateLimiterOptions = { - numRequestsAllowed?: number; - intervalTimeInMS?: number; -}; - -export const defaultRateLimiterOptions: RateLimiterOptions = { - numRequestsAllowed: settings.get('API_Enable_Rate_Limiter_Limit_Calls_Default'), - intervalTimeInMS: settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'), -}; - -const createApi = function _createApi(options: { version?: string; useDefaultAuth?: true } = {}): APIClass { - return new APIClass({ - apiPath: '', - useDefaultAuth: false, - prettyJson: process.env.NODE_ENV === 'development', - ...options, - }); -}; - -export const API: { - api: Router<'/api', any, APIActionHandler>; - v1: APIClass<'/v1'>; - default: APIClass; - ApiClass: typeof APIClass; - channels?: { - create: { - validate: (params: { - user: { value: string }; - name?: { key: string; value?: string }; - members?: { key: string; value?: string[] }; - customFields?: { key: string; value?: string }; - teams?: { key: string; value?: string[] }; - teamId?: { key: string; value?: string }; - }) => Promise; - execute: ( - userId: string, - params: { - name?: string; - members?: string[]; - customFields?: Record; - extraData?: Record; - readOnly?: boolean; - }, - ) => Promise<{ channel: IRoom }>; - }; - }; -} = { - ApiClass: APIClass, - api: new RocketChatAPIRouter('/api'), - v1: createApi({ - version: 'v1', - useDefaultAuth: true, - }), - default: createApi({}), -}; - -settings.watch('Accounts_CustomFields', (value) => { - if (!value) { - return API.v1?.setLimitedCustomFields([]); - } - try { - const customFields = JSON.parse(value); - const nonPublicCustomFields = Object.keys(customFields).filter((customFieldKey) => customFields[customFieldKey].public !== true); - API.v1.setLimitedCustomFields(nonPublicCustomFields); - } catch (error) { - console.warn('Invalid Custom Fields', error); - } -}); - -settings.watch('API_Enable_Rate_Limiter_Limit_Time_Default', (value) => { - defaultRateLimiterOptions.intervalTimeInMS = value; - API.v1.reloadRoutesToRefreshRateLimiter(); -}); - -settings.watch('API_Enable_Rate_Limiter_Limit_Calls_Default', (value) => { - defaultRateLimiterOptions.numRequestsAllowed = value; - API.v1.reloadRoutesToRefreshRateLimiter(); -}); - -export const startRestAPI = () => { - (WebApp.rawConnectHandlers as unknown as ReturnType).use( - API.api - .use( - metricsMiddleware({ - basePathRegex: new RegExp(/^\/api\/v1\//), - api: API.v1, - settings, - endpointTimeSummary: metrics.rocketchatRestApi, - endpointTimeHistogram: metrics.rocketchatRestApiSeconds, - responseSizeHistogram: metrics.rocketchatRestApiResponseSizeBytes, - activeRequestsGauge: metrics.rocketchatRestApiActiveRequests, - }), - ) - .use(tracerSpanMiddleware) - .use(remoteAddressMiddleware) - .use(cors(settings)) - .use(loggerMiddleware(logger)) - .use(API.v1.router) - .use(API.default.router).router, - ); -}; - -export type ExtractApiClassEndpoints> = - TApi extends APIClass ? (TOperations extends { method: string } ? Prettify : never) : never; diff --git a/apps/meteor/app/api/server/index.ts b/apps/meteor/app/api/server/index.ts deleted file mode 100644 index 5a6a6f06cbbab..0000000000000 --- a/apps/meteor/app/api/server/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -import './ajv'; -import './helpers/composeRoomWithLastMessage'; -import './helpers/getPaginationItems'; -import './helpers/getUserFromParams'; -import './helpers/getUserInfo'; -import './helpers/isUserFromParams'; -import './helpers/parseJsonQuery'; -import './default/info'; -import './v1/assets'; -import './v1/calendar'; -import './v1/call-history'; -import './v1/channels'; -import './v1/chat'; -import './v1/cloud'; -import './v1/commands'; -import './v1/e2e'; -import './v1/emoji-custom'; -import './v1/groups'; -import './v1/im'; -import './v1/integrations'; -import './v1/invites'; -import './v1/import'; -import './v1/ldap'; -import './v1/media-calls'; -import './v1/misc'; -import './v1/permissions'; -import './v1/presence'; -import './v1/push'; -import './v1/roles'; -import './v1/rooms'; -import './v1/settings'; -import './v1/stats'; -import './v1/subscriptions'; -import './v1/users'; -import './v1/videoConference'; -import './v1/autotranslate'; -import './v1/webdav'; -import './v1/oauthapps'; -import './v1/custom-sounds'; -import './v1/custom-user-status'; -import './v1/instances'; -import './v1/banners'; -import './v1/email-inbox'; -import './v1/mailer'; -import './v1/teams'; -import './v1/moderation'; -import './v1/uploads'; - -// This has to come last so all endpoints are registered before generating the OpenAPI documentation -import './default/openApi'; - -export { API, defaultRateLimiterOptions } from './api'; diff --git a/apps/meteor/app/api/server/lib/getServerInfo.ts b/apps/meteor/app/api/server/lib/getServerInfo.ts deleted file mode 100644 index adaf6ec6998c5..0000000000000 --- a/apps/meteor/app/api/server/lib/getServerInfo.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { IWorkspaceInfo } from '@rocket.chat/core-typings'; -import { License } from '@rocket.chat/license'; - -import { getTrimmedServerVersion } from './getTrimmedServerVersion'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { - getCachedSupportedVersionsToken, - wrapPromise, -} from '../../../cloud/server/functions/supportedVersionsToken/supportedVersionsToken'; -import { settings } from '../../../settings/server'; -import { Info, minimumClientVersions } from '../../../utils/rocketchat.info'; - -export async function getServerInfo(userId?: string): Promise { - const hasPermissionToViewStatistics = userId && (await hasPermissionAsync(userId, 'view-statistics')); - const supportedVersionsToken = await wrapPromise(getCachedSupportedVersionsToken()); - const cloudWorkspaceId = settings.get('Cloud_Workspace_Id'); - - return { - workspaceUrl: License.getWorkspaceUrl(), - hashedWorkspaceUrl: License.getHashedWorkspaceUrl(), - version: getTrimmedServerVersion(), - ...(hasPermissionToViewStatistics && { - info: { - ...Info, - }, - version: Info.version, - }), - - minimumClientVersions, - ...(supportedVersionsToken.success && - supportedVersionsToken.result && { - supportedVersions: { signed: supportedVersionsToken.result }, - }), - - cloudWorkspaceId, - }; -} diff --git a/apps/meteor/app/api/server/lib/getTrimmedServerVersion.ts b/apps/meteor/app/api/server/lib/getTrimmedServerVersion.ts deleted file mode 100644 index c0ceb9e880d5c..0000000000000 --- a/apps/meteor/app/api/server/lib/getTrimmedServerVersion.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Info } from '../../../utils/rocketchat.info'; - -// Removes the patch version from the server version string -export const getTrimmedServerVersion = (): string => Info.version.replace(/(\d+\.\d+).*/, '$1'); diff --git a/apps/meteor/app/api/server/lib/integrations.ts b/apps/meteor/app/api/server/lib/integrations.ts deleted file mode 100644 index 13b4d286b3058..0000000000000 --- a/apps/meteor/app/api/server/lib/integrations.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { IIntegration, IUser } from '@rocket.chat/core-typings'; -import { Integrations } from '@rocket.chat/models'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; - -const hasIntegrationsPermission = async (userId: string, integration: IIntegration): Promise => { - const type = integration.type === 'webhook-incoming' ? 'incoming' : 'outgoing'; - - if (await hasPermissionAsync(userId, `manage-${type}-integrations`)) { - return true; - } - - if (userId === integration._createdBy?._id) { - return hasPermissionAsync(userId, `manage-own-${type}-integrations`); - } - - return false; -}; - -export const findOneIntegration = async ({ - userId, - integrationId, - createdBy, -}: { - userId: string; - integrationId: string; - createdBy?: IUser['_id']; -}): Promise => { - const integration = await Integrations.findOneByIdAndCreatedByIfExists({ - _id: integrationId, - createdBy, - }); - if (!integration) { - throw new Error('The integration does not exists.'); - } - if (!(await hasIntegrationsPermission(userId, integration))) { - throw new Error('not-authorized'); - } - return integration; -}; diff --git a/apps/meteor/app/api/server/lib/messages.ts b/apps/meteor/app/api/server/lib/messages.ts deleted file mode 100644 index cce09a1b27418..0000000000000 --- a/apps/meteor/app/api/server/lib/messages.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { IMessage, IUser } from '@rocket.chat/core-typings'; -import { Rooms, Messages, Users } from '@rocket.chat/models'; -import type { FindOptions } from 'mongodb'; - -import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; - -export async function findMentionedMessages({ - uid, - roomId, - pagination: { offset, count, sort }, -}: { - uid: string; - roomId: string; - pagination: { offset: number; count: number; sort: FindOptions['sort'] }; -}): Promise<{ - messages: IMessage[]; - count: number; - offset: number; - total: number; -}> { - const room = await Rooms.findOneById(roomId); - if (!room || !(await canAccessRoomAsync(room, { _id: uid }))) { - throw new Error('error-not-allowed'); - } - const user = await Users.findOneById>(uid, { projection: { username: 1 } }); - if (!user) { - throw new Error('invalid-user'); - } - - const { cursor, totalCount } = Messages.findPaginatedVisibleByMentionAndRoomId(user.username, roomId, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - }); - - const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); - - return { - messages, - count: messages.length, - offset, - total, - }; -} - -export async function findStarredMessages({ - uid, - roomId, - pagination: { offset, count, sort }, -}: { - uid: string; - roomId: string; - pagination: { offset: number; count: number; sort: FindOptions['sort'] }; -}): Promise<{ - messages: IMessage[]; - count: number; - offset: number; - total: number; -}> { - const room = await Rooms.findOneById(roomId); - if (!room || !(await canAccessRoomAsync(room, { _id: uid }))) { - throw new Error('error-not-allowed'); - } - const user = await Users.findOneById>(uid, { projection: { username: 1 } }); - if (!user) { - throw new Error('invalid-user'); - } - - const { cursor, totalCount } = Messages.findStarredByUserAtRoom(uid, roomId, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - }); - - const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); - - return { - messages, - count: messages.length, - offset, - total, - }; -} - -export async function findDiscussionsFromRoom({ - uid, - roomId, - text, - pagination: { offset, count, sort }, -}: { - uid: string; - roomId: string; - text: string; - pagination: { offset: number; count: number; sort: FindOptions['sort'] }; -}): Promise<{ - messages: IMessage[]; - count: number; - offset: number; - total: number; -}> { - const room = await Rooms.findOneById(roomId); - - if (!room || !(await canAccessRoomAsync(room, { _id: uid }))) { - throw new Error('error-not-allowed'); - } - - const { cursor, totalCount } = await Messages.findDiscussionsByRoomAndText(roomId, text, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - }); - - const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); - - return { - messages, - count: messages.length, - offset, - total, - }; -} diff --git a/apps/meteor/app/api/server/lib/rooms.ts b/apps/meteor/app/api/server/lib/rooms.ts deleted file mode 100644 index f93551e4e9c08..0000000000000 --- a/apps/meteor/app/api/server/lib/rooms.ts +++ /dev/null @@ -1,202 +0,0 @@ -import type { IRoom, IRoomAbacRedaction, ISubscription, RoomAdminFieldsType, RoomType } from '@rocket.chat/core-typings'; -import { Rooms, Subscriptions } from '@rocket.chat/models'; -import type { FindOptions, Sort } from 'mongodb'; - -import { scopeAdminRoomsForAbac } from './scopeAdminRoomsForAbac'; -import { adminFields } from '../../../../lib/rooms/adminFields'; -import { hasAtLeastOnePermissionAsync, hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { stripABACManagedFieldsForAdmin } from '../../../authorization/server/lib/isABACManagedRoom'; - -export async function findAdminRooms({ - uid, - filter, - types = [], - pagination: { offset, count, sort }, -}: { - uid: string; - filter: string; - types: Array; - pagination: { offset: number; count: number; sort: Sort }; -}): Promise<{ - rooms: Array & IRoomAbacRedaction>; - count: number; - offset: number; - total: number; -}> { - if (!(await hasPermissionAsync(uid, 'view-room-administration'))) { - throw new Error('error-not-authorized'); - } - const name = filter?.trim(); - const discussion = types?.includes('discussions'); - const includeTeams = types?.includes('teams'); - const typesToRemove = ['discussions', 'teams']; - const showTypes = Array.isArray(types) ? types.filter((type): type is RoomType => !typesToRemove.includes(type)) : []; - const options: FindOptions = { - projection: adminFields, - skip: offset, - limit: count, - }; - - const result = Rooms.findByNameOrFnameContainingAndTypes(name, showTypes, discussion, includeTeams, options); - - const { cursor, totalCount } = result; - - const [rooms, total] = await Promise.all([ - cursor - .sort(sort || { default: -1, name: 1 }) - .map(stripABACManagedFieldsForAdmin) - .toArray(), - totalCount, - ]); - - return { - rooms: await scopeAdminRoomsForAbac(rooms, uid), - count: rooms.length, - offset, - total, - }; -} - -export async function findAdminRoom({ - uid, - rid, -}: { - uid: string; - rid: string; -}): Promise<(Pick & IRoomAbacRedaction) | null> { - if (!(await hasPermissionAsync(uid, 'view-room-administration'))) { - throw new Error('error-not-authorized'); - } - - const room = await Rooms.findOneById(rid, { projection: adminFields }); - if (!room) { - return null; - } - - const [scoped] = await scopeAdminRoomsForAbac([stripABACManagedFieldsForAdmin(room)], uid); - return scoped ?? null; -} - -export async function findChannelAndPrivateAutocomplete({ uid, selector }: { uid: string; selector: { name: string } }): Promise<{ - items: IRoom[]; -}> { - const options: FindOptions = { - projection: { - _id: 1, - fname: 1, - name: 1, - t: 1, - avatarETag: 1, - encrypted: 1, - }, - limit: 10, - sort: { - name: 1, - }, - }; - - const userRoomsIds = (await Subscriptions.findByUserId(uid, { projection: { rid: 1 } }).toArray()).map( - (item: Pick) => item.rid, - ); - - const rooms = await Rooms.findRoomsWithoutDiscussionsByRoomIds(selector.name, userRoomsIds, options).toArray(); - - return { - items: rooms, - }; -} - -export async function findAdminRoomsAutocomplete({ uid, selector }: { uid: string; selector: { name: string } }): Promise<{ - items: IRoom[]; -}> { - if (!(await hasAtLeastOnePermissionAsync(uid, ['view-room-administration', 'can-audit']))) { - throw new Error('error-not-authorized'); - } - const options: FindOptions = { - projection: { - _id: 1, - fname: 1, - name: 1, - t: 1, - avatarETag: 1, - encrypted: 1, - }, - limit: 10, - sort: { - name: 1, - }, - }; - - const rooms = await Rooms.findRoomsByNameOrFnameStarting(selector.name, options).toArray(); - - return { - items: rooms, - }; -} - -export async function findChannelAndPrivateAutocompleteWithPagination({ - uid, - selector, - pagination: { offset, count, sort }, -}: { - uid: string; - selector: { name: string }; - pagination: { offset: number; count: number; sort: Sort }; -}): Promise<{ - items: IRoom[]; - total: number; -}> { - const userRoomsIds = (await Subscriptions.findByUserId(uid, { projection: { rid: 1 } }).toArray()).map( - (item: Pick) => item.rid, - ); - - const options: FindOptions = { - projection: { - _id: 1, - fname: 1, - name: 1, - t: 1, - avatarETag: 1, - }, - sort: sort || { name: 1 }, - skip: offset, - limit: count, - }; - - const { cursor, totalCount } = Rooms.findPaginatedRoomsWithoutDiscussionsByRoomIds(selector.name, userRoomsIds, options); - - const [rooms, total] = await Promise.all([cursor.toArray(), totalCount]); - - return { - items: rooms, - total, - }; -} - -export async function findRoomsAvailableForTeams({ uid, name }: { uid: string; name?: string }): Promise<{ - items: IRoom[]; -}> { - const options: FindOptions = { - projection: { - _id: 1, - fname: 1, - name: 1, - t: 1, - avatarETag: 1, - }, - limit: 10, - sort: { - name: 1, - }, - }; - - const userRooms = ( - (await Subscriptions.findByUserIdAndRoles(uid, ['owner'], { projection: { rid: 1 } }).toArray()) as Pick[] - ).map((item) => item.rid); - - const rooms = await Rooms.findChannelAndGroupListWithoutTeamsByNameStartingByOwner(name, userRooms, options).toArray(); - - return { - items: rooms, - }; -} diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts deleted file mode 100644 index 19e9ccba16585..0000000000000 --- a/apps/meteor/app/api/server/lib/users.ts +++ /dev/null @@ -1,242 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { Users, Subscriptions } from '@rocket.chat/models'; -import { escapeRegExp } from '@rocket.chat/string-helpers'; -import type { Mongo } from 'meteor/mongo'; -import type { Filter, FindOptions, RootFilterOperators } from 'mongodb'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { settings } from '../../../settings/server'; - -type UserAutoComplete = Required>; - -export async function findUsersToAutocomplete({ - uid, - selector, -}: { - uid: string; - selector: { exceptions: Required['username'][]; conditions: Filter; term: string }; -}): Promise<{ - items: UserAutoComplete[]; -}> { - const searchFields = settings.get('Accounts_SearchFields').trim().split(','); - const exceptions = selector.exceptions || []; - const conditions = selector.conditions || {}; - const options: FindOptions & { limit: number } = { - projection: { - name: 1, - username: 1, - nickname: 1, - status: 1, - avatarETag: 1, - freeSwitchExtension: 1, - }, - sort: { - username: 1, - }, - limit: 10, - }; - - // Search on DMs first, to list known users before others. - const contacts = await Subscriptions.findConnectedUsersExcept(uid, selector.term, exceptions, searchFields, conditions, 10, 'd'); - if (contacts.length >= options.limit) { - return { items: contacts as UserAutoComplete[] }; - } - - options.limit -= contacts.length; - contacts.forEach(({ username }) => exceptions.push(username)); - - if (!(await hasPermissionAsync(uid, 'view-outside-room'))) { - const users = await Subscriptions.findConnectedUsersExcept(uid, selector.term, exceptions, searchFields, conditions, 10); - return { items: contacts.concat(users) as UserAutoComplete[] }; - } - - const users = await Users.findActiveByUsernameOrNameRegexWithExceptionsAndConditions( - new RegExp(escapeRegExp(selector.term), 'i'), - exceptions, - conditions, - options, - ).toArray(); - - return { - items: (contacts as UserAutoComplete[]).concat(users), - }; -} - -/** - * Returns a new query object with the inclusive fields only - */ -export function getInclusiveFields(query: Record): Record { - const newQuery = Object.create(null); - - for (const [key, value] of Object.entries(query)) { - if (value === 1) { - newQuery[key] = value; - } - } - - return newQuery; -} - -/** - * get the default fields if **fields** are empty (`{}`) or `undefined`/`null` - * @param fields the fields from parsed jsonQuery - */ -export function getNonEmptyFields(fields: Record): Record { - const defaultFields = { - name: 1, - username: 1, - emails: 1, - roles: 1, - status: 1, - active: 1, - avatarETag: 1, - lastLogin: 1, - type: 1, - } as const; - - if (!fields || Object.keys(fields).length === 0) { - return defaultFields; - } - - return { ...defaultFields, ...fields }; -} - -/** - * get the default query if **query** is empty (`{}`) or `undefined`/`null` - * @param query the query from parsed jsonQuery - */ -export function getNonEmptyQuery(query: Mongo.Query | undefined | null, canSeeAllUserInfo?: boolean): Mongo.Query { - const defaultQuery: Mongo.Query = { - $or: [{ username: { $regex: '', $options: 'i' } }, { name: { $regex: '', $options: 'i' } }], - }; - - if (canSeeAllUserInfo) { - defaultQuery.$or?.push({ 'emails.address': { $regex: '', $options: 'i' } }); - } - - if (!query || Object.keys(query).length === 0) { - return defaultQuery; - } - - return { ...defaultQuery, ...query }; -} - -type FindPaginatedUsersByStatusProps = { - uid: string; - offset: number; - count: number; - sort: Record; - status?: 'active' | 'deactivated'; - roles?: string[] | null; - searchTerm?: string; - hasLoggedIn?: boolean; - type?: string; - inactiveReason?: ('deactivated' | 'pending_approval' | 'idle_too_long')[]; -}; - -export async function findPaginatedUsersByStatus({ - uid, - offset, - count, - sort, - status, - roles, - searchTerm, - hasLoggedIn, - type, - inactiveReason, -}: FindPaginatedUsersByStatusProps) { - const actualSort: Record = sort || { username: 1 }; - if (sort?.status) { - actualSort.active = sort.status; - } - const match: Filter> = {}; - switch (status) { - case 'active': - match.active = true; - break; - case 'deactivated': - match.active = false; - break; - } - - if (hasLoggedIn !== undefined) { - match.lastLogin = { $exists: hasLoggedIn }; - } - - if (type) { - match.type = type; - } - - const canSeeAllUserInfo = await hasPermissionAsync(uid, 'view-full-other-user-info'); - - const projection = { - name: 1, - username: 1, - emails: 1, - roles: 1, - status: 1, - active: 1, - avatarETag: 1, - lastLogin: 1, - type: 1, - reason: 1, - federated: 1, - freeSwitchExtension: 1, - }; - - if (searchTerm?.trim()) { - match.$or = [ - ...(canSeeAllUserInfo ? [{ 'emails.address': { $regex: escapeRegExp(searchTerm || ''), $options: 'i' } }] : []), - { - username: { $regex: escapeRegExp(searchTerm || ''), $options: 'i' }, - }, - { - name: { $regex: escapeRegExp(searchTerm || ''), $options: 'i' }, - }, - ]; - } - if (roles?.length && !roles.includes('all')) { - match.roles = { $in: roles }; - } - - if (inactiveReason) { - const inactiveReasonCondition = { - $or: [ - { inactiveReason: { $in: inactiveReason } }, - // This condition is to make it backward compatible with the old behavior - // The deactivated users not having the inactiveReason field should be returned as well - ...(inactiveReason.includes('deactivated') || inactiveReason.includes('idle_too_long') - ? [{ inactiveReason: { $exists: false } }] - : []), - ], - }; - - if (match.$or) { - match.$and = [{ $or: match.$or }, inactiveReasonCondition]; - delete match.$or; - } else { - Object.assign(match, inactiveReasonCondition); - } - } - - const { cursor, totalCount } = Users.findPaginated( - { - ...match, - }, - { - sort: actualSort, - skip: offset, - limit: count, - projection, - allowDiskUse: true, - }, - ); - const [users, total] = await Promise.all([cursor.toArray(), totalCount]); - return { - users, - count: users.length, - offset, - total, - }; -} diff --git a/apps/meteor/app/api/server/lib/webdav.ts b/apps/meteor/app/api/server/lib/webdav.ts deleted file mode 100644 index db0c47a64e2d5..0000000000000 --- a/apps/meteor/app/api/server/lib/webdav.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { IWebdavAccount } from '@rocket.chat/core-typings'; -import { WebdavAccounts } from '@rocket.chat/models'; - -export async function findWebdavAccountsByUserId({ uid }: { uid: string }): Promise { - return WebdavAccounts.findWithUserId(uid, { - projection: { - _id: 1, - username: 1, - serverURL: 1, - name: 1, - }, - }).toArray(); -} diff --git a/apps/meteor/app/api/server/middlewares/cors.ts b/apps/meteor/app/api/server/middlewares/cors.ts deleted file mode 100644 index b46be3c54c08c..0000000000000 --- a/apps/meteor/app/api/server/middlewares/cors.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { MiddlewareHandler } from 'hono'; - -import type { CachedSettings } from '../../../settings/server/CachedSettings'; - -const defaultHeaders = { - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, HEAD, PATCH', - 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, X-User-Id, X-Auth-Token, x-visitor-token, Authorization', -}; - -export const cors = - (settings: CachedSettings): MiddlewareHandler => - async (c, next) => { - const { req, res } = c; - if (req.method !== 'OPTIONS') { - res.headers.set('Access-Control-Allow-Origin', '*'); - res.headers.set('Access-Control-Allow-Headers', defaultHeaders['Access-Control-Allow-Headers']); - - await next(); - return; - } - - // check if a pre-flight request - if (!req.header('access-control-request-method') && !req.header('origin')) { - await next(); - return; - } - - if (!settings.get('API_Enable_CORS')) { - return c.body('CORS not enabled. Go to "Admin > General > REST Api" to enable it.', 405); - } - - const CORSOriginSetting = String(settings.get('API_CORS_Origin')); - - if (CORSOriginSetting === '*') { - res.headers.set('Access-Control-Allow-Origin', '*'); - res.headers.set('Access-Control-Allow-Methods', defaultHeaders['Access-Control-Allow-Methods']); - res.headers.set('Access-Control-Allow-Headers', defaultHeaders['Access-Control-Allow-Headers']); - await next(); - return; - } - - const origins = CORSOriginSetting.trim() - .split(',') - .map((origin) => String(origin).trim().toLocaleLowerCase()); - - const originHeader = req.header('origin'); - - // if invalid origin reply without required CORS headers - if (!originHeader || !origins.includes(originHeader)) { - return c.body('Invalid origin', 403); - } - - res.headers.set('Vary', 'Origin'); - res.headers.set('Access-Control-Allow-Origin', originHeader); - res.headers.set('Access-Control-Allow-Methods', defaultHeaders['Access-Control-Allow-Methods']); - res.headers.set('Access-Control-Allow-Headers', defaultHeaders['Access-Control-Allow-Headers']); - await next(); - }; diff --git a/apps/meteor/app/api/server/middlewares/logger.ts b/apps/meteor/app/api/server/middlewares/logger.ts deleted file mode 100644 index 17759687390df..0000000000000 --- a/apps/meteor/app/api/server/middlewares/logger.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { Logger } from '@rocket.chat/logger'; -import { censorUrl } from '@rocket.chat/tools'; -import type { MiddlewareHandler } from 'hono'; - -import { getRestPayload } from '../../../../server/lib/logger/logPayloads'; - -export const loggerMiddleware = - (logger: Logger): MiddlewareHandler => - async (c, next) => { - const startTime = Date.now(); - - const log = logger.logger.child( - { - method: c.req.method, - url: censorUrl(c.req.url), - userId: c.req.header('x-user-id'), - userAgent: c.req.header('user-agent'), - length: c.req.header('content-length'), - host: c.req.header('host'), - referer: c.req.header('referer'), - remoteIP: c.get('remoteAddress'), - ...(['POST', 'PUT', 'PATCH', 'DELETE'].includes(c.req.method) && (await getRestPayload(c.req))), - }, - { - redact: [ - 'payload.password', // Potentially logged by v1/login - ], - }, - ); - - await next(); - - log.http({ - status: c.res.status, - responseTime: Date.now() - startTime, - }); - }; diff --git a/apps/meteor/app/api/server/middlewares/permissions.ts b/apps/meteor/app/api/server/middlewares/permissions.ts deleted file mode 100644 index b168eadfed684..0000000000000 --- a/apps/meteor/app/api/server/middlewares/permissions.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Logger } from '@rocket.chat/logger'; -import type { Method } from '@rocket.chat/rest-typings'; -import type { MiddlewareHandler } from 'hono'; - -import { applyBreakingChanges } from '../ApiClass'; -import { API } from '../api'; -import { type PermissionsPayload, checkPermissionsForInvocation } from '../api.helpers'; -import type { TypedOptions } from '../definition'; -import type { HonoContext } from '../router'; - -const logger = new Logger('PermissionsMiddleware'); - -export const permissionsMiddleware = - (options: TypedOptions): MiddlewareHandler => - async (c: HonoContext, next) => { - if (!options.permissionsRequired) { - return next(); - } - - const user = c.get('user'); - - if (!user) { - if (applyBreakingChanges) { - const unauthorized = API.v1.unauthorized('You must be logged in to do this'); - return c.json(unauthorized.body, unauthorized.statusCode); - } - - const failure = API.v1.forbidden('User does not have the permissions required for this action [error-unauthorized]'); - return c.json(failure.body, failure.statusCode); - } - - let hasPermission: boolean; - try { - hasPermission = await checkPermissionsForInvocation( - user._id, - options.permissionsRequired as PermissionsPayload, - c.req.method as Method, - ); - } catch (e) { - logger.error({ msg: 'Error checking permissions for invocation', err: e }); - const error = API.v1.internalError(); - return c.json(error.body, error.statusCode); - } - - if (!hasPermission) { - if (applyBreakingChanges) { - const forbidden = API.v1.forbidden('User does not have the permissions required for this action [error-unauthorized]'); - return c.json(forbidden.body, forbidden.statusCode); - } - - const failure = API.v1.forbidden('User does not have the permissions required for this action [error-unauthorized]'); - return c.json(failure.body, failure.statusCode); - } - - return next(); - }; diff --git a/apps/meteor/app/api/server/v1/assets.ts b/apps/meteor/app/api/server/v1/assets.ts deleted file mode 100644 index fda2522406021..0000000000000 --- a/apps/meteor/app/api/server/v1/assets.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Settings } from '@rocket.chat/models'; -import { - ajv, - isAssetsUnsetAssetProps, - validateUnauthorizedErrorResponse, - validateForbiddenErrorResponse, - validateBadRequestErrorResponse, -} from '@rocket.chat/rest-typings'; - -import { updateAuditedByUser } from '../../../../server/settings/lib/auditedSettingUpdates'; -import { RocketChatAssets, refreshClients } from '../../../assets/server'; -import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; -import { settings } from '../../../settings/server'; -import { API } from '../api'; -import { getUploadFormData } from '../lib/getUploadFormData'; - -const successResponseSchema = ajv.compile({ - type: 'object', - properties: { success: { type: 'boolean', enum: [true] } }, - required: ['success'], - additionalProperties: false, -}); - -API.v1.post( - 'assets.setAsset', - { - authRequired: true, - permissionsRequired: ['manage-assets'], - response: { - 200: successResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const asset = await getUploadFormData( - { - request: this.request, - }, - { field: 'asset', sizeLimit: settings.get('FileUpload_MaxFileSize') }, - ); - - const { fileBuffer, fields, filename, mimetype } = asset; - - const { refreshAllClients, assetName: customName } = fields; - - const assetName = customName || filename; - const assetsKeys = Object.keys(RocketChatAssets.assets); - - const isValidAsset = assetsKeys.includes(assetName); - if (!isValidAsset) { - throw new Error('Invalid asset'); - } - - const { key, value } = await RocketChatAssets.setAssetWithBuffer(fileBuffer, mimetype, assetName); - - const { modifiedCount } = await updateAuditedByUser({ - _id: this.userId, - username: this.user.username ?? '', - ip: this.requestIp ?? '', - useragent: this.request.headers.get('user-agent') ?? '', - })(Settings.updateValueById, key, value); - - if (modifiedCount) { - void notifyOnSettingChangedById(key); - } - - if (refreshAllClients) { - await refreshClients(this.userId); - } - - return API.v1.success(); - }, -); - -API.v1.post( - 'assets.unsetAsset', - { - authRequired: true, - body: isAssetsUnsetAssetProps, - permissionsRequired: ['manage-assets'], - response: { - 200: successResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const { assetName, refreshAllClients } = this.bodyParams; - const isValidAsset = Object.keys(RocketChatAssets.assets).includes(assetName); - if (!isValidAsset) { - throw Error('Invalid asset'); - } - - const { key, value } = await RocketChatAssets.unsetAsset(assetName); - - const { modifiedCount } = await updateAuditedByUser({ - _id: this.userId, - username: this.user.username ?? '', - ip: this.requestIp ?? '', - useragent: this.request.headers.get('user-agent') ?? '', - })(Settings.updateValueById, key, value); - - if (modifiedCount) { - void notifyOnSettingChangedById(key); - } - - if (refreshAllClients) { - await refreshClients(this.userId); - } - return API.v1.success(); - }, -); diff --git a/apps/meteor/app/api/server/v1/autotranslate.ts b/apps/meteor/app/api/server/v1/autotranslate.ts deleted file mode 100644 index b8cdeebe32260..0000000000000 --- a/apps/meteor/app/api/server/v1/autotranslate.ts +++ /dev/null @@ -1,165 +0,0 @@ -import type { IMessage, ISupportedLanguage } from '@rocket.chat/core-typings'; -import { Messages, Rooms } from '@rocket.chat/models'; -import { - ajv, - validateUnauthorizedErrorResponse, - validateBadRequestErrorResponse, - validateForbiddenErrorResponse, - isAutotranslateSaveSettingsParamsPOST, - isAutotranslateGetSupportedLanguagesParamsGET, -} from '@rocket.chat/rest-typings'; - -import { canAccessRoomAsync } from '../../../authorization/server'; -import { getSupportedLanguages } from '../../../autotranslate/server/functions/getSupportedLanguages'; -import { saveAutoTranslateSettings } from '../../../autotranslate/server/functions/saveSettings'; -import { translateMessage } from '../../../autotranslate/server/functions/translateMessage'; -import { settings } from '../../../settings/server'; -import type { ExtractRoutesFromAPI } from '../ApiClass'; -import { API } from '../api'; - -type AutotranslateTranslateMessageParamsPOST = { - messageId: string; - targetLanguage?: string; -}; - -const AutotranslateTranslateMessageParamsPostSchema = { - type: 'object', - properties: { - messageId: { - type: 'string', - }, - targetLanguage: { - type: 'string', - nullable: true, - }, - }, - required: ['messageId'], - additionalProperties: false, -}; - -const isAutotranslateTranslateMessageParamsPOST = ajv.compile( - AutotranslateTranslateMessageParamsPostSchema, -); - -const autotranslateEndpoints = API.v1 - .get( - 'autotranslate.getSupportedLanguages', - { - authRequired: true, - query: isAutotranslateGetSupportedLanguagesParamsGET, - response: { - 200: ajv.compile<{ languages: ISupportedLanguage[] }>({ - type: 'object', - properties: { - languages: { type: 'array', items: { type: 'object' } }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['languages', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - if (!settings.get('AutoTranslate_Enabled')) { - return API.v1.failure('AutoTranslate is disabled.'); - } - const { targetLanguage } = this.queryParams; - const languages = await getSupportedLanguages(this.userId, targetLanguage); - - return API.v1.success({ languages: languages || [] }); - }, - ) - .post( - 'autotranslate.saveSettings', - { - authRequired: true, - body: isAutotranslateSaveSettingsParamsPOST, - response: { - 200: ajv.compile({ - type: 'object', - properties: { success: { type: 'boolean', enum: [true] } }, - required: ['success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { roomId, field, value, defaultLanguage } = this.bodyParams; - if (!settings.get('AutoTranslate_Enabled')) { - return API.v1.failure('AutoTranslate is disabled.'); - } - - // ajv 2020-12 with coerceTypes coerces booleans to strings, so check for both - if (field === 'autoTranslate' && value !== true && value !== 'true' && value !== false && value !== 'false') { - return API.v1.failure('The bodyParam "autoTranslate" must be a boolean.'); - } - - if (field === 'autoTranslateLanguage' && (typeof value !== 'string' || !Number.isNaN(Number.parseInt(value)))) { - return API.v1.failure('The bodyParam "autoTranslateLanguage" must be a string.'); - } - - await saveAutoTranslateSettings(this.userId, roomId, field, value === true || value === 'true' ? '1' : String(value), { - defaultLanguage: defaultLanguage || '', - }); - - return API.v1.success(); - }, - ) - .post( - 'autotranslate.translateMessage', - { - authRequired: true, - body: isAutotranslateTranslateMessageParamsPOST, - response: { - 200: ajv.compile<{ message: IMessage }>({ - type: 'object', - properties: { - message: { $ref: '#/components/schemas/IMessage' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['message', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const { messageId, targetLanguage } = this.bodyParams; - if (!settings.get('AutoTranslate_Enabled')) { - return API.v1.failure('AutoTranslate is disabled.'); - } - if (!messageId) { - return API.v1.failure('The bodyParam "messageId" is required.'); - } - const message = await Messages.findOneById(messageId); - if (!message) { - return API.v1.failure('Message not found.'); - } - - const room = await Rooms.findOneById(message.rid); - if (!room || !(await canAccessRoomAsync(room, { _id: this.userId }))) { - return API.v1.forbidden(); - } - - const translatedMessage = await translateMessage(targetLanguage, message); - - if (!translatedMessage) { - return API.v1.failure('Failed to translate message.'); - } - - return API.v1.success({ message: translatedMessage }); - }, - ); - -type AutotranslateEndpoints = ExtractRoutesFromAPI; - -declare module '@rocket.chat/rest-typings' { - // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface - interface Endpoints extends AutotranslateEndpoints {} -} diff --git a/apps/meteor/app/api/server/v1/cloud.ts b/apps/meteor/app/api/server/v1/cloud.ts deleted file mode 100644 index 1338192c00ec5..0000000000000 --- a/apps/meteor/app/api/server/v1/cloud.ts +++ /dev/null @@ -1,311 +0,0 @@ -import type { CloudRegistrationIntentData, CloudConfirmationPollData, CloudRegistrationStatus } from '@rocket.chat/core-typings'; -import { - isCloudConfirmationPollProps, - isCloudCreateRegistrationIntentProps, - isCloudManualRegisterProps, - ajv, - validateUnauthorizedErrorResponse, - validateForbiddenErrorResponse, - validateBadRequestErrorResponse, -} from '@rocket.chat/rest-typings'; - -import { CloudWorkspaceRegistrationError } from '../../../../lib/errors/CloudWorkspaceRegistrationError'; -import { SystemLogger } from '../../../../server/lib/logger/system'; -import { getCheckoutUrl } from '../../../cloud/server/functions/getCheckoutUrl'; -import { getConfirmationPoll } from '../../../cloud/server/functions/getConfirmationPoll'; -import { - CloudWorkspaceAccessTokenEmptyError, - CloudWorkspaceAccessTokenError, -} from '../../../cloud/server/functions/getWorkspaceAccessToken'; -import { registerPreIntentWorkspaceWizard } from '../../../cloud/server/functions/registerPreIntentWorkspaceWizard'; -import { removeLicense } from '../../../cloud/server/functions/removeLicense'; -import { retrieveRegistrationStatus } from '../../../cloud/server/functions/retrieveRegistrationStatus'; -import { saveRegistrationData, saveRegistrationDataManual } from '../../../cloud/server/functions/saveRegistrationData'; -import { startRegisterWorkspaceSetupWizard } from '../../../cloud/server/functions/startRegisterWorkspaceSetupWizard'; -import { syncWorkspace } from '../../../cloud/server/functions/syncWorkspace'; -import { API } from '../api'; - -const successResponseSchema = ajv.compile({ - type: 'object', - properties: { success: { type: 'boolean', enum: [true] } }, - required: ['success'], - additionalProperties: false, -}); - -const manualRegisterResponseSchema = ajv.compile({ - type: 'object', - properties: { success: { type: 'boolean', enum: [true] } }, - required: ['success'], - additionalProperties: false, -}); - -const createRegistrationIntentResponseSchema = ajv.compile<{ intentData: CloudRegistrationIntentData }>({ - type: 'object', - properties: { - intentData: { $ref: '#/components/schemas/CloudRegistrationIntentData' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['intentData', 'success'], - additionalProperties: false, -}); - -const registerPreIntentResponseSchema = ajv.compile<{ offline: boolean }>({ - type: 'object', - properties: { - offline: { type: 'boolean' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['offline', 'success'], - additionalProperties: false, -}); - -const confirmationPollResponseSchema = ajv.compile<{ pollData: CloudConfirmationPollData }>({ - type: 'object', - properties: { - pollData: { - oneOf: [ - { $ref: '#/components/schemas/CloudConfirmationPollDataPending' }, - { $ref: '#/components/schemas/CloudConfirmationPollDataSuccess' }, - ], - }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['pollData', 'success'], - additionalProperties: false, -}); - -const registrationStatusResponseSchema = ajv.compile<{ registrationStatus: CloudRegistrationStatus }>({ - type: 'object', - properties: { - registrationStatus: { $ref: '#/components/schemas/CloudRegistrationStatus' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['registrationStatus', 'success'], - additionalProperties: false, -}); - -const checkoutUrlResponseSchema = ajv.compile<{ url: string }>({ - type: 'object', - properties: { - url: { type: 'string' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['url', 'success'], - additionalProperties: false, -}); - -API.v1.post( - 'cloud.manualRegister', - { - authRequired: true, - permissionsRequired: ['register-on-cloud'], - body: isCloudManualRegisterProps, - response: { - 200: manualRegisterResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const registrationInfo = await retrieveRegistrationStatus(); - - if (registrationInfo.workspaceRegistered) { - return API.v1.failure('Workspace is already registered'); - } - - const settingsData = JSON.parse(Buffer.from(this.bodyParams.cloudBlob, 'base64').toString()); - - await saveRegistrationDataManual(settingsData); - - return API.v1.success(); - }, -); - -API.v1.post( - 'cloud.createRegistrationIntent', - { - authRequired: true, - permissionsRequired: ['manage-cloud'], - body: isCloudCreateRegistrationIntentProps, - response: { - 200: createRegistrationIntentResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const intentData = await startRegisterWorkspaceSetupWizard(this.bodyParams.resend, this.bodyParams.email); - - if (intentData) { - return API.v1.success({ intentData }); - } - - return API.v1.failure('Invalid query'); - }, -); - -API.v1.post( - 'cloud.registerPreIntent', - { - authRequired: true, - permissionsRequired: ['manage-cloud'], - response: { - 200: registerPreIntentResponseSchema, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - return API.v1.success({ offline: !(await registerPreIntentWorkspaceWizard()) }); - }, -); - -API.v1.get( - 'cloud.confirmationPoll', - { - authRequired: true, - permissionsRequired: ['manage-cloud'], - query: isCloudConfirmationPollProps, - response: { - 200: confirmationPollResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const { deviceCode } = this.queryParams; - - const pollData = await getConfirmationPoll(deviceCode); - if (pollData) { - if ('successful' in pollData && pollData.successful) { - await saveRegistrationData(pollData.payload); - } - return API.v1.success({ pollData }); - } - - return API.v1.failure('Invalid query'); - }, -); - -API.v1.get( - 'cloud.registrationStatus', - { - authRequired: true, - permissionsRequired: ['manage-cloud'], - response: { - 200: registrationStatusResponseSchema, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const registrationStatus = await retrieveRegistrationStatus(); - - return API.v1.success({ registrationStatus }); - }, -); - -API.v1.post( - 'cloud.syncWorkspace', - { - authRequired: true, - permissionsRequired: ['manage-cloud'], - rateLimiterOptions: { numRequestsAllowed: 2, intervalTimeInMS: 60000 }, - response: { - 200: successResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - try { - await syncWorkspace(); - - return API.v1.success(); - } catch (error) { - return API.v1.failure('Error during workspace sync'); - } - }, -); - -API.v1.post( - 'cloud.removeLicense', - { - authRequired: true, - permissionsRequired: ['manage-cloud'], - rateLimiterOptions: { numRequestsAllowed: 2, intervalTimeInMS: 60000 }, - response: { - 200: successResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - try { - await removeLicense(); - return API.v1.success(); - } catch (error) { - switch (true) { - case error instanceof CloudWorkspaceRegistrationError: - case error instanceof CloudWorkspaceAccessTokenEmptyError: - case error instanceof CloudWorkspaceAccessTokenError: { - SystemLogger.info({ - msg: 'Manual license removal failed', - endpoint: 'cloud.removeLicense', - error, - }); - break; - } - default: { - SystemLogger.error({ - msg: 'Manual license removal failed', - endpoint: 'cloud.removeLicense', - error, - }); - break; - } - } - return API.v1.failure('License removal failed'); - } - }, -); - -/** - * Declaring endpoint here because we don't want this available to the sdk client - */ -declare module '@rocket.chat/rest-typings' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface Endpoints { - '/v1/cloud.checkoutUrl': { - GET: () => { url: string }; - }; - } -} - -API.v1.get( - 'cloud.checkoutUrl', - { - authRequired: true, - permissionsRequired: ['manage-cloud'], - response: { - 200: checkoutUrlResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const checkoutUrl = await getCheckoutUrl(); - - if (!checkoutUrl.url) { - return API.v1.failure(); - } - - return API.v1.success({ url: checkoutUrl.url }); - }, -); diff --git a/apps/meteor/app/api/server/v1/integrations.ts b/apps/meteor/app/api/server/v1/integrations.ts deleted file mode 100644 index 2870fdb8236e8..0000000000000 --- a/apps/meteor/app/api/server/v1/integrations.ts +++ /dev/null @@ -1,365 +0,0 @@ -import type { IIntegration, IIntegrationHistory, INewIncomingIntegration, INewOutgoingIntegration } from '@rocket.chat/core-typings'; -import { Integrations, IntegrationHistory } from '@rocket.chat/models'; -import { - ajv, - isIntegrationsCreateProps, - isIntegrationsHistoryProps, - isIntegrationsRemoveProps, - isIntegrationsGetProps, - isIntegrationsUpdateProps, - isIntegrationsListProps, - validateBadRequestErrorResponse, - validateForbiddenErrorResponse, - validateUnauthorizedErrorResponse, -} from '@rocket.chat/rest-typings'; -import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { Match, check } from 'meteor/check'; -import type { Filter } from 'mongodb'; - -import { - mountIntegrationHistoryQueryBasedOnPermissions, - mountIntegrationQueryBasedOnPermissions, -} from '../../../integrations/server/lib/mountQueriesBasedOnPermission'; -import { addIncomingIntegration } from '../../../integrations/server/methods/incoming/addIncomingIntegration'; -import { deleteIncomingIntegration } from '../../../integrations/server/methods/incoming/deleteIncomingIntegration'; -import { updateIncomingIntegration } from '../../../integrations/server/methods/incoming/updateIncomingIntegration'; -import { addOutgoingIntegration } from '../../../integrations/server/methods/outgoing/addOutgoingIntegration'; -import { deleteOutgoingIntegration } from '../../../integrations/server/methods/outgoing/deleteOutgoingIntegration'; -import { updateOutgoingIntegration } from '../../../integrations/server/methods/outgoing/updateOutgoingIntegration'; -import { API } from '../api'; -import { getPaginationItems } from '../helpers/getPaginationItems'; -import { findOneIntegration } from '../lib/integrations'; - -const integrationSuccessSchema = ajv.compile<{ integration: IIntegration | null }>({ - type: 'object', - properties: { - integration: { oneOf: [{ $ref: '#/components/schemas/IIncomingIntegration' }, { $ref: '#/components/schemas/IOutgoingIntegration' }] }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['integration', 'success'], - additionalProperties: false, -}); - -API.v1.post( - 'integrations.create', - { - authRequired: true, - body: isIntegrationsCreateProps, - response: { - 200: integrationSuccessSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - switch (this.bodyParams.type) { - case 'webhook-outgoing': - return API.v1.success({ - integration: await addOutgoingIntegration(this.userId, this.bodyParams as INewOutgoingIntegration), - }); - case 'webhook-incoming': - return API.v1.success({ - integration: await addIncomingIntegration(this.userId, this.bodyParams as INewIncomingIntegration), - }); - default: - return API.v1.failure('Invalid integration type.'); - } - }, -); - -API.v1.get( - 'integrations.history', - { - authRequired: true, - query: isIntegrationsHistoryProps, - permissionsRequired: { - GET: { permissions: ['manage-outgoing-integrations', 'manage-own-outgoing-integrations'], operation: 'hasAny' }, - }, - response: { - 200: ajv.compile<{ history: IIntegrationHistory[]; offset: number; items: number; count: number; total: number }>({ - type: 'object', - properties: { - history: { type: 'array', items: { $ref: '#/components/schemas/IIntegrationHistory' } }, - offset: { type: 'number' }, - items: { type: 'number' }, - count: { type: 'number' }, - total: { type: 'number' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['history', 'offset', 'items', 'count', 'total', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const { userId, queryParams } = this; - - if (!queryParams.id || queryParams.id.trim() === '') { - return API.v1.failure('Invalid integration id.'); - } - - const { id } = queryParams; - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort, fields: projection, query } = await this.parseJsonQuery(); - const ourQuery = Object.assign(await mountIntegrationHistoryQueryBasedOnPermissions(userId, id), query); - - const { cursor, totalCount } = IntegrationHistory.findPaginated(ourQuery, { - sort: sort || { _updatedAt: -1 }, - skip: offset, - limit: count, - projection, - }); - - const [history, total] = await Promise.all([cursor.toArray(), totalCount]); - - return API.v1.success({ - history, - offset, - items: history.length, - count: history.length, - total, - }); - }, -); - -API.v1.get( - 'integrations.list', - { - authRequired: true, - query: isIntegrationsListProps, - permissionsRequired: { - GET: { - permissions: [ - 'manage-outgoing-integrations', - 'manage-own-outgoing-integrations', - 'manage-incoming-integrations', - 'manage-own-incoming-integrations', - ], - operation: 'hasAny', - }, - }, - response: { - 200: ajv.compile<{ integrations: IIntegration[]; offset: number; items: number; count: number; total: number }>({ - type: 'object', - properties: { - integrations: { - type: 'array', - items: { - oneOf: [{ $ref: '#/components/schemas/IIncomingIntegration' }, { $ref: '#/components/schemas/IOutgoingIntegration' }], - }, - }, - offset: { type: 'number' }, - items: { type: 'number' }, - count: { type: 'number' }, - total: { type: 'number' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['integrations', 'offset', 'items', 'count', 'total', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort, fields, query } = await this.parseJsonQuery(); - const { name, type } = this.queryParams; - - const filter = { - ...query, - ...(name ? { name: { $regex: escapeRegExp(name), $options: 'i' } } : {}), - ...(type ? { type } : {}), - }; - - const ourQuery = Object.assign(await mountIntegrationQueryBasedOnPermissions(this.userId), filter) as Filter; - - const { cursor, totalCount } = Integrations.findPaginated(ourQuery, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - projection: fields, - }); - - const [integrations, total] = await Promise.all([cursor.toArray(), totalCount]); - - return API.v1.success({ - integrations, - offset, - items: integrations.length, - count: integrations.length, - total, - }); - }, -); - -API.v1.post( - 'integrations.remove', - { - authRequired: true, - body: isIntegrationsRemoveProps, - permissionsRequired: { - POST: { - permissions: [ - 'manage-outgoing-integrations', - 'manage-own-outgoing-integrations', - 'manage-incoming-integrations', - 'manage-own-incoming-integrations', - ], - operation: 'hasAny', - }, - }, - response: { - 200: integrationSuccessSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const { bodyParams } = this; - - let integration: IIntegration | null = null; - switch (bodyParams.type) { - case 'webhook-outgoing': - if (!bodyParams.target_url && !bodyParams.integrationId) { - return API.v1.failure('An integrationId or target_url needs to be provided.'); - } - - if (bodyParams.target_url) { - integration = await Integrations.findOne({ urls: bodyParams.target_url }); - } else if (bodyParams.integrationId) { - integration = await Integrations.findOne({ _id: bodyParams.integrationId }); - } - - if (!integration) { - return API.v1.failure('No integration found.'); - } - - const outgoingId = integration._id; - - await deleteOutgoingIntegration(outgoingId, this.userId); - - return API.v1.success({ - integration, - }); - case 'webhook-incoming': - check( - bodyParams, - Match.ObjectIncluding({ - integrationId: String, - }), - ); - - integration = await Integrations.findOne({ _id: bodyParams.integrationId }); - - if (!integration) { - return API.v1.failure('No integration found.'); - } - - const incomingId = integration._id; - await deleteIncomingIntegration(incomingId, this.userId); - - return API.v1.success({ - integration, - }); - default: - return API.v1.failure('Invalid integration type.'); - } - }, -); - -API.v1.get( - 'integrations.get', - { - authRequired: true, - query: isIntegrationsGetProps, - response: { - 200: ajv.compile<{ integration: IIntegration | null }>({ - type: 'object', - properties: { - integration: { - oneOf: [{ $ref: '#/components/schemas/IIncomingIntegration' }, { $ref: '#/components/schemas/IOutgoingIntegration' }], - }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['integration', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { integrationId, createdBy } = this.queryParams; - if (!integrationId) { - return API.v1.failure('The query parameter "integrationId" is required.'); - } - - return API.v1.success({ - integration: await findOneIntegration({ - userId: this.userId, - integrationId, - createdBy, - }), - }); - }, -); - -API.v1.put( - 'integrations.update', - { - authRequired: true, - body: isIntegrationsUpdateProps, - response: { - 200: integrationSuccessSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { bodyParams } = this; - - let integration: IIntegration | null = null; - switch (bodyParams.type) { - case 'webhook-outgoing': - if (bodyParams.target_url) { - integration = await Integrations.findOne({ urls: bodyParams.target_url }); - } else if (bodyParams.integrationId) { - integration = await Integrations.findOne({ _id: bodyParams.integrationId }); - } - - if (!integration) { - return API.v1.failure('No integration found.'); - } - - await updateOutgoingIntegration(this.userId, integration._id, bodyParams as Parameters[2]); - - return API.v1.success({ - integration: await Integrations.findOne({ _id: integration._id }), - }); - case 'webhook-incoming': - integration = await Integrations.findOne({ _id: bodyParams.integrationId }); - - if (!integration) { - return API.v1.failure('No integration found.'); - } - - await updateIncomingIntegration( - this.userId, - integration._id, - bodyParams as unknown as Parameters[2], - ); - - return API.v1.success({ - integration: await Integrations.findOne({ _id: integration._id }), - }); - default: - return API.v1.failure('Invalid integration type.'); - } - }, -); diff --git a/apps/meteor/app/api/server/v1/permissions.ts b/apps/meteor/app/api/server/v1/permissions.ts deleted file mode 100644 index 38c63b1f22a4c..0000000000000 --- a/apps/meteor/app/api/server/v1/permissions.ts +++ /dev/null @@ -1,195 +0,0 @@ -import type { IPermission } from '@rocket.chat/core-typings'; -import { Permissions, Roles } from '@rocket.chat/models'; -import { - ajv, - ajvQuery, - validateUnauthorizedErrorResponse, - validateBadRequestErrorResponse, - validateForbiddenErrorResponse, -} from '@rocket.chat/rest-typings'; -import { Meteor } from 'meteor/meteor'; - -import { permissionsGetMethod } from '../../../authorization/server/streamer/permissions'; -import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener'; -import type { ExtractRoutesFromAPI } from '../ApiClass'; -import { API } from '../api'; - -type PermissionsListAllProps = { - updatedSince?: string; -}; - -type PermissionsUpdateProps = { - permissions: { _id: string; roles: string[] }[]; -}; - -const permissionListAllSchema = { - type: 'object', - properties: { - updatedSince: { - type: 'string', - nullable: true, - }, - }, - required: [], - additionalProperties: false, -}; - -const permissionUpdatePropsSchema = { - type: 'object', - properties: { - permissions: { - type: 'array', - items: { - type: 'object', - properties: { - _id: { type: 'string' }, - roles: { - type: 'array', - items: { type: 'string' }, - uniqueItems: true, - }, - }, - additionalProperties: false, - required: ['_id', 'roles'], - }, - }, - }, - required: ['permissions'], - additionalProperties: false, -}; - -const isPermissionsListAll = ajvQuery.compile(permissionListAllSchema); - -const isBodyParamsValidPermissionUpdate = ajv.compile(permissionUpdatePropsSchema); - -const permissionsEndpoints = API.v1 - .get( - 'permissions.listAll', - { - authRequired: true, - query: isPermissionsListAll, - response: { - 200: ajv.compile<{ - update: IPermission[]; - remove: IPermission[]; - }>({ - type: 'object', - properties: { - update: { - type: 'array', - items: { $ref: '#/components/schemas/IPermission' }, - }, - remove: { - type: 'array', - items: { $ref: '#/components/schemas/IPermission' }, - }, - success: { - type: 'boolean', - enum: [true], - description: 'Indicates if the request was successful.', - }, - }, - required: ['update', 'remove', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { updatedSince } = this.queryParams; - - let updatedSinceDate: Date | undefined; - if (updatedSince) { - if (isNaN(Date.parse(updatedSince))) { - throw new Meteor.Error('error-roomId-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); - } - updatedSinceDate = new Date(updatedSince); - } - - const result = (await permissionsGetMethod(updatedSinceDate)) as { - update: IPermission[]; - remove: IPermission[]; - }; - - if (Array.isArray(result)) { - return API.v1.success({ - update: result, - remove: [], - }); - } - - return API.v1.success(result); - }, - ) - .post( - 'permissions.update', - { - authRequired: true, - permissionsRequired: ['access-permissions'], - body: isBodyParamsValidPermissionUpdate, - response: { - 200: ajv.compile<{ - permissions: IPermission[]; - }>({ - type: 'object', - properties: { - permissions: { - type: 'array', - items: { $ref: '#/components/schemas/IPermission' }, - }, - success: { - type: 'boolean', - enum: [true], - description: 'Indicates if the request was successful.', - }, - }, - required: ['permissions', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const { bodyParams } = this; - - if (!isBodyParamsValidPermissionUpdate(bodyParams)) { - return API.v1.failure('Invalid body params', 'error-invalid-body-params'); - } - - const permissionKeys = bodyParams.permissions.map(({ _id }) => _id); - const permissions = await Permissions.find({ _id: { $in: permissionKeys } }).toArray(); - - if (permissions.length !== bodyParams.permissions.length) { - return API.v1.failure('Invalid permission', 'error-invalid-permission'); - } - - const roleKeys = [...new Set(bodyParams.permissions.flatMap((p) => p.roles))]; - - const roles = await Roles.find({ _id: { $in: roleKeys } }).toArray(); - - if (roles.length !== roleKeys.length) { - return API.v1.failure('Invalid role', 'error-invalid-role'); - } - - for (const permission of bodyParams.permissions) { - await Permissions.setRoles(permission._id, permission.roles); - void notifyOnPermissionChangedById(permission._id); - } - - const result = (await permissionsGetMethod()) as IPermission[]; - - return API.v1.success({ - permissions: result, - }); - }, - ); - -export type PermissionsEndpoints = ExtractRoutesFromAPI; - -declare module '@rocket.chat/rest-typings' { - // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface - interface Endpoints extends PermissionsEndpoints {} -} diff --git a/apps/meteor/app/api/server/v1/push.ts b/apps/meteor/app/api/server/v1/push.ts deleted file mode 100644 index e16dc9bb37de4..0000000000000 --- a/apps/meteor/app/api/server/v1/push.ts +++ /dev/null @@ -1,398 +0,0 @@ -import { Push } from '@rocket.chat/core-services'; -import { pushTokenTypes } from '@rocket.chat/core-typings'; -import type { IMessage, IPushNotificationConfig, IPushToken, IPushTokenTypes } from '@rocket.chat/core-typings'; -import { Messages, PushToken, Users, Rooms, Settings } from '@rocket.chat/models'; -import { - ajv, - isPushGetProps, - validateNotFoundErrorResponse, - validateBadRequestErrorResponse, - validateUnauthorizedErrorResponse, - validateForbiddenErrorResponse, -} from '@rocket.chat/rest-typings'; -import type { JSONSchemaType } from 'ajv'; -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; - -import { executePushTest } from '../../../../server/lib/pushConfig'; -import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; -import PushNotification from '../../../push-notifications/server/lib/PushNotification'; -import { settings } from '../../../settings/server'; -import type { ExtractRoutesFromAPI } from '../ApiClass'; -import { API } from '../api'; -import type { SuccessResult } from '../definition'; - -type PushTokenPOST = { - id?: string; - type: IPushTokenTypes; - value: string; - appName: string; - voipToken?: string; -}; - -const PushTokenPOSTSchema: JSONSchemaType = { - type: 'object', - properties: { - id: { - type: 'string', - nullable: true, - }, - type: { - type: 'string', - enum: pushTokenTypes, - }, - value: { - type: 'string', - minLength: 1, - }, - appName: { - type: 'string', - minLength: 1, - }, - voipToken: { - type: 'string', - nullable: true, - }, - }, - required: ['type', 'value', 'appName'], - additionalProperties: false, -}; - -export const isPushTokenPOSTProps = ajv.compile(PushTokenPOSTSchema); - -type PushTokenDELETE = { - token: string; -}; - -const PushTokenDELETESchema: JSONSchemaType = { - type: 'object', - properties: { - token: { - type: 'string', - minLength: 1, - }, - }, - required: ['token'], - additionalProperties: false, -}; - -export const isPushTokenDELETEProps = ajv.compile(PushTokenDELETESchema); - -type PushTokenResult = Pick; - -/** - * Pick only the attributes we actually want to return on the endpoint, ensuring nothing from older schemas get mixed in - */ -function cleanTokenResult(result: Omit): PushTokenResult { - const { _id, token, appName, userId, enabled, createdAt, _updatedAt, voipToken } = result; - - return { - _id, - token, - appName, - userId, - enabled, - createdAt, - _updatedAt, - voipToken, - }; -} - -const pushTokenEndpoints = API.v1 - .post( - 'push.token', - { - response: { - 200: ajv.compile['body']>({ - additionalProperties: false, - type: 'object', - properties: { - success: { - type: 'boolean', - description: 'Indicates if the request was successful.', - }, - result: { - type: 'object', - description: 'The updated token data for this device', - properties: { - _id: { - type: 'string', - }, - token: { - type: 'object', - properties: { - apn: { - type: 'string', - }, - gcm: { - type: 'string', - }, - }, - required: [], - additionalProperties: false, - }, - appName: { - type: 'string', - }, - userId: { - type: 'string', - nullable: true, - }, - enabled: { - type: 'boolean', - }, - createdAt: { - type: 'string', - }, - _updatedAt: { - type: 'string', - }, - voipToken: { - type: 'string', - nullable: true, - }, - }, - additionalProperties: false, - }, - }, - required: ['success', 'result'], - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - body: isPushTokenPOSTProps, - authRequired: true, - }, - async function action() { - const { id, type, value, appName, voipToken } = this.bodyParams; - - if (voipToken && !id) { - return API.v1.failure('voip-tokens-must-specify-device-id'); - } - - const rawToken = this.request.headers.get('x-auth-token'); - if (!rawToken) { - throw new Meteor.Error('error-authToken-param-not-valid', 'The required "authToken" header param is missing or invalid.'); - } - const authToken = Accounts._hashLoginToken(rawToken); - - const result = await Push.registerPushToken({ - ...(id && { _id: id }), - token: { [type]: value } as IPushToken['token'], - authToken, - appName, - userId: this.userId, - ...(voipToken && { voipToken }), - }); - - return API.v1.success({ result: cleanTokenResult(result) }); - }, - ) - .delete( - 'push.token', - { - response: { - 200: ajv.compile({ - additionalProperties: false, - type: 'object', - properties: { - success: { - type: 'boolean', - }, - }, - required: ['success'], - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - 404: validateNotFoundErrorResponse, - }, - body: isPushTokenDELETEProps, - authRequired: true, - }, - async function action() { - const { token } = this.bodyParams; - - const removeResult = await PushToken.removeAllByTokenStringAndUserId(token, this.userId); - - if (removeResult.deletedCount === 0) { - return API.v1.notFound(); - } - - return API.v1.success(); - }, - ); - -const pushGetResponseSchema = ajv.compile<{ - data: { message: IMessage; notification: IPushNotificationConfig }; -}>({ - type: 'object', - properties: { - data: { - type: 'object', - properties: { - message: { type: 'object' }, - notification: { - type: 'object', - properties: { - from: { type: 'string' }, - title: { type: 'string' }, - text: { type: 'string' }, - userId: { type: 'string' }, - badge: { type: 'number', nullable: true }, - sound: { type: 'string', nullable: true }, - priority: { type: 'number', nullable: true }, - payload: { type: 'object', nullable: true }, - notId: { type: 'number', nullable: true }, - gcm: { - type: 'object', - properties: { - style: { type: 'string' }, - image: { type: 'string' }, - }, - required: ['style', 'image'], - nullable: true, - }, - apn: { - type: 'object', - properties: { - category: { type: 'string' }, - topicSuffix: { type: 'string', nullable: true }, - }, - required: ['category'], - nullable: true, - }, - }, - required: ['from', 'title', 'text', 'userId'], - }, - }, - required: ['message', 'notification'], - }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['data', 'success'], - additionalProperties: false, -}); - -const pushInfoResponseSchema = ajv.compile<{ pushGatewayEnabled: boolean; defaultPushGateway: boolean }>({ - type: 'object', - properties: { - pushGatewayEnabled: { type: 'boolean' }, - defaultPushGateway: { type: 'boolean' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['pushGatewayEnabled', 'defaultPushGateway', 'success'], - additionalProperties: false, -}); - -const pushGetInfoEndpoints = API.v1 - .get( - 'push.get', - { - authRequired: true, - query: isPushGetProps, - response: { - 200: pushGetResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { id } = this.queryParams; - - const receiver = await Users.findOneById(this.userId); - if (!receiver) { - throw new Error('error-user-not-found'); - } - - const message = await Messages.findOneById(id); - if (!message) { - throw new Error('error-message-not-found'); - } - - const room = await Rooms.findOneById(message.rid); - if (!room) { - throw new Error('error-room-not-found'); - } - - if (!(await canAccessRoomAsync(room, receiver))) { - throw new Error('error-not-allowed'); - } - - const data = await PushNotification.getNotificationForMessageId({ receiver, room, message }); - - return API.v1.success({ data }); - }, - ) - .get( - 'push.info', - { - authRequired: true, - response: { - 200: pushInfoResponseSchema, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const defaultGateway = (await Settings.findOneById('Push_gateway', { projection: { packageValue: 1 } }))?.packageValue; - const defaultPushGateway = settings.get('Push_gateway') === defaultGateway; - return API.v1.success({ - pushGatewayEnabled: Boolean(settings.get('Push_enable')), - defaultPushGateway, - }); - }, - ); - -const pushTestEndpoints = API.v1.post( - 'push.test', - { - authRequired: true, - rateLimiterOptions: { - numRequestsAllowed: 1, - intervalTimeInMS: 1000, - }, - permissionsRequired: ['test-push-notifications'], - body: ajv.compile({ type: 'object', additionalProperties: false }), - response: { - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 200: ajv.compile<{ tokensCount: number }>({ - type: 'object', - properties: { - tokensCount: { type: 'integer' }, - success: { - type: 'boolean', - enum: [true], - }, - }, - required: ['tokensCount', 'success'], - additionalProperties: false, - }), - }, - }, - - async function action() { - if (settings.get('Push_enable') !== true) { - throw new Meteor.Error('error-push-disabled', 'Push is disabled', { - method: 'push_test', - }); - } - - const tokensCount = await executePushTest(this.userId, this.user.username); - return API.v1.success({ tokensCount }); - }, -); - -type PushTestEndpoints = ExtractRoutesFromAPI; - -type PushTokenEndpoints = ExtractRoutesFromAPI; - -type PushGetInfoEndpoints = ExtractRoutesFromAPI; - -type PushEndpoints = PushTestEndpoints & PushTokenEndpoints & PushGetInfoEndpoints; - -declare module '@rocket.chat/rest-typings' { - // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface - interface Endpoints extends PushEndpoints {} -} diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts deleted file mode 100644 index 1cf83d741bdb2..0000000000000 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ /dev/null @@ -1,1735 +0,0 @@ -import { FederationMatrix, MeteorError, Room, Team } from '@rocket.chat/core-services'; -import { - type IRoom, - type IRoomAbacRedaction, - type IUpload, - type RequiredField, - type RoomAdminFieldsType, - isPrivateRoom, - isPublicRoom, - type IUser, -} from '@rocket.chat/core-typings'; -import { Messages, Rooms, Users, Uploads, Subscriptions } from '@rocket.chat/models'; -import type { Notifications } from '@rocket.chat/rest-typings'; -import { - ajv, - ajvQuery, - isGETRoomsNameExists, - isRoomsImagesProps, - isRoomsMuteUnmuteUserProps, - isRoomsBanUserProps, - isRoomsUnbanUserProps, - isRoomsBannedUsersProps, - isRoomsExportProps, - isRoomsIsMemberProps, - isRoomsCleanHistoryProps, - isRoomsOpenProps, - isRoomsJoinProps, - isRoomsMembersOrderedByRoleProps, - isRoomsChangeArchivationStateProps, - isRoomsHideProps, - isRoomsInviteProps, - isRoomsCreateDiscussionProps, - isRoomsAdminRoomsProps, - isRoomsAutocompleteAdminRoomsPayload, - isRoomsAdminRoomsGetRoomProps, - isRoomsAutoCompleteChannelAndPrivateProps, - isRoomsAutocompleteChannelAndPrivateWithPaginationProps, - isRoomsAutocompleteAvailableForTeamsProps, - isRoomsSaveRoomSettingsProps, - validateBadRequestErrorResponse, - validateUnauthorizedErrorResponse, - validateForbiddenErrorResponse, - validateNotFoundErrorResponse, -} from '@rocket.chat/rest-typings'; -import { isTruthy } from '@rocket.chat/tools'; -import { Meteor } from 'meteor/meteor'; - -import { adminFields } from '../../../../lib/rooms/adminFields'; -import { omit } from '../../../../lib/utils/omit'; -import { banUserFromRoomMethod } from '../../../../server/lib/banUserFromRoom'; -import * as dataExport from '../../../../server/lib/dataExport'; -import { eraseRoom } from '../../../../server/lib/eraseRoom'; -import { findUsersOfRoomOrderedByRole } from '../../../../server/lib/findUsersOfRoomOrderedByRole'; -import { openRoom } from '../../../../server/lib/openRoom'; -import type { RoomRoles } from '../../../../server/lib/roles/getRoomRoles'; -import { unbanUserFromRoom } from '../../../../server/lib/unbanUserFromRoom'; -import { hideRoomMethod } from '../../../../server/methods/hideRoom'; -import { muteUserInRoom } from '../../../../server/methods/muteUserInRoom'; -import { toggleFavoriteMethod } from '../../../../server/methods/toggleFavorite'; -import { unmuteUserInRoom } from '../../../../server/methods/unmuteUserInRoom'; -import { roomsGetMethod } from '../../../../server/publications/room'; -import { canAccessRoomAsync, canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { stripABACManagedFieldsForAdmin } from '../../../authorization/server/lib/isABACManagedRoom'; -import { saveRoomSettings } from '../../../channel-settings/server/methods/saveRoomSettings'; -import { createDiscussion } from '../../../discussion/server/methods/createDiscussion'; -import { FileUpload } from '../../../file-upload/server'; -import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; -import { syncRolePrioritiesForRoomIfRequired } from '../../../lib/server/functions/syncRolePrioritiesForRoomIfRequired'; -import { notifyOnSubscriptionChanged } from '../../../lib/server/lib/notifyListener'; -import { executeArchiveRoom } from '../../../lib/server/methods/archiveRoom'; -import { cleanRoomHistoryMethod } from '../../../lib/server/methods/cleanRoomHistory'; -import { executeGetRoomRoles } from '../../../lib/server/methods/getRoomRoles'; -import { leaveRoomMethod } from '../../../lib/server/methods/leaveRoom'; -import { executeUnarchiveRoom } from '../../../lib/server/methods/unarchiveRoom'; -import { applyAirGappedRestrictionsValidation } from '../../../license/server/airGappedRestrictionsWrapper'; -import type { NotificationFieldType } from '../../../push-notifications/server/methods/saveNotificationSettings'; -import { saveNotificationSettingsMethod } from '../../../push-notifications/server/methods/saveNotificationSettings'; -import { settings } from '../../../settings/server'; -import type { ExtractRoutesFromAPI } from '../ApiClass'; -import { API } from '../api'; -import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessage'; -import { getPaginationItems } from '../helpers/getPaginationItems'; -import { getUserFromParams } from '../helpers/getUserFromParams'; -import { MultipartUploadHandler } from '../lib/MultipartUploadHandler'; -import { - findAdminRoom, - findAdminRooms, - findAdminRoomsAutocomplete, - findChannelAndPrivateAutocomplete, - findChannelAndPrivateAutocompleteWithPagination, - findRoomsAvailableForTeams, -} from '../lib/rooms'; -import { scopeAdminRoomsForAbac } from '../lib/scopeAdminRoomsForAbac'; - -export async function findRoomByIdOrName({ - params, - checkedArchived = true, -}: { - params: - | { - roomId?: string; - } - | { - roomName?: string; - }; - checkedArchived?: boolean; -}): Promise { - if ( - (!('roomId' in params) && !('roomName' in params)) || - ('roomId' in params && !(params as { roomId?: string }).roomId && 'roomName' in params && !(params as { roomName?: string }).roomName) - ) { - throw new Meteor.Error('error-roomid-param-not-provided', 'The parameter "roomId" or "roomName" is required'); - } - - const projection = { ...API.v1.defaultFieldsToExclude }; - - let room; - if ('roomId' in params) { - room = await Rooms.findOneById(params.roomId || '', { projection }); - } else if ('roomName' in params) { - room = await Rooms.findOneByName(params.roomName || '', { projection }); - } - - if (!room) { - throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any channel'); - } - if (checkedArchived && room.archived) { - throw new Meteor.Error('error-room-archived', `The channel, ${room.name}, is archived`); - } - - return room; -} - -API.v1.get( - 'rooms.nameExists', - { - authRequired: true, - query: isGETRoomsNameExists, - response: { - 200: ajv.compile<{ exists: boolean }>({ - type: 'object', - properties: { - exists: { type: 'boolean' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['exists', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { roomName } = this.queryParams; - - const room = await Rooms.findOneByName(roomName, { projection: { _id: 1 } }); - - return API.v1.success({ exists: !!room }); - }, -); - -const roomDeleteEndpoint = API.v1.post( - 'rooms.delete', - { - authRequired: true, - body: ajv.compile<{ roomId: string }>({ - type: 'object', - properties: { - roomId: { - type: 'string', - description: 'The ID of the room to delete.', - }, - }, - required: ['roomId'], - additionalProperties: false, - }), - response: { - 200: ajv.compile({ - type: 'object', - properties: { - success: { - type: 'boolean', - enum: [true], - description: 'Indicates if the request was successful.', - }, - }, - required: ['success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { roomId } = this.bodyParams; - - const room = await Rooms.findOneById(roomId); - - if (!room) { - throw new MeteorError('error-invalid-room', 'Invalid room', { - method: 'eraseRoom', - }); - } - - if (room.teamMain) { - throw new Meteor.Error('error-cannot-delete-team-channel', 'Cannot delete a team channel', { - method: 'eraseRoom', - }); - } - - await eraseRoom(room, this.user); - - return API.v1.success(); - }, -); - -API.v1.get( - 'rooms.get', - { - authRequired: true, - response: { - 200: ajv.compile<{ update: IRoom[]; remove: IRoom[] }>({ - type: 'object', - properties: { - update: { type: 'array', items: { type: 'object' } }, // relaxed: IRoom composed with lastMessage - remove: { type: 'array', items: { type: 'object' } }, // relaxed: IRoom composed with lastMessage - success: { type: 'boolean', enum: [true] }, - }, - required: ['update', 'remove', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { updatedSince } = this.queryParams; - - let updatedSinceDate; - if (updatedSince) { - if (isNaN(Date.parse(updatedSince))) { - throw new Meteor.Error('error-updatedSince-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); - } else { - updatedSinceDate = new Date(updatedSince); - } - } - - let result = await roomsGetMethod(this.userId, updatedSinceDate); - - if (Array.isArray(result)) { - result = { - update: result, - remove: [], - }; - } - - return API.v1.success({ - update: await Promise.all(result.update.map((room) => composeRoomWithLastMessage(room, this.userId))), - remove: await Promise.all(result.remove.map((room) => composeRoomWithLastMessage(room, this.userId))), - }); - }, -); - -API.v1.addRoute( - 'rooms.media/:rid', - { authRequired: true }, - { - async post() { - if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { - return API.v1.forbidden(); - } - - const { file, fields } = await MultipartUploadHandler.parseRequest(this.incoming, { - field: 'file', - maxSize: settings.get('FileUpload_MaxFileSize'), - }); - - if (!file) { - throw new Meteor.Error('error-no-file-uploaded', 'No file was uploaded'); - } - - const expiresAt = new Date(); - expiresAt.setHours(expiresAt.getHours() + 24); - - let content; - - if (fields.content) { - try { - content = JSON.parse(fields.content); - } catch (e) { - console.error(e); - throw new Meteor.Error('invalid-field-content'); - } - } - - const details = { - name: file.filename, - size: file.size, - type: file.mimetype, - rid: this.urlParams.rid, - userId: this.userId, - content, - expiresAt, - }; - - // TODO: In the future, we should isolate file receival from storage and post-processing. - const fileStore = FileUpload.getStore('Uploads'); - const uploadedFile = await fileStore.insert(details, file.tempFilePath); - - uploadedFile.path = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`); - - await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id')); - - return API.v1.success({ - file: { - _id: uploadedFile._id, - url: uploadedFile.path, - }, - }); - }, - }, -); - -API.v1.addRoute( - 'rooms.mediaConfirm/:rid/:fileId', - { authRequired: true }, - { - async post() { - if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) { - return API.v1.forbidden(); - } - - const file = await Uploads.findOneByIdAndUserIdAndRoomId(this.urlParams.fileId, this.userId, this.urlParams.rid); - - if (!file) { - throw new Meteor.Error('invalid-file'); - } - - if ((this.bodyParams.description?.length ?? 0) > settings.get('Message_MaxAllowedSize')) { - throw new Meteor.Error('error-message-size-exceeded'); - } - - file.description = this.bodyParams.description; - delete this.bodyParams.description; - - if (this.bodyParams.fileName) { - file.name = this.bodyParams.fileName; - delete this.bodyParams.fileName; - } - - if (this.bodyParams.fileContent) { - file.content = this.bodyParams.fileContent; - delete this.bodyParams.fileContent; - } - - await applyAirGappedRestrictionsValidation(() => - sendFileMessage(this.userId, { roomId: this.urlParams.rid, file, msgData: this.bodyParams }), - ); - - await Uploads.confirmTemporaryFile(this.urlParams.fileId, this.userId); - - const message = await Messages.getMessageByFileIdAndUsername(file._id, this.userId); - - return API.v1.success({ - message, - }); - }, - }, -); - -const saveNotificationBodySchema = ajv.compile<{ - roomId: string; - notifications: Record; -}>({ - type: 'object', - properties: { - roomId: { type: 'string', minLength: 1 }, - notifications: { - type: 'object', - minProperties: 1, - additionalProperties: { type: 'string' }, - }, - }, - required: ['roomId', 'notifications'], - additionalProperties: false, -}); - -const saveNotificationResponseSchema = ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [true] }, - }, - required: ['success'], - additionalProperties: false, -}); - -const roomsSaveNotificationEndpoint = API.v1.post( - 'rooms.saveNotification', - { - authRequired: true, - body: saveNotificationBodySchema, - response: { - 200: saveNotificationResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { roomId, notifications } = this.bodyParams; - - await Promise.all( - Object.entries(notifications as Notifications).map(async ([notificationKey, notificationValue]) => - saveNotificationSettingsMethod(this.userId, roomId, notificationKey as NotificationFieldType, notificationValue), - ), - ); - - return API.v1.success({ success: true }); - }, -); - -const saveDraftBodySchema = ajv.compile<{ rid: IRoom['_id']; draft: string }>({ - type: 'object', - properties: { - rid: { type: 'string', minLength: 1 }, - draft: { type: 'string' }, - }, - required: ['rid', 'draft'], - additionalProperties: false, -}); - -const saveDraftResponseSchema = ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [true] }, - }, - required: ['success'], - additionalProperties: false, -}); - -const roomsSaveDraftEndpoint = API.v1.post( - 'rooms.saveDraft', - { - authRequired: true, - body: saveDraftBodySchema, - response: { - 200: saveDraftResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { rid, draft } = this.bodyParams; - - if (draft.length > (settings.get('Message_MaxAllowedSize') ?? 0)) { - return API.v1.failure('error-message-size-exceeded'); - } - - const subscription = await Subscriptions.updateDraftByRoomIdAndUserId(rid, this.userId, draft || undefined); - if (!subscription) { - throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription'); - } - - void notifyOnSubscriptionChanged(subscription); - - return API.v1.success(); - }, -); - -API.v1.post( - 'rooms.cleanHistory', - { - authRequired: true, - body: isRoomsCleanHistoryProps, - response: { - 200: ajv.compile<{ _id: string; count: number }>({ - type: 'object', - properties: { - _id: { type: 'string' }, - count: { type: 'number' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['_id', 'count', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const room = await findRoomByIdOrName({ params: this.bodyParams }); - const { _id } = room; - - if (!room || !(await canAccessRoomAsync(room, { _id: this.userId }))) { - return API.v1.failure('User does not have access to the room [error-not-allowed]', 'error-not-allowed'); - } - - const { latest, oldest, inclusive = false, limit, excludePinned, filesOnly, ignoreThreads, ignoreDiscussion, users } = this.bodyParams; - - const count = await cleanRoomHistoryMethod(this.userId, { - roomId: _id, - latest: new Date(latest), - oldest: new Date(oldest), - inclusive, - limit, - excludePinned: [true, 'true', 1, '1'].includes(excludePinned ?? false), - filesOnly: [true, 'true', 1, '1'].includes(filesOnly ?? false), - ignoreThreads: [true, 'true', 1, '1'].includes(ignoreThreads ?? false), - ignoreDiscussion: [true, 'true', 1, '1'].includes(ignoreDiscussion ?? false), - fromUsers: users?.filter(isTruthy) || [], - }); - - return API.v1.success({ _id, count }); - }, -); - -API.v1.get( - 'rooms.info', - { - authRequired: true, - response: { - 200: ajv.compile<{ room: IRoom | null }>({ - type: 'object', - properties: { - room: { type: ['object', 'null'] }, - team: { type: 'object' }, - parent: { type: 'object' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['room', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const room = await findRoomByIdOrName({ params: this.queryParams, checkedArchived: false }); - const { fields } = await this.parseJsonQuery(); - - if (!room || !(await canAccessRoomAsync(room, { _id: this.userId }))) { - return API.v1.failure('not-allowed', 'Not Allowed'); - } - - const discussionParent = - room.prid && - (await Rooms.findOneById>(room.prid, { - projection: { name: 1, fname: 1, t: 1, prid: 1, u: 1 }, - })); - const { team, parentRoom } = await Team.getRoomInfo(room); - const parent = discussionParent || parentRoom; - - return API.v1.success({ - room: await Rooms.findOneByIdOrName(room._id, { projection: fields }), - ...(team && { team }), - ...(parent && { parent }), - }); - }, -); - -API.v1.post( - 'rooms.createDiscussion', - { - authRequired: true, - body: isRoomsCreateDiscussionProps, - response: { - 200: ajv.compile<{ discussion: IRoom }>({ - type: 'object', - properties: { - discussion: { type: 'object' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['discussion', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { prid, pmid, reply, t_name, users, encrypted, topic } = this.bodyParams; - - const discussion = await applyAirGappedRestrictionsValidation(() => - createDiscussion(this.userId, { - prid, - pmid, - t_name, - reply, - users: users?.filter(isTruthy) || [], - encrypted, - topic, - }), - ); - - return API.v1.success({ discussion }); - }, -); - -API.v1.get( - 'rooms.getDiscussions', - { - authRequired: true, - response: { - 200: ajv.compile<{ discussions: IRoom[]; count: number; offset: number; total: number }>({ - type: 'object', - properties: { - discussions: { type: 'array', items: { type: 'object' } }, // relaxed: discussions have extra room fields - count: { type: 'number' }, - offset: { type: 'number' }, - total: { type: 'number' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['discussions', 'count', 'offset', 'total', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const room = await findRoomByIdOrName({ params: this.queryParams }); - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort, fields, query } = await this.parseJsonQuery(); - - if (!room || !(await canAccessRoomAsync(room, { _id: this.userId }))) { - return API.v1.failure('not-allowed', 'Not Allowed'); - } - - const ourQuery = Object.assign(query, { prid: room._id }); - - const { cursor, totalCount } = await Rooms.findPaginated(ourQuery, { - sort: sort || { fname: 1 }, - skip: offset, - limit: count, - projection: fields, - }); - - const [discussions, total] = await Promise.all([cursor.toArray(), totalCount]); - - return API.v1.success({ - discussions, - count: discussions.length, - offset, - total, - }); - }, -); - -API.v1.get( - 'rooms.images', - { - authRequired: true, - query: isRoomsImagesProps, - response: { - 200: ajv.compile<{ files: IUpload[]; count: number; offset: number; total: number }>({ - type: 'object', - properties: { - files: { type: 'array', items: { type: 'object' } }, // relaxed: IUpload with user transform - count: { type: 'number' }, - offset: { type: 'number' }, - total: { type: 'number' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['files', 'count', 'offset', 'total', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const room = await Rooms.findOneById>(this.queryParams.roomId, { - projection: { t: 1, teamId: 1, prid: 1 }, - }); - - if (!room || !(await canAccessRoomAsync(room, { _id: this.userId }))) { - return API.v1.forbidden(); - } - - let initialImage: IUpload | null = null; - if (this.queryParams.startingFromId) { - initialImage = await Uploads.findOneById(this.queryParams.startingFromId); - if (initialImage && initialImage.rid !== room._id) { - initialImage = null; - } - } - - const { offset, count } = await getPaginationItems(this.queryParams); - - const { cursor, totalCount } = Uploads.findImagesByRoomId(room._id, initialImage?.uploadedAt, { - skip: offset, - limit: count, - }); - - const [files, total] = await Promise.all([cursor.toArray(), totalCount]); - - // If the initial image was not returned in the query, insert it as the first element of the list - if (initialImage && !files.find(({ _id }) => _id === initialImage._id)) { - files.splice(0, 0, initialImage); - } - - return API.v1.success({ - files, - count, - offset, - total, - }); - }, -); - -API.v1.get( - 'rooms.adminRooms', - { - authRequired: true, - query: isRoomsAdminRoomsProps, - response: { - 200: ajv.compile<{ - rooms: Array & IRoomAbacRedaction>; - count: number; - offset: number; - total: number; - }>({ - type: 'object', - properties: { - rooms: { type: 'array', items: { type: 'object' } }, // relaxed: IRoom with admin fields + optional ABAC redaction - count: { type: 'number' }, - offset: { type: 'number' }, - total: { type: 'number' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['rooms', 'count', 'offset', 'total', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - const { types, filter } = this.queryParams; - - return API.v1.success( - await findAdminRooms({ - uid: this.userId, - filter: filter || '', - types: types ?? [], - pagination: { - offset, - count, - sort, - }, - }), - ); - }, -); - -API.v1.get( - 'rooms.autocomplete.adminRooms', - { - authRequired: true, - query: isRoomsAutocompleteAdminRoomsPayload, - response: { - 200: ajv.compile<{ items: IRoom[] }>({ - type: 'object', - properties: { - items: { type: 'array', items: { type: 'object' } }, // relaxed: IRoom autocomplete subset - success: { type: 'boolean', enum: [true] }, - }, - required: ['items', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { selector } = this.queryParams; - - return API.v1.success( - await findAdminRoomsAutocomplete({ - uid: this.userId, - selector: JSON.parse(selector), - }), - ); - }, -); - -API.v1.get( - 'rooms.adminRooms.getRoom', - { - authRequired: true, - query: isRoomsAdminRoomsGetRoomProps, - response: { - 200: ajv.compile & IRoomAbacRedaction>({ - allOf: [ - { $ref: '#/components/schemas/IRoomAdmin' }, - { - type: 'object', - properties: { - success: { type: 'boolean', enum: [true] }, - abacAttributesRedacted: { type: 'boolean' }, - }, - required: ['success'], - }, - ], - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { rid } = this.queryParams; - const room = await findAdminRoom({ - uid: this.userId, - rid: rid || '', - }); - - if (!room) { - return API.v1.failure('not-allowed', 'Not Allowed'); - } - return API.v1.success(room); - }, -); - -API.v1.get( - 'rooms.autocomplete.channelAndPrivate', - { - authRequired: true, - query: isRoomsAutoCompleteChannelAndPrivateProps, - response: { - 200: ajv.compile<{ items: IRoom[] }>({ - type: 'object', - properties: { - items: { type: 'array', items: { type: 'object' } }, // relaxed: IRoom autocomplete subset - success: { type: 'boolean', enum: [true] }, - }, - required: ['items', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { selector } = this.queryParams; - - return API.v1.success( - await findChannelAndPrivateAutocomplete({ - uid: this.userId, - selector: JSON.parse(selector), - }), - ); - }, -); - -API.v1.get( - 'rooms.autocomplete.channelAndPrivate.withPagination', - { - authRequired: true, - query: isRoomsAutocompleteChannelAndPrivateWithPaginationProps, - response: { - 200: ajv.compile<{ items: IRoom[]; total: number }>({ - type: 'object', - properties: { - items: { type: 'array', items: { type: 'object' } }, // relaxed: IRoom autocomplete subset - total: { type: 'number' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['items', 'total', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { selector } = this.queryParams; - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - - return API.v1.success( - await findChannelAndPrivateAutocompleteWithPagination({ - uid: this.userId, - selector: JSON.parse(selector), - pagination: { - offset, - count, - sort, - }, - }), - ); - }, -); - -API.v1.get( - 'rooms.autocomplete.availableForTeams', - { - authRequired: true, - query: isRoomsAutocompleteAvailableForTeamsProps, - response: { - 200: ajv.compile<{ items: IRoom[] }>({ - type: 'object', - properties: { - items: { type: 'array', items: { type: 'object' } }, // relaxed: IRoom autocomplete subset - success: { type: 'boolean', enum: [true] }, - }, - required: ['items', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { name } = this.queryParams; - - return API.v1.success( - await findRoomsAvailableForTeams({ - uid: this.userId, - name, - }), - ); - }, -); - -API.v1.post( - 'rooms.saveRoomSettings', - { - authRequired: true, - body: isRoomsSaveRoomSettingsProps, - response: { - 200: ajv.compile<{ rid: string }>({ - type: 'object', - properties: { - rid: { type: 'string' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['rid', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { rid, ...params } = this.bodyParams; - - const result = await saveRoomSettings(this.userId, rid, params); - - return API.v1.success({ rid: result.rid }); - }, -); - -const successResponseSchema = ajv.compile({ - type: 'object', - properties: { success: { type: 'boolean', enum: [true] } }, - required: ['success'], - additionalProperties: false, -}); - -API.v1.post( - 'rooms.changeArchivationState', - { - authRequired: true, - body: isRoomsChangeArchivationStateProps, - response: { - 200: successResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { rid, action } = this.bodyParams; - - if (action === 'archive') { - await executeArchiveRoom(this.userId, rid); - } else { - await executeUnarchiveRoom(this.userId, rid); - } - - return API.v1.success(); - }, -); - -API.v1.post( - 'rooms.export', - { - authRequired: true, - body: isRoomsExportProps, - response: { - 200: ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [true] }, - missing: { type: 'array', items: { type: 'string' } }, - }, - required: ['success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { rid, type } = this.bodyParams; - - if (!(await hasPermissionAsync(this.userId, 'mail-messages', rid))) { - throw new Meteor.Error('error-action-not-allowed', 'Mailing is not allowed'); - } - - const room = await Rooms.findOneById(rid); - if (!room) { - throw new Meteor.Error('error-invalid-room'); - } - - const user = await Users.findOneById(this.userId); - - if (!user || !(await canAccessRoomAsync(room, user))) { - throw new Meteor.Error('error-not-allowed', 'Not Allowed'); - } - - if (type === 'file') { - const { dateFrom, dateTo } = this.bodyParams; - const { format } = this.bodyParams; - - const convertedDateFrom = dateFrom ? new Date(dateFrom) : new Date(0); - const convertedDateTo = dateTo ? new Date(dateTo) : new Date(); - convertedDateTo.setDate(convertedDateTo.getDate() + 1); - - if (convertedDateFrom > convertedDateTo) { - throw new Meteor.Error('error-invalid-dates', 'From date cannot be after To date'); - } - - void dataExport.sendFile( - { - rid, - format, - dateFrom: convertedDateFrom, - dateTo: convertedDateTo, - }, - user, - ); - return API.v1.success(); - } - - if (type === 'email') { - const { toUsers, toEmails, subject, messages } = this.bodyParams; - - if ((!toUsers || toUsers.length === 0) && (!toEmails || toEmails.length === 0)) { - throw new Meteor.Error('error-invalid-recipient'); - } - - const result = await dataExport.sendViaEmail( - { - rid, - toUsers: (toUsers as string[]) || [], - toEmails: toEmails || [], - subject: subject || '', - messages: messages || [], - language: user.language || 'en', - }, - user, - ); - - return API.v1.success(result); - } - - return API.v1.failure(); - }, -); - -API.v1.get( - 'rooms.isMember', - { - authRequired: true, - query: isRoomsIsMemberProps, - response: { - 200: ajv.compile<{ isMember: boolean }>({ - type: 'object', - properties: { - isMember: { type: 'boolean' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['isMember', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const { roomId } = this.queryParams; - const usernameOrUserId = 'userId' in this.queryParams ? this.queryParams.userId : this.queryParams.username; - const [room, user] = await Promise.all([ - findRoomByIdOrName({ - params: { roomId }, - }), - Users.findOneByIdOrUsername(usernameOrUserId), - ]); - - if (!user?._id) { - return API.v1.failure('error-user-not-found'); - } - - if (await canAccessRoomAsync(room, { _id: this.user._id })) { - return API.v1.success({ - isMember: (await Subscriptions.countByRoomIdAndUserId(room._id, user._id)) > 0, - }); - } - return API.v1.forbidden(); - }, -); - -API.v1.get( - 'rooms.membersOrderedByRole', - { - authRequired: true, - query: isRoomsMembersOrderedByRoleProps, - response: { - 200: ajv.compile<{ members: IUser[]; count: number; offset: number; total: number }>({ - type: 'object', - properties: { - members: { type: 'array', items: { type: 'object' } }, // relaxed: projected IUser with role priority - count: { type: 'number' }, - offset: { type: 'number' }, - total: { type: 'number' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['members', 'count', 'offset', 'total', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 404: validateNotFoundErrorResponse, - }, - }, - async function action() { - const findResult = await findRoomByIdOrName({ - params: this.queryParams, - checkedArchived: false, - }); - - if (!(await canAccessRoomAsync(findResult, this.user))) { - return API.v1.notFound('The required "roomId" or "roomName" param provided does not match any room'); - } - - if (!isPublicRoom(findResult) && !isPrivateRoom(findResult)) { - return API.v1.failure('error-room-type-not-supported'); - } - - if (findResult.broadcast && !(await hasPermissionAsync(this.userId, 'view-broadcast-member-list', findResult._id))) { - return API.v1.unauthorized(); - } - - // Ensures that role priorities for the specified room are synchronized correctly. - // This function acts as a soft migration. If the `roomRolePriorities` field - // for the room has already been created and is up-to-date, no updates will be performed. - // If not, it will synchronize the role priorities of the users of the room. - await syncRolePrioritiesForRoomIfRequired(findResult._id); - - const { offset: skip, count: limit } = await getPaginationItems(this.queryParams); - const { sort = {} } = await this.parseJsonQuery(); - - const { status, filter } = this.queryParams; - - const { members, total } = await findUsersOfRoomOrderedByRole({ - rid: findResult._id, - ...(status && { status: { $in: status } }), - skip, - limit, - filter, - sort, - }); - - return API.v1.success({ - members, - count: members.length, - offset: skip, - total, - }); - }, -); - -API.v1.post( - 'rooms.muteUser', - { - authRequired: true, - body: isRoomsMuteUnmuteUserProps, - response: { - 200: successResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const user = await getUserFromParams(this.bodyParams); - - if (!user.username) { - return API.v1.failure('Invalid user'); - } - - await muteUserInRoom(this.userId, { rid: this.bodyParams.roomId, username: user.username }); - - return API.v1.success(); - }, -); - -API.v1.post( - 'rooms.unmuteUser', - { - authRequired: true, - body: isRoomsMuteUnmuteUserProps, - response: { - 200: successResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const user = await getUserFromParams(this.bodyParams); - - if (!user.username) { - return API.v1.failure('Invalid user'); - } - - await unmuteUserInRoom(this.userId, { rid: this.bodyParams.roomId, username: user.username }); - - return API.v1.success(); - }, -); - -API.v1.post( - 'rooms.open', - { - authRequired: true, - body: isRoomsOpenProps, - response: { - 200: successResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { roomId } = this.bodyParams; - - await openRoom(this.userId, roomId); - - return API.v1.success(); - }, -); - -API.v1.post( - 'rooms.join', - { - authRequired: true, - body: isRoomsJoinProps, - response: { - 200: ajv.compile<{ room: IRoom }>({ - type: 'object', - properties: { - room: { type: 'object' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['room', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { joinCode, ...params } = this.bodyParams; - const room = await findRoomByIdOrName({ params }); - - await Room.join({ room, user: this.user, joinCode }); - - return API.v1.success({ - room: await findRoomByIdOrName({ params }), - }); - }, -); - -API.v1.post( - 'rooms.hide', - { - authRequired: true, - body: isRoomsHideProps, - response: { - 200: successResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { roomId } = this.bodyParams; - - if (!(await canAccessRoomIdAsync(roomId, this.userId))) { - return API.v1.unauthorized(); - } - - const user = await Users.findOneById(this.userId, { projections: { _id: 1 } }); - - if (!user) { - return API.v1.failure('error-invalid-user'); - } - - const modCount = await hideRoomMethod(this.userId, roomId); - - if (!modCount) { - return API.v1.failure('error-room-already-hidden'); - } - - return API.v1.success(); - }, -); - -type RoomsFavorite = - | { - roomId: string; - favorite: boolean; - } - | { - roomName: string; - favorite: boolean; - }; - -type RoomsLeave = - | { - roomId: string; - } - | { - roomName: string; - }; - -const isRoomGetRolesPropsSchema = { - type: 'object', - properties: { - rid: { type: 'string' }, - }, - additionalProperties: false, - required: ['rid'], -}; - -const RoomsFavoriteSchema = { - anyOf: [ - { - type: 'object', - properties: { - favorite: { type: 'boolean' }, - roomName: { type: 'string' }, - }, - required: ['roomName', 'favorite'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - favorite: { type: 'boolean' }, - roomId: { type: 'string' }, - }, - required: ['roomId', 'favorite'], - additionalProperties: false, - }, - ], -}; - -const isRoomsLeavePropsSchema = { - anyOf: [ - { - type: 'object', - properties: { - roomId: { type: 'string' }, - }, - required: ['roomId'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - roomName: { type: 'string' }, - }, - required: ['roomName'], - additionalProperties: false, - }, - ], -}; - -const isRoomsFavoriteProps = ajv.compile(RoomsFavoriteSchema); -const isRoomsLeaveProps = ajv.compile(isRoomsLeavePropsSchema); -const roomsBannedUsersResponseSchema = ajv.compile<{ - success: true; - bannedUsers: RequiredField, '_id' | 'username'>[]; - count: number; - offset: number; - total: number; -}>({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [true] }, - bannedUsers: { - type: 'array', - items: { - type: 'object', - properties: { - _id: { type: 'string' }, - username: { type: 'string' }, - name: { type: 'string' }, - }, - required: ['_id', 'username'], - additionalProperties: false, - }, - }, - count: { type: 'number' }, - offset: { type: 'number' }, - total: { type: 'number' }, - }, - required: ['success', 'bannedUsers', 'count', 'offset', 'total'], - additionalProperties: false, -}); - -export const roomEndpoints = API.v1 - .get( - 'rooms.roles', - { - authRequired: true, - query: ajvQuery.compile<{ - rid: string; - }>(isRoomGetRolesPropsSchema), - response: { - 200: ajv.compile<{ - roles: RoomRoles[]; - }>({ - type: 'object', - properties: { - roles: { - type: 'array', - items: { - type: 'object', - properties: { - rid: { type: 'string' }, - u: { - type: 'object', - properties: { _id: { type: 'string' }, username: { type: 'string' } }, - required: ['_id', 'username'], - }, - roles: { type: 'array', items: { type: 'string' } }, - }, - required: ['rid', 'u', 'roles'], - }, - }, - }, - required: ['roles'], - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function () { - const { rid } = this.queryParams; - const roles = await executeGetRoomRoles(rid, this.user); - - return API.v1.success({ - roles, - }); - }, - ) - .get( - 'rooms.adminRooms.privateRooms', - { - authRequired: true, - permissionsRequired: ['view-room-administration'], - query: ajvQuery.compile<{ - filter?: string; - offset?: number; - count?: number; - sort?: string; - }>({ - type: 'object', - properties: { - filter: { type: 'string' }, - offset: { type: 'number' }, - count: { type: 'number' }, - sort: { type: 'string' }, - }, - additionalProperties: true, - }), - response: { - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateUnauthorizedErrorResponse, - 200: ajv.compile<{ - rooms: Array & IRoomAbacRedaction>; - count: number; - offset: number; - total: number; - }>({ - type: 'object', - properties: { - rooms: { - type: 'array', - items: { type: 'object' }, // relaxed: IRoom subset - }, - count: { type: 'number' }, - offset: { type: 'number' }, - total: { type: 'number' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['rooms', 'count', 'offset', 'total', 'success'], - additionalProperties: false, - }), - }, - }, - async function action() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - const { filter } = this.queryParams; - - const name = (filter || '').trim(); - - const { cursor, totalCount } = Rooms.findPrivateRoomsAndTeamsPaginated(name, { - skip: offset, - limit: count, - sort: sort || { default: -1, name: 1 }, - projection: adminFields, - }); - - const [rooms, total] = await Promise.all([cursor.map(stripABACManagedFieldsForAdmin).toArray(), totalCount]); - - return API.v1.success({ - rooms: await scopeAdminRoomsForAbac(rooms, this.userId), - count: rooms.length, - offset, - total, - }); - }, - ) - .post( - 'rooms.invite', - { - authRequired: true, - body: isRoomsInviteProps, - response: { - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 200: ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [true] }, - }, - required: ['success'], - additionalProperties: false, - }), - }, - }, - async function action() { - const { roomId, action } = this.bodyParams; - - try { - await FederationMatrix.handleInvite(roomId, this.userId, action); - return API.v1.success(); - } catch (error) { - return API.v1.failure({ error: `Failed to handle invite: ${error instanceof Error ? error.message : String(error)}` }); - } - }, - ) - .post( - 'rooms.favorite', - { - authRequired: true, - body: isRoomsFavoriteProps, - response: { - 200: ajv.compile({ - type: 'object', - properties: { - success: { - type: 'boolean', - enum: [true], - description: 'Indicates if the request was successful.', - }, - }, - required: ['success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { favorite } = this.bodyParams; - - const room = await findRoomByIdOrName({ params: this.bodyParams }); - - await toggleFavoriteMethod(this.userId, room._id, favorite); - - return API.v1.success(); - }, - ) - .post( - 'rooms.leave', - { - authRequired: true, - body: isRoomsLeaveProps, - response: { - 200: ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [true] }, - }, - required: ['success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const room = await findRoomByIdOrName({ params: this.bodyParams }); - - const user = await Users.findOneById(this.userId); - - if (!user) { - return API.v1.failure('error-invalid-user'); - } - - await leaveRoomMethod(user, room._id); - - return API.v1.success(); - }, - ) - .post( - 'rooms.banUser', - { - authRequired: true, - body: isRoomsBanUserProps, - response: { - 200: ajv.compile({ - type: 'object', - properties: { success: { type: 'boolean', enum: [true] } }, - required: ['success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const user = await getUserFromParams(this.bodyParams); - - if (!user.username) { - return API.v1.failure('Invalid user'); - } - - await banUserFromRoomMethod(this.userId, { rid: this.bodyParams.roomId, username: user.username }); - - return API.v1.success(); - }, - ) - .post( - 'rooms.unbanUser', - { - authRequired: true, - body: isRoomsUnbanUserProps, - response: { - 200: ajv.compile({ - type: 'object', - properties: { success: { type: 'boolean', enum: [true] } }, - required: ['success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const user = await getUserFromParams(this.bodyParams, true); - - if (!user.username) { - return API.v1.failure('Invalid user'); - } - - await unbanUserFromRoom(this.userId, { rid: this.bodyParams.roomId, username: user.username }); - - return API.v1.success(); - }, - ) - .get( - 'rooms.bannedUsers', - { - authRequired: true, - query: isRoomsBannedUsersProps, - response: { - 200: roomsBannedUsersResponseSchema, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { roomId } = this.queryParams; - - if (!(await canAccessRoomIdAsync(roomId, this.userId))) { - return API.v1.unauthorized(); - } - - const { offset, count } = await getPaginationItems(this.queryParams); - - const { cursor, totalCount } = Subscriptions.findPaginated({ rid: roomId, status: 'BANNED' as const }, { offset, count }); - - const [bannedSubs, total] = await Promise.all([cursor.toArray(), totalCount]); - - const userIds = bannedSubs.map((sub) => sub.u._id); - const users = await Users.find, '_id' | 'username'>>( - { _id: { $in: userIds } }, - { projection: { username: 1, name: 1 } }, - ).toArray(); - - return API.v1.success({ - bannedUsers: users, - count: users.length, - offset, - total, - }); - }, - ); -type RoomEndpoints = ExtractRoutesFromAPI & - ExtractRoutesFromAPI & - ExtractRoutesFromAPI & - ExtractRoutesFromAPI; - -declare module '@rocket.chat/rest-typings' { - // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface - interface Endpoints extends RoomEndpoints {} -} diff --git a/apps/meteor/app/api/server/v1/settings.ts b/apps/meteor/app/api/server/v1/settings.ts deleted file mode 100644 index 6c764f4105bef..0000000000000 --- a/apps/meteor/app/api/server/v1/settings.ts +++ /dev/null @@ -1,448 +0,0 @@ -import type { - FacebookOAuthConfiguration, - ISetting, - ISettingColor, - LoginServiceConfiguration, - TwitterOAuthConfiguration, - OAuthConfiguration, -} from '@rocket.chat/core-typings'; -import { isActionSettingWithEndpoint, isSettingAction, isSettingColor } from '@rocket.chat/core-typings'; -import { LoginServiceConfiguration as LoginServiceConfigurationModel, Settings } from '@rocket.chat/models'; -import { - ajv, - isSettingsUpdatePropDefault, - isSettingsUpdatePropsActions, - isSettingsUpdatePropsColor, - isSettingsPublicWithPaginationProps, - isSettingsGetParams, - isSettingsBulkProps, - validateForbiddenErrorResponse, - validateUnauthorizedErrorResponse, - validateBadRequestErrorResponse, -} from '@rocket.chat/rest-typings'; -import { Meteor } from 'meteor/meteor'; -import type { FindOptions } from 'mongodb'; -import _ from 'underscore'; - -import { updateAuditedByUser } from '../../../../server/settings/lib/auditedSettingUpdates'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { disableCustomScripts } from '../../../lib/server/functions/disableCustomScripts'; -import { saveSettingsBulk } from '../../../lib/server/functions/saveSettingsBulk'; -import { checkSettingValueBounds } from '../../../lib/server/lib/checkSettingValueBonds'; -import { notifyOnSettingChanged, notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; -import { addOAuthServiceMethod } from '../../../lib/server/methods/addOAuthService'; -import { SettingsEvents, settings } from '../../../settings/server'; -import { setValue } from '../../../settings/server/raw'; -import { API } from '../api'; -import { getPaginationItems } from '../helpers/getPaginationItems'; - -async function fetchSettings( - query: Parameters[0], - sort: FindOptions['sort'], - offset: FindOptions['skip'], - count: FindOptions['limit'], - fields: FindOptions['projection'], -): Promise<{ settings: ISetting[]; totalCount: number }> { - const { cursor, totalCount } = Settings.findPaginated(query || {}, { - sort: sort || { _id: 1 }, - skip: offset, - limit: count, - projection: { _id: 1, value: 1, enterprise: 1, invalidValue: 1, modules: 1, ...fields }, - }); - - const [settingsList, total] = await Promise.all([cursor.toArray(), totalCount]); - - SettingsEvents.emit('fetch-settings', settingsList); - return { settings: settingsList, totalCount: total }; -} - -const settingsPublicResponseSchema = ajv.compile<{ settings: ISetting[]; count: number; offset: number; total: number }>({ - type: 'object', - properties: { - settings: { type: 'array', items: { type: 'object' } }, - count: { type: 'number' }, - offset: { type: 'number' }, - total: { type: 'number' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['settings', 'count', 'offset', 'total', 'success'], - additionalProperties: false, -}); - -const settingsOAuthResponseSchema = ajv.compile<{ services: Partial[] }>({ - type: 'object', - properties: { - services: { type: 'array', items: { type: 'object' } }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['services', 'success'], - additionalProperties: false, -}); - -const addCustomOAuthBodySchema = ajv.compile<{ name: string }>({ - type: 'object', - properties: { name: { type: 'string' } }, - required: ['name'], - additionalProperties: false, -}); - -const settingsListResponseSchema = ajv.compile<{ settings: ISetting[]; count: number; offset: number; total: number }>({ - type: 'object', - properties: { - settings: { type: 'array', items: { type: 'object' } }, - count: { type: 'number' }, - offset: { type: 'number' }, - total: { type: 'number' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['settings', 'count', 'offset', 'total', 'success'], - additionalProperties: false, -}); - -const settingByIdGetResponseSchema = ajv.compile>({ - type: 'object', - properties: { - _id: { type: 'string' }, - value: {}, - success: { type: 'boolean', enum: [true] }, - }, - required: ['_id', 'value', 'success'], - additionalProperties: false, -}); - -const settingByIdPostResponseSchema = ajv.compile({ - type: 'object', - properties: { success: { type: 'boolean', enum: [true] } }, - required: ['success'], - additionalProperties: false, -}); - -const settingsUpdateBodySchema = ajv.compile<{ value?: unknown; execute?: boolean; editor?: string }>({ - type: 'object', - properties: { - value: {}, - execute: { type: 'boolean' }, - editor: { type: 'string' }, - }, - additionalProperties: true, -}); - -const serviceConfigurationsResponseSchema = ajv.compile<{ configurations: LoginServiceConfiguration[] }>({ - type: 'object', - properties: { - configurations: { type: 'array', items: { type: 'object' } }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['configurations', 'success'], - additionalProperties: false, -}); - -API.v1.get( - 'settings.public', - { - authRequired: false, - query: isSettingsPublicWithPaginationProps, - response: { - 200: settingsPublicResponseSchema, - }, - }, - async function action() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort, fields, query } = await this.parseJsonQuery(); - const { _id } = this.queryParams; - - const parsedQueryId = typeof _id === 'string' && _id ? { _id: { $in: _id.split(',').map((id) => id.trim()) } } : {}; - - const ourQuery = { - ...query, - ...parsedQueryId, - hidden: { $ne: true }, - public: true, - }; - - const { settings: settingsList, totalCount: total } = await fetchSettings(ourQuery, sort, offset, count, fields); - - return API.v1.success({ - settings: settingsList, - count: settingsList.length, - offset, - total, - }); - }, -); - -API.v1.get( - 'settings.oauth', - { - authRequired: false, - response: { - 200: settingsOAuthResponseSchema, - }, - }, - async function action() { - const oAuthServicesEnabled = await LoginServiceConfigurationModel.find({}, { projection: { secret: 0 } }).toArray(); - - return API.v1.success({ - services: oAuthServicesEnabled.map((service) => { - if (!service) { - return service; - } - - if ((service as OAuthConfiguration).custom || (service.service && ['saml', 'cas', 'wordpress'].includes(service.service))) { - return { ...service }; - } - - return { - _id: service._id, - name: service.service, - clientId: - (service as FacebookOAuthConfiguration).appId || - (service as OAuthConfiguration).clientId || - (service as TwitterOAuthConfiguration).consumerKey, - buttonLabelText: service.buttonLabelText || '', - buttonColor: service.buttonColor || '', - buttonLabelColor: service.buttonLabelColor || '', - custom: false, - }; - }), - }); - }, -); - -API.v1.post( - 'settings.addCustomOAuth', - { - authRequired: true, - twoFactorRequired: true, - body: addCustomOAuthBodySchema, - response: { - 200: ajv.compile({ - type: 'object', - properties: { success: { type: 'boolean', enum: [true] } }, - required: ['success'], - additionalProperties: false, - }), - 400: ajv.compile({ - type: 'object', - properties: { success: { type: 'boolean', enum: [false] }, error: { type: 'string' } }, - required: ['success'], - additionalProperties: false, - }), - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { name } = this.bodyParams; - if (!name?.trim()) { - throw new Meteor.Error('error-name-param-not-provided', 'The parameter "name" is required'); - } - - await addOAuthServiceMethod(this.userId, name); - - return API.v1.success(); - }, -); - -API.v1.get( - 'settings', - { - authRequired: true, - query: isSettingsGetParams, - response: { - 200: settingsListResponseSchema, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { includeDefaults } = this.queryParams; - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort, fields, query } = await this.parseJsonQuery(); - - let ourQuery: Parameters[0] = { - hidden: { $ne: true }, - }; - - if (!(await hasPermissionAsync(this.userId, 'view-privileged-setting'))) { - ourQuery.public = true; - } - - ourQuery = Object.assign({}, query, ourQuery); - - if (includeDefaults) { - fields.packageValue = 1; - } - - const { settings: settingsList, totalCount: total } = await fetchSettings(ourQuery, sort, offset, count, fields); - - return API.v1.success({ - settings: settingsList, - count: settingsList.length, - offset, - total, - }); - }, -); - -API.v1.get( - 'settings/:_id', - { - authRequired: true, - permissionsRequired: { - GET: { permissions: ['view-privileged-setting'], operation: 'hasAll' }, - }, - response: { - 200: settingByIdGetResponseSchema, - 400: ajv.compile({ - type: 'object', - properties: { success: { type: 'boolean', enum: [false] } }, - required: ['success'], - additionalProperties: true, - }), - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const { _id } = this.urlParams; - const setting = await Settings.findOneNotHiddenById(_id); - if (!setting) { - return API.v1.failure(); - } - return API.v1.success(_.pick(setting, '_id', 'value')); - }, -); - -API.v1.post( - 'settings/:_id', - { - authRequired: true, - permissionsRequired: { - POST: { permissions: ['edit-privileged-setting'], operation: 'hasAll' }, - }, - twoFactorRequired: true, - body: settingsUpdateBodySchema, - response: { - 200: settingByIdPostResponseSchema, - 400: ajv.compile({ - type: 'object', - properties: { success: { type: 'boolean', enum: [false] } }, - required: ['success'], - additionalProperties: true, - }), - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const { _id } = this.urlParams; - if (typeof _id !== 'string') { - throw new Meteor.Error('error-id-param-not-provided', 'The parameter "id" is required'); - } - - if (disableCustomScripts() && /^Custom_Script_/.test(_id)) { - return API.v1.forbidden('Custom scripts are disabled'); - } - - const setting = await Settings.findOneNotHiddenById(_id); - - if (!setting) { - return API.v1.failure(); - } - - const { bodyParams } = this; - - if ( - isSettingAction(setting) && - isSettingsUpdatePropsActions(bodyParams) && - bodyParams.execute && - !isActionSettingWithEndpoint(setting.value) - ) { - await Meteor.callAsync(setting.value); - return API.v1.success(); - } - - const auditSettingOperation = updateAuditedByUser({ - _id: this.userId, - username: this.user.username, - ip: this.requestIp ?? '', - useragent: this.request.headers.get('user-agent') ?? '', - }); - - if (isSettingColor(setting) && isSettingsUpdatePropsColor(bodyParams)) { - const updateOptionsPromise = Settings.updateOptionsById(_id, { editor: bodyParams.editor }); - const updateValuePromise = auditSettingOperation(Settings.updateValueNotHiddenById, _id, bodyParams.value); - - const [updateOptionsResult, updateValueResult] = await Promise.all([updateOptionsPromise, updateValuePromise]); - - if (updateOptionsResult.modifiedCount || updateValueResult.modifiedCount) { - await notifyOnSettingChangedById(_id); - } - - return API.v1.success(); - } - - if (isSettingsUpdatePropDefault(bodyParams)) { - checkSettingValueBounds(setting, bodyParams.value); - - const { matchedCount } = await auditSettingOperation(Settings.updateValueNotHiddenById, _id, bodyParams.value); - - if (!matchedCount) { - return API.v1.failure(); - } - - const s = await Settings.findOneNotHiddenById(_id); - if (!s) { - return API.v1.failure(); - } - - settings.set(s); - setValue(_id, bodyParams.value); - - await notifyOnSettingChanged(s); - - return API.v1.success(); - } - - return API.v1.failure(); - }, -); - -API.v1.post( - 'settings', - { - authRequired: true, - twoFactorRequired: true, - twoFactorOptions: { disableRememberMe: true }, - body: isSettingsBulkProps, - response: { - 200: settingByIdPostResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - await saveSettingsBulk(this.userId, this.bodyParams.settings, { - username: this.user.username ?? '', - ip: this.requestIp ?? '', - useragent: this.request.headers.get('user-agent') ?? '', - }); - - return API.v1.success(); - }, -); - -API.v1.get( - 'service.configurations', - { - authRequired: false, - response: { - 200: serviceConfigurationsResponseSchema, - }, - }, - async function action() { - return API.v1.success({ - configurations: await LoginServiceConfigurationModel.find({}, { projection: { secret: 0 } }).toArray(), - }); - }, -); diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts deleted file mode 100644 index 66a5ff9304009..0000000000000 --- a/apps/meteor/app/api/server/v1/users.ts +++ /dev/null @@ -1,2090 +0,0 @@ -import { MeteorError, Presence, Team } from '@rocket.chat/core-services'; -import type { IExportOperation, ILoginToken, IPersonalAccessToken, IUser, UserStatus } from '@rocket.chat/core-typings'; -import { Users, Subscriptions, Sessions, OAuthAccessTokens, OAuthRefreshTokens, OAuthAuthCodes } from '@rocket.chat/models'; -import { - isUserCreateParamsPOST, - isUserSetActiveStatusParamsPOST, - isUserDeactivateIdleParamsPOST, - isUsersInfoParamsGetProps, - isUsersListStatusProps, - isUsersSendWelcomeEmailProps, - isUserRegisterParamsPOST, - isUserLogoutParamsPOST, - isUsersListTeamsProps, - isUsersAutocompleteProps, - isUsersSetAvatarProps, - isUsersUpdateParamsPOST, - isUsersUpdateOwnBasicInfoParamsPOST, - isUsersSetPreferencesParamsPOST, - isUsersCheckUsernameAvailabilityParamsGET, - isUsersSendConfirmationEmailParamsPOST, - isUsersListParamsGET, - isUsersPresenceParamsGET, - isUsersRequestDataDownloadParamsGET, - isUsersGetPresenceParamsGET, - isUsersGetStatusParamsGET, - ajv, - validateBadRequestErrorResponse, - validateUnauthorizedErrorResponse, - validateForbiddenErrorResponse, -} from '@rocket.chat/rest-typings'; -import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { getLoginExpirationInMs } from '@rocket.chat/tools'; -import { Accounts } from 'meteor/accounts-base'; -import { Match, check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; -import type { Filter } from 'mongodb'; - -import { generatePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/generateToken'; -import { regeneratePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/regenerateToken'; -import { removePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/removeToken'; -import { UserChangedAuditStore } from '../../../../server/lib/auditServerEvents/userChanged'; -import { i18n } from '../../../../server/lib/i18n'; -import { SystemLogger } from '../../../../server/lib/logger/system'; -import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey'; -import { registerUser } from '../../../../server/methods/registerUser'; -import { requestDataDownload } from '../../../../server/methods/requestDataDownload'; -import { resetAvatar } from '../../../../server/methods/resetAvatar'; -import { saveUserPreferences } from '../../../../server/methods/saveUserPreferences'; -import { executeSaveUserProfile } from '../../../../server/methods/saveUserProfile'; -import { sendConfirmationEmail } from '../../../../server/methods/sendConfirmationEmail'; -import { sendForgotPasswordEmail } from '../../../../server/methods/sendForgotPasswordEmail'; -import { executeSetUserActiveStatus } from '../../../../server/methods/setUserActiveStatus'; -import { getUserForCheck, emailCheck } from '../../../2fa/server/code'; -import { resetTOTP } from '../../../2fa/server/functions/resetTOTP'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { checkEmailAvailability } from '../../../lib/server/functions/checkEmailAvailability'; -import { - checkUsernameAvailability, - checkUsernameAvailabilityWithValidation, -} from '../../../lib/server/functions/checkUsernameAvailability'; -import { deleteUser } from '../../../lib/server/functions/deleteUser'; -import { getAvatarSuggestionForUser } from '../../../lib/server/functions/getAvatarSuggestionForUser'; -import { getFullUserDataByUniqueSearchTerm, defaultFields, fullFields } from '../../../lib/server/functions/getFullUserData'; -import { generateUsernameSuggestion } from '../../../lib/server/functions/getUsernameSuggestion'; -import { saveCustomFields } from '../../../lib/server/functions/saveCustomFields'; -import { saveCustomFieldsWithoutValidation } from '../../../lib/server/functions/saveCustomFieldsWithoutValidation'; -import { saveUser } from '../../../lib/server/functions/saveUser'; -import { sendWelcomeEmail } from '../../../lib/server/functions/saveUser/sendUserEmail'; -import { canEditExtension } from '../../../lib/server/functions/saveUser/validateUserEditing'; -import { setUserAvatar } from '../../../lib/server/functions/setUserAvatar'; -import { setUsernameWithValidation } from '../../../lib/server/functions/setUsername'; -import { validateCustomFields } from '../../../lib/server/functions/validateCustomFields'; -import { validateNameChars } from '../../../lib/server/functions/validateNameChars'; -import { validateUsername } from '../../../lib/server/functions/validateUsername'; -import { notifyOnUserChange, notifyOnUserChangeAsync } from '../../../lib/server/lib/notifyListener'; -import { generateAccessToken } from '../../../lib/server/methods/createToken'; -import { deleteUserOwnAccount } from '../../../lib/server/methods/deleteUserOwnAccount'; -import { settings } from '../../../settings/server'; -import { isSMTPConfigured } from '../../../utils/server/functions/isSMTPConfigured'; -import { getURL } from '../../../utils/server/getURL'; -import type { ExtractRoutesFromAPI } from '../ApiClass'; -import { API } from '../api'; -import { getPaginationItems } from '../helpers/getPaginationItems'; -import { getUserFromParams } from '../helpers/getUserFromParams'; -import { getUserInfo } from '../helpers/getUserInfo'; -import { isUserFromParams } from '../helpers/isUserFromParams'; -import { getUploadFormData } from '../lib/getUploadFormData'; -import { isValidQuery } from '../lib/isValidQuery'; -import { findPaginatedUsersByStatus, findUsersToAutocomplete, getInclusiveFields, getNonEmptyFields, getNonEmptyQuery } from '../lib/users'; - -API.v1.addRoute( - 'users.getAvatar', - { authRequired: true }, - { - async get() { - const user = await getUserFromParams(this.queryParams); - - const url = getURL(`/avatar/${user.username}`, { cdn: false, full: true }); - this.response.headers.set('Location', url); - - return { - statusCode: 307, - body: url, - }; - }, - }, -); - -const voidSuccessResponse = ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [true] }, - }, - required: ['success'], - additionalProperties: false, -}); - -// user shape varies by projection and permissions โ€” use $ref when IUser is available in typia -const userObjectResponse = ajv.compile<{ user: object }>({ - type: 'object', - properties: { - user: { type: 'object' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['user', 'success'], - additionalProperties: false, -}); - -API.v1.post( - 'users.update', - { - authRequired: true, - twoFactorRequired: true, - body: isUsersUpdateParamsPOST, - response: { - 200: userObjectResponse, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const userData = { _id: this.bodyParams.userId, ...this.bodyParams.data }; - - if (userData.name && !validateNameChars(userData.name)) { - return API.v1.failure('Name contains invalid characters'); - } - const auditStore = new UserChangedAuditStore({ - _id: this.user._id, - ip: this.requestIp || '', - useragent: this.request.headers.get('user-agent') || '', - username: this.user.username, - }); - - await saveUser(this.userId, userData, { auditStore }); - - if (typeof this.bodyParams.data.active !== 'undefined') { - const { - userId, - data: { active }, - confirmRelinquish, - } = this.bodyParams; - await executeSetUserActiveStatus(this.userId, userId, active, Boolean(confirmRelinquish)); - } - - const { fields } = await this.parseJsonQuery(); - - const user = await Users.findOneById(this.bodyParams.userId, { projection: fields }); - if (!user) { - return API.v1.failure('User not found'); - } - - return API.v1.success({ user }); - }, -); - -API.v1 - .post( - 'users.updateOwnBasicInfo', - { - authRequired: true, - userWithoutUsername: true, - body: isUsersUpdateOwnBasicInfoParamsPOST, - rateLimiterOptions: { - numRequestsAllowed: 1, - intervalTimeInMS: 60000, - }, - response: { - 200: userObjectResponse, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const userData = { - email: this.bodyParams.data.email, - realname: this.bodyParams.data.name, - username: this.bodyParams.data.username, - nickname: this.bodyParams.data.nickname, - bio: this.bodyParams.data.bio, - statusText: this.bodyParams.data.statusText, - statusType: this.bodyParams.data.statusType, - newPassword: this.bodyParams.data.newPassword, - typedPassword: this.bodyParams.data.currentPassword, - }; - - if (userData.realname && !validateNameChars(userData.realname)) { - return API.v1.failure('Name contains invalid characters'); - } - - // saveUserProfile now uses the default two factor authentication procedures, so we need to provide that - const twoFactorOptions = !userData.typedPassword - ? null - : { - twoFactorCode: userData.typedPassword, - twoFactorMethod: 'password', - }; - - await executeSaveUserProfile.call(this, this.user, userData, this.bodyParams.customFields, twoFactorOptions); - - return API.v1.success({ - user: await getUserInfo((await Users.findOneById(this.userId, { projection: API.v1.defaultFieldsToExclude })) as IUser, false), - }); - }, - ) - .post( - 'users.setPreferences', - { - authRequired: true, - body: isUsersSetPreferencesParamsPOST, - response: { - 200: userObjectResponse, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - if ( - this.bodyParams.userId && - this.bodyParams.userId !== this.userId && - !(await hasPermissionAsync(this.userId, 'edit-other-user-info')) - ) { - throw new Meteor.Error('error-action-not-allowed', 'Editing user is not allowed'); - } - const userId = this.bodyParams.userId ? this.bodyParams.userId : this.userId; - if (!(await Users.findOneById(userId))) { - throw new Meteor.Error('error-invalid-user', 'The optional "userId" param provided does not match any users'); - } - - await saveUserPreferences(this.bodyParams.data, userId); - const user = await Users.findOneById(userId, { - projection: { - 'settings.preferences': 1, - 'language': 1, - }, - }); - - if (!user) { - return API.v1.failure('User not found'); - } - - return API.v1.success({ - user: { - _id: user._id, - settings: { - preferences: { - ...user.settings?.preferences, - language: user.language, - }, - }, - } as unknown as Required>, - }); - }, - ) - .post( - 'users.setAvatar', - { - authRequired: true, - body: isUsersSetAvatarProps, - response: { - 200: voidSuccessResponse, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const canEditOtherUserAvatar = await hasPermissionAsync(this.userId, 'edit-other-user-avatar'); - - if (!settings.get('Accounts_AllowUserAvatarChange') && !canEditOtherUserAvatar) { - throw new Meteor.Error('error-not-allowed', 'Change avatar is not allowed', { - method: 'users.setAvatar', - }); - } - - let user = await (async (): Promise | undefined | null> => { - if (isUserFromParams(this.bodyParams, this.userId, this.user)) { - return Users.findOneById(this.userId); - } - if (canEditOtherUserAvatar) { - return getUserFromParams(this.bodyParams); - } - })(); - - if (!user) { - return API.v1.forbidden(); - } - - if (this.bodyParams.avatarUrl) { - await setUserAvatar(user, this.bodyParams.avatarUrl, '', 'url'); - return API.v1.success(); - } - - const image = await getUploadFormData( - { - request: this.request, - }, - { field: 'image', sizeLimit: settings.get('FileUpload_MaxFileSize') }, - ); - - if (!image) { - return API.v1.failure("The 'image' param is required"); - } - - const { fields, fileBuffer, mimetype } = image; - - const sentTheUserByFormData = fields.userId || fields.username; - if (sentTheUserByFormData) { - if (fields.userId) { - user = await Users.findOneById>(fields.userId, { projection: { username: 1 } }); - } else if (fields.username) { - user = await Users.findOneByUsernameIgnoringCase>(fields.username, { - projection: { username: 1 }, - }); - } - - if (!user) { - throw new Meteor.Error('error-invalid-user', 'The optional "userId" or "username" param provided does not match any users'); - } - - const isAnotherUser = this.userId !== user._id; - if (isAnotherUser && !(await hasPermissionAsync(this.userId, 'edit-other-user-avatar'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } - } - - await setUserAvatar(user, fileBuffer, mimetype, 'rest'); - - return API.v1.success(); - }, - ) - .post( - 'users.create', - { - authRequired: true, - body: isUserCreateParamsPOST, - response: { - 200: userObjectResponse, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - // New change made by pull request #5152 - if (typeof this.bodyParams.joinDefaultChannels === 'undefined') { - this.bodyParams.joinDefaultChannels = true; - } - - if (this.bodyParams.name && !validateNameChars(this.bodyParams.name)) { - return API.v1.failure('Name contains invalid characters'); - } - - if (this.bodyParams.customFields) { - validateCustomFields(this.bodyParams.customFields); - } - - if (this.bodyParams.freeSwitchExtension && !(await canEditExtension(this.bodyParams.freeSwitchExtension))) { - return API.v1.failure('Setting user voice call extension is not allowed', 'error-action-not-allowed'); - } - - const newUserId = await saveUser(this.userId, this.bodyParams); - const userId = typeof newUserId !== 'string' ? this.userId : newUserId; - - if (this.bodyParams.customFields) { - await saveCustomFieldsWithoutValidation(userId, this.bodyParams.customFields); - } - - if (typeof this.bodyParams.active !== 'undefined') { - await executeSetUserActiveStatus(this.userId, userId, this.bodyParams.active); - } - - const { fields } = await this.parseJsonQuery(); - - const user = await Users.findOneById(userId, { projection: fields }); - if (!user) { - return API.v1.failure('User not found'); - } - - return API.v1.success({ user }); - }, - ); - -API.v1.post( - 'users.delete', - { - authRequired: true, - permissionsRequired: ['delete-user'], - body: ajv.compile<{ userId?: string; username?: string; user?: string; confirmRelinquish?: boolean }>({ - type: 'object', - properties: { - userId: { type: 'string' }, - username: { type: 'string' }, - user: { type: 'string' }, - confirmRelinquish: { type: 'boolean', nullable: true }, - }, - anyOf: [{ required: ['userId'] }, { required: ['username'] }, { required: ['user'] }], - additionalProperties: false, - }), - response: { - 200: ajv.compile<{ deletedRooms: string[] }>({ - type: 'object', - properties: { - deletedRooms: { type: 'array', items: { type: 'string' } }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['deletedRooms', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const user = await getUserFromParams(this.bodyParams); - const { confirmRelinquish = false } = this.bodyParams; - - const { deletedRooms } = await deleteUser(user._id, confirmRelinquish, this.userId); - - return API.v1.success({ deletedRooms }); - }, -); - -API.v1.post( - 'users.deleteOwnAccount', - { - authRequired: true, - body: ajv.compile<{ password: string; confirmRelinquish?: boolean }>({ - type: 'object', - properties: { - password: { type: 'string' }, - confirmRelinquish: { type: 'boolean', nullable: true }, - }, - required: ['password'], - additionalProperties: false, - }), - response: { - 200: ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [true] }, - }, - required: ['success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - if (!settings.get('Accounts_AllowDeleteOwnAccount')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } - - const { confirmRelinquish = false } = this.bodyParams; - - await deleteUserOwnAccount(this.userId, this.bodyParams.password, confirmRelinquish); - - return API.v1.success(); - }, -); - -API.v1.post( - 'users.setActiveStatus', - { - authRequired: true, - body: isUserSetActiveStatusParamsPOST, - permissionsRequired: { - POST: { permissions: ['edit-other-user-active-status', 'manage-moderation-actions'], operation: 'hasAny' }, - }, - response: { - 200: ajv.compile<{ user: Pick }>({ - type: 'object', - properties: { - user: { - type: 'object', - properties: { - _id: { type: 'string' }, - active: { type: 'boolean' }, - }, - required: ['_id', 'active'], - additionalProperties: false, - }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['user', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { userId, activeStatus, confirmRelinquish = false } = this.bodyParams; - await executeSetUserActiveStatus(this.userId, userId, activeStatus, confirmRelinquish); - - const user = await Users.findOneById(this.bodyParams.userId, { projection: { active: 1 } }); - if (!user) { - return API.v1.failure('User not found'); - } - return API.v1.success({ - user, - }); - }, -); - -API.v1.post( - 'users.deactivateIdle', - { - authRequired: true, - body: isUserDeactivateIdleParamsPOST, - permissionsRequired: ['edit-other-user-active-status'], - response: { - 200: ajv.compile<{ count: number }>({ - type: 'object', - properties: { - count: { type: 'number' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['count', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { daysIdle, role = 'user' } = this.bodyParams; - - const lastLoggedIn = new Date(); - lastLoggedIn.setDate(lastLoggedIn.getDate() - daysIdle); - - const ids = await Users.findActiveNotLoggedInAfterWithRole(lastLoggedIn, role, { projection: { _id: 1 } }) - .map(({ _id }: { _id: string }) => _id) - .toArray(); - - const { modifiedCount: count } = await Users.setActiveNotLoggedInAfterWithRole(lastLoggedIn, role, false); - - await Promise.all([ - OAuthAccessTokens.deleteByUserIds(ids), - OAuthRefreshTokens.deleteByUserIds(ids), - OAuthAuthCodes.deleteByUserIds(ids), - ]); - - ids.forEach((_id) => { - void notifyOnUserChange({ - clientAction: 'updated', - id: _id, - diff: { 'services.resume.loginTokens': [], 'active': false }, - }); - }); - - return API.v1.success({ - count, - }); - }, -); - -API.v1.get( - 'users.info', - { - authRequired: true, - query: isUsersInfoParamsGetProps, - response: { - // user shape varies by projection, permissions, and includeUserRooms - 200: userObjectResponse, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const searchTerms: [string, 'id' | 'username' | 'importId' | 'email' | 'freeSwitchExtension'] | false = - ('userId' in this.queryParams && !!this.queryParams.userId && [this.queryParams.userId, 'id']) || - ('username' in this.queryParams && !!this.queryParams.username && [this.queryParams.username, 'username']) || - ('importId' in this.queryParams && !!this.queryParams.importId && [this.queryParams.importId, 'importId']) || - ('email' in this.queryParams && !!this.queryParams.email && [this.queryParams.email, 'email']) || - ('freeSwitchExtension' in this.queryParams && - !!this.queryParams.freeSwitchExtension && [this.queryParams.freeSwitchExtension, 'freeSwitchExtension']); - - if (!searchTerms) { - return API.v1.failure('Invalid search query.'); - } - - const user = await getFullUserDataByUniqueSearchTerm(this.userId, ...searchTerms); - - if (!user) { - return API.v1.failure('User not found.'); - } - - const myself = user._id === this.userId; - - if (this.queryParams.includeUserRooms === 'true' && (myself || (await hasPermissionAsync(this.userId, 'view-other-user-channels')))) { - return API.v1.success({ - user: { - ...user, - rooms: await Subscriptions.findByUserId(user._id, { - projection: { - rid: 1, - name: 1, - t: 1, - roles: 1, - unread: 1, - federated: 1, - }, - sort: { - t: 1, - name: 1, - }, - }).toArray(), - }, - }); - } - - return API.v1.success({ - user, - }); - }, -); - -// users.list accepts arbitrary query filter fields (name, username, etc.) -// that cannot be statically defined โ€” keeping as addRoute until params are known -API.v1.addRoute( - 'users.list', - { - authRequired: true, - queryOperations: ['$or', '$and'], - permissionsRequired: ['view-d-room'], - query: isUsersListParamsGET, - }, - { - async get() { - if ( - settings.get('API_Apply_permission_view-outside-room_on_users-list') && - !(await hasPermissionAsync(this.userId, 'view-outside-room')) - ) { - return API.v1.forbidden(); - } - const canViewFullOtherUserInfo = await hasPermissionAsync(this.userId, 'view-full-other-user-info'); - - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort, fields, query } = await this.parseJsonQuery(); - - const nonEmptyFields = getNonEmptyFields(fields); - - const inclusiveFields = getInclusiveFields(nonEmptyFields); - - const inclusiveFieldsKeys = Object.keys(inclusiveFields); - - const nonEmptyQuery = getNonEmptyQuery(query, canViewFullOtherUserInfo); - - if ('email' in this.queryParams && this.queryParams.email) { - if (!canViewFullOtherUserInfo) { - return API.v1.forbidden(); - } - const escapedEmail = escapeRegExp(this.queryParams.email); - nonEmptyQuery['emails.address'] = { - $regex: `^${escapedEmail}$`, - $options: 'i', - }; - } - - // if user provided a query, validate it with their allowed operators - // otherwise we use the default query (with $regex and $options) - if ( - !isValidQuery( - nonEmptyQuery, - [ - ...inclusiveFieldsKeys, - inclusiveFieldsKeys.includes('emails') && 'emails.address.*', - inclusiveFieldsKeys.includes('username') && 'username.*', - inclusiveFieldsKeys.includes('name') && 'name.*', - inclusiveFieldsKeys.includes('type') && 'type.*', - inclusiveFieldsKeys.includes('customFields') && 'customFields.*', - ].filter(Boolean) as string[], - // At this point, we have already validated the user query not containing malicious fields - // On here we are using our own query so we can allow some extra fields - [...this.queryOperations, '$regex', '$options'], - ) - ) { - throw new Meteor.Error('error-invalid-query', isValidQuery.errors.join('\n')); - } - - const actualSort = sort || { username: 1 }; - - if (sort?.status) { - actualSort.active = sort.status; - } - - if (sort?.name) { - actualSort.nameInsensitive = sort.name; - } - - const limit = - count !== 0 - ? [ - { - $limit: count, - }, - ] - : []; - - const result = await Users.col - .aggregate<{ sortedResults: IUser[]; totalCount: { total: number }[] }>([ - { - $match: nonEmptyQuery, - }, - { - $project: inclusiveFields, - }, - { - $addFields: { - nameInsensitive: { - $toLower: '$name', - }, - }, - }, - { - $facet: { - sortedResults: [ - { - $sort: actualSort, - }, - { - $skip: offset, - }, - ...limit, - ], - totalCount: [{ $group: { _id: null, total: { $sum: 1 } } }], - }, - }, - ]) - .toArray(); - - const { - sortedResults: users, - totalCount: [{ total } = { total: 0 }], - } = result[0]; - - return API.v1.success({ - users, - count: users.length, - offset, - total, - }); - }, - }, -); - -API.v1.get( - 'users.listByStatus', - { - authRequired: true, - query: isUsersListStatusProps, - permissionsRequired: ['view-d-room'], - response: { - 200: ajv.compile<{ users: IUser[]; count: number; offset: number; total: number }>({ - type: 'object', - properties: { - // user shape varies by projection and permissions - users: { type: 'array' }, - count: { type: 'number' }, - offset: { type: 'number' }, - total: { type: 'number' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['users', 'count', 'offset', 'total', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - if ( - settings.get('API_Apply_permission_view-outside-room_on_users-list') && - !(await hasPermissionAsync(this.userId, 'view-outside-room')) - ) { - return API.v1.forbidden(); - } - - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - const { status, hasLoggedIn, type, roles, searchTerm, inactiveReason } = this.queryParams; - - return API.v1.success( - await findPaginatedUsersByStatus({ - uid: this.userId, - offset, - count, - sort, - status, - roles, - searchTerm, - hasLoggedIn, - type, - inactiveReason, - }), - ); - }, -); - -API.v1.post( - 'users.sendWelcomeEmail', - { - authRequired: true, - body: isUsersSendWelcomeEmailProps, - permissionsRequired: ['send-mail'], - response: { - 200: ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [true] }, - }, - required: ['success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { email } = this.bodyParams; - - if (!isSMTPConfigured()) { - throw new MeteorError('error-email-send-failed', 'SMTP is not configured', { - method: 'sendWelcomeEmail', - }); - } - - const user = await Users.findOneByEmailAddress(email.trim(), { projection: { name: 1 } }); - - if (!user) { - throw new MeteorError('error-invalid-user', 'Invalid user', { - method: 'sendWelcomeEmail', - }); - } - - await sendWelcomeEmail({ ...user, email }); - - return API.v1.success(); - }, -); - -API.v1.post( - 'users.register', - { - authRequired: false, - rateLimiterOptions: { - numRequestsAllowed: settings.get('Rate_Limiter_Limit_RegisterUser') ?? 1, - intervalTimeInMS: settings.get('API_Enable_Rate_Limiter_Limit_Time_Default') ?? 60000, - }, - body: isUserRegisterParamsPOST, - response: { - 200: userObjectResponse, - 400: validateBadRequestErrorResponse, - }, - }, - async function action() { - const { secret: secretURL, ...params } = this.bodyParams; - - if (this.userId) { - return API.v1.failure('Logged in users can not register again.'); - } - - if (params.name && !validateNameChars(params.name)) { - return API.v1.failure('Name contains invalid characters'); - } - - if (!validateUsername(this.bodyParams.username)) { - return API.v1.failure(`The username provided is not valid`); - } - - if (!(await checkUsernameAvailability(this.bodyParams.username))) { - return API.v1.failure('Username is already in use'); - } - if (!(await checkEmailAvailability(this.bodyParams.email))) { - return API.v1.failure('Email already exists'); - } - if (this.bodyParams.customFields) { - try { - validateCustomFields(this.bodyParams.customFields); - } catch (e) { - return API.v1.failure(e); - } - } - - // Register the user - const userId = await registerUser({ - ...params, - ...(secretURL && { secretURL }), - }); - - if (typeof userId !== 'string') { - return API.v1.failure('Error creating user'); - } - - // Now set their username - const { fields } = await this.parseJsonQuery(); - await setUsernameWithValidation(userId, this.bodyParams.username); - - const user = await Users.findOneById(userId, { projection: fields }); - if (!user) { - return API.v1.failure('User not found'); - } - - if (this.bodyParams.customFields) { - await saveCustomFields(userId, this.bodyParams.customFields); - } - - return API.v1.success({ user }); - }, -); - -API.v1.post( - 'users.resetAvatar', - { - authRequired: true, - body: ajv.compile<{ userId?: string; username?: string; user?: string }>({ - type: 'object', - properties: { - userId: { type: 'string' }, - username: { type: 'string' }, - user: { type: 'string' }, - }, - additionalProperties: false, - }), - response: { - 200: voidSuccessResponse, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const user = await getUserFromParams(this.bodyParams); - - if (settings.get('Accounts_AllowUserAvatarChange') && user._id === this.userId) { - await resetAvatar(this.userId, this.userId); - } else if ( - (await hasPermissionAsync(this.userId, 'edit-other-user-avatar')) || - (await hasPermissionAsync(this.userId, 'manage-moderation-actions')) - ) { - await resetAvatar(this.userId, user._id); - } else { - throw new Meteor.Error('error-not-allowed', 'Reset avatar is not allowed', { - method: 'users.resetAvatar', - }); - } - - return API.v1.success(); - }, -); - -const usersEndpoints = API.v1 - .post( - 'users.createToken', - { - authRequired: true, - body: ajv.compile<{ userId: string; secret: string }>({ - type: 'object', - properties: { - userId: { - type: 'string', - minLength: 1, - }, - secret: { - type: 'string', - minLength: 1, - }, - }, - required: ['userId', 'secret'], - additionalProperties: false, - }), - response: { - 200: ajv.compile<{ data: { userId: string; authToken: string } }>({ - type: 'object', - properties: { - data: { - type: 'object', - properties: { - userId: { - type: 'string', - minLength: 1, - }, - authToken: { - type: 'string', - minLength: 1, - }, - }, - required: ['userId', 'authToken'], - additionalProperties: false, - }, - success: { - type: 'boolean', - enum: [true], - }, - }, - required: ['data', 'success'], - additionalProperties: false, - }), - 400: ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [false] }, - error: { type: 'string' }, - errorType: { type: 'string' }, - }, - required: ['success'], - additionalProperties: false, - }), - }, - }, - async function action() { - const user = await getUserFromParams(this.bodyParams); - - const data = await generateAccessToken(user._id, this.bodyParams.secret); - - return API.v1.success({ data }); - }, - ) - .get( - 'users.getAvatarSuggestion', - { - authRequired: true, - response: { - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 200: ajv.compile<{ - suggestions: Record< - string, - { - blob: string; - contentType: string; - service: string; - url: string; - } - >; - }>({ - type: 'object', - properties: { - success: { - type: 'boolean', - enum: [true], - }, - suggestions: { - type: 'object', - additionalProperties: { - type: 'object', - properties: { - blob: { - type: 'string', - }, - contentType: { - type: 'string', - }, - service: { - type: 'string', - }, - url: { - type: 'string', - format: 'uri', - }, - }, - required: ['blob', 'contentType', 'service', 'url'], - additionalProperties: false, - }, - }, - }, - required: ['success', 'suggestions'], - additionalProperties: false, - }), - }, - }, - async function action() { - const suggestions = await getAvatarSuggestionForUser(this.user); - - return API.v1.success({ suggestions }); - }, - ); - -API.v1.get( - 'users.getPreferences', - { - authRequired: true, - response: { - 200: ajv.compile<{ preferences: Record }>({ - type: 'object', - properties: { - // preferences is a dynamic key-value object that varies per user - preferences: { type: 'object' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['preferences', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const user = await Users.findOneById(this.userId); - if (user?.settings) { - const { preferences = {} } = user?.settings; - preferences.language = user?.language; - - return API.v1.success({ - preferences, - }); - } - return API.v1.failure(i18n.t('Accounts_Default_User_Preferences_not_available').toUpperCase()); - }, -); - -API.v1 - .post( - 'users.forgotPassword', - { - authRequired: false, - body: ajv.compile<{ email: string }>({ - type: 'object', - properties: { - email: { type: 'string' }, - }, - required: ['email'], - additionalProperties: false, - }), - response: { - 200: ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [true] }, - }, - required: ['success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - }, - }, - async function action() { - const isPasswordResetEnabled = settings.get('Accounts_PasswordReset'); - - if (!isPasswordResetEnabled) { - return API.v1.failure('Password reset is not enabled'); - } - - await sendForgotPasswordEmail(this.bodyParams.email.toLowerCase()); - return API.v1.success(); - }, - ) - .get( - 'users.getUsernameSuggestion', - { - authRequired: true, - userWithoutUsername: true, - response: { - 200: ajv.compile<{ result: string }>({ - type: 'object', - properties: { - result: { type: 'string' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['result', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const result = await generateUsernameSuggestion(this.user); - - if (!result) { - return API.v1.failure('No username suggestion found'); - } - - return API.v1.success({ result }); - }, - ); - -const tokenNameBodySchema = ajv.compile<{ tokenName: string; bypassTwoFactor?: boolean }>({ - type: 'object', - properties: { - tokenName: { type: 'string' }, - bypassTwoFactor: { type: 'boolean', nullable: true }, - }, - required: ['tokenName'], - additionalProperties: false, -}); - -const tokenResponseSchema = ajv.compile<{ token: string }>({ - type: 'object', - properties: { - token: { type: 'string' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['token', 'success'], - additionalProperties: false, -}); - -API.v1 - .get( - 'users.checkUsernameAvailability', - { - authRequired: true, - query: isUsersCheckUsernameAvailabilityParamsGET, - response: { - 200: ajv.compile<{ result: boolean }>({ - type: 'object', - properties: { - result: { type: 'boolean' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['result', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { username } = this.queryParams; - - const result = await checkUsernameAvailabilityWithValidation(this.userId, username); - - return API.v1.success({ result }); - }, - ) - .post( - 'users.generatePersonalAccessToken', - { - authRequired: true, - twoFactorRequired: true, - body: tokenNameBodySchema, - response: { - 200: tokenResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { tokenName, bypassTwoFactor = false } = this.bodyParams; - const token = await generatePersonalAccessTokenOfUser({ tokenName, userId: this.userId, bypassTwoFactor }); - - return API.v1.success({ token }); - }, - ) - .post( - 'users.regeneratePersonalAccessToken', - { - authRequired: true, - twoFactorRequired: true, - body: ajv.compile<{ tokenName: string }>({ - type: 'object', - properties: { - tokenName: { type: 'string' }, - }, - required: ['tokenName'], - additionalProperties: false, - }), - response: { - 200: tokenResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { tokenName } = this.bodyParams; - const token = await regeneratePersonalAccessTokenOfUser(tokenName, this.userId); - - return API.v1.success({ token }); - }, - ) - .get( - 'users.getPersonalAccessTokens', - { - authRequired: true, - permissionsRequired: ['create-personal-access-tokens'], - response: { - 200: ajv.compile<{ tokens: { name: string; createdAt: string; lastTokenPart: string; bypassTwoFactor: boolean }[] }>({ - type: 'object', - properties: { - tokens: { - type: 'array', - items: { - type: 'object', - properties: { - name: { type: 'string' }, - createdAt: { type: 'string' }, - lastTokenPart: { type: 'string' }, - bypassTwoFactor: { type: 'boolean' }, - }, - required: ['name', 'createdAt', 'lastTokenPart', 'bypassTwoFactor'], - additionalProperties: false, - }, - }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['tokens', 'success'], - additionalProperties: false, - }), - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const user = (await Users.getLoginTokensByUserId(this.userId).toArray())[0] as unknown as IUser | undefined; - - const isPersonalAccessToken = (loginToken: ILoginToken | IPersonalAccessToken): loginToken is IPersonalAccessToken => - 'type' in loginToken && loginToken.type === 'personalAccessToken'; - - return API.v1.success({ - tokens: - user?.services?.resume?.loginTokens?.filter(isPersonalAccessToken).map((loginToken) => ({ - name: loginToken.name, - createdAt: loginToken.createdAt.toISOString(), - lastTokenPart: loginToken.lastTokenPart, - bypassTwoFactor: Boolean(loginToken.bypassTwoFactor), - })) || [], - }); - }, - ) - .post( - 'users.removePersonalAccessToken', - { - authRequired: true, - twoFactorRequired: true, - body: ajv.compile<{ tokenName: string }>({ - type: 'object', - properties: { - tokenName: { type: 'string' }, - }, - required: ['tokenName'], - additionalProperties: false, - }), - response: { - 200: voidSuccessResponse, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - await removePersonalAccessTokenOfUser(this.bodyParams.tokenName, this.userId); - - return API.v1.success(); - }, - ); - -API.v1 - .post( - 'users.2fa.enableEmail', - { - authRequired: true, - response: { - 200: voidSuccessResponse, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const hasUnverifiedEmail = this.user.emails?.some((email) => !email.verified); - if (hasUnverifiedEmail) { - throw new MeteorError('error-invalid-user', 'You need to verify your emails before setting up 2FA'); - } - - await Users.enableEmail2FAByUserId(this.userId); - - // When 2FA is enable we logout all other clients - const xAuthToken = this.request.headers.get('x-auth-token') as string; - if (!xAuthToken) { - return API.v1.success(); - } - - const hashedToken = Accounts._hashLoginToken(xAuthToken); - - if (!(await Users.removeNonPATLoginTokensExcept(this.userId, hashedToken))) { - throw new MeteorError('error-logging-out-other-clients', 'Error logging out other clients'); - } - - // TODO this can be optmized so places that care about loginTokens being removed are invoked directly - // instead of having to listen to every watch.users event - void notifyOnUserChangeAsync(async () => { - const user = await Users.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1, 'services.email2fa': 1 } }); - if (!user) { - return; - } - - return { - clientAction: 'updated', - id: this.user._id, - diff: { 'services.resume.loginTokens': user.services?.resume?.loginTokens, 'services.email2fa': user.services?.email2fa }, - }; - }); - - return API.v1.success(); - }, - ) - .post( - 'users.2fa.disableEmail', - { - authRequired: true, - twoFactorRequired: true, - twoFactorOptions: { disableRememberMe: true }, - response: { - 200: voidSuccessResponse, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - await Users.disableEmail2FAByUserId(this.userId); - - void notifyOnUserChangeAsync(async () => { - const user = await Users.findOneById(this.userId, { projection: { 'services.email2fa': 1 } }); - if (!user) { - return; - } - - return { - clientAction: 'updated', - id: this.user._id, - diff: { 'services.email2fa': user.services?.email2fa }, - }; - }); - - return API.v1.success(); - }, - ) - .post( - 'users.2fa.sendEmailCode', - { - body: ajv.compile<{ emailOrUsername: string }>({ - type: 'object', - properties: { - emailOrUsername: { type: 'string' }, - }, - required: ['emailOrUsername'], - additionalProperties: false, - }), - response: { - 200: voidSuccessResponse, - 400: validateBadRequestErrorResponse, - }, - }, - async function action() { - const { emailOrUsername } = this.bodyParams; - - const method = emailOrUsername.includes('@') ? 'findOneByEmailAddress' : 'findOneByUsername'; - const userId = this.userId || (await Users[method](emailOrUsername, { projection: { _id: 1 } }))?._id; - - if (!userId) { - // this.logger.error('[2fa] User was not found when requesting 2fa email code'); - return API.v1.success(); - } - const user = await getUserForCheck(userId); - if (!user) { - // this.logger.error('[2fa] User was not found when requesting 2fa email code'); - return API.v1.success(); - } - - await emailCheck.sendEmailCode(user); - - return API.v1.success(); - }, - ); - -API.v1.post( - 'users.sendConfirmationEmail', - { - authRequired: false, - body: isUsersSendConfirmationEmailParamsPOST, - rateLimiterOptions: { - numRequestsAllowed: 1, - intervalTimeInMS: 60000, - }, - response: { - 200: voidSuccessResponse, - 400: validateBadRequestErrorResponse, - }, - }, - async function action() { - void sendConfirmationEmail(this.bodyParams.email).catch((err) => SystemLogger.error({ msg: 'sendConfirmationEmail failed', err })); - return API.v1.success(); - }, -); - -API.v1.get( - 'users.presence', - { - authRequired: true, - query: isUsersPresenceParamsGET, - response: { - 200: ajv.compile<{ users: object[]; full: boolean }>({ - type: 'object', - properties: { - // user shape varies by projection and permissions - users: { type: 'array' }, - full: { type: 'boolean' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['users', 'full', 'success'], - additionalProperties: false, - }), - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - // if presence broadcast is disabled, return an empty array (all users are "offline") - if (settings.get('Presence_broadcast_disabled')) { - return API.v1.success({ - users: [], - full: true, - }); - } - - const { from, ids } = this.queryParams; - - const options = { - projection: { - username: 1, - name: 1, - status: 1, - utcOffset: 1, - statusText: 1, - statusSource: 1, - statusExpiresAt: 1, - avatarETag: 1, - }, - }; - - if (ids) { - return API.v1.success({ - users: await Users.findPresenceUsersByIds(Array.isArray(ids) ? ids : ids.split(','), options).toArray(), - full: false, - }); - } - - if (from) { - const ts = new Date(from); - const diff = (Date.now() - Number(ts)) / 1000 / 60; - - if (diff < 10) { - return API.v1.success({ - users: await Users.findNotIdUpdatedFrom(this.userId, ts, options).toArray(), - full: false, - }); - } - } - - return API.v1.success({ - users: await Users.findUsersNotOffline(options).toArray(), - full: true, - }); - }, -); - -API.v1 - .get( - 'users.requestDataDownload', - { - authRequired: true, - query: isUsersRequestDataDownloadParamsGET, - response: { - 200: ajv.compile<{ - requested: boolean; - exportOperation: IExportOperation; - url: string | null; - pendingOperationsBeforeMyRequest: number; - }>({ - type: 'object', - properties: { - requested: { type: 'boolean' }, - // IExportOperation has complex/dynamic shape not yet in typia - exportOperation: { type: 'object' }, - url: { type: 'string', nullable: true }, - pendingOperationsBeforeMyRequest: { type: 'number' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['requested', 'exportOperation', 'pendingOperationsBeforeMyRequest', 'success'], - additionalProperties: false, - }), - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { fullExport = false } = this.queryParams; - const result = await requestDataDownload({ userData: this.user, fullExport: fullExport === 'true' }); - - return API.v1.success({ - requested: Boolean(result.requested), - exportOperation: result.exportOperation, - url: result.url, - pendingOperationsBeforeMyRequest: result.pendingOperationsBeforeMyRequest, - }); - }, - ) - .post( - 'users.logoutOtherClients', - { - authRequired: true, - response: { - 200: ajv.compile<{ token: string; tokenExpires: string }>({ - type: 'object', - properties: { - token: { type: 'string' }, - tokenExpires: { type: 'string' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['token', 'tokenExpires', 'success'], - additionalProperties: false, - }), - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const xAuthToken = this.request.headers.get('x-auth-token') as string; - - if (!xAuthToken) { - throw new Meteor.Error('error-parameter-required', 'x-auth-token is required'); - } - const hashedToken = Accounts._hashLoginToken(xAuthToken); - - if (!(await Users.removeNonPATLoginTokensExcept(this.userId, hashedToken))) { - throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); - } - - const me = (await Users.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1 } })) as Pick; - - void notifyOnUserChange({ - clientAction: 'updated', - id: this.userId, - diff: { 'services.resume.loginTokens': me.services?.resume?.loginTokens }, - }); - - const token = me.services?.resume?.loginTokens?.find((token) => token.hashedToken === hashedToken); - - const loginExp = settings.get('Accounts_LoginExpiration'); - - const tokenExpires = (token && 'when' in token && new Date(token.when.getTime() + getLoginExpirationInMs(loginExp))) || undefined; - - return API.v1.success({ - token: xAuthToken, - tokenExpires: tokenExpires?.toISOString() || '', - }); - }, - ); - -API.v1.get( - 'users.autocomplete', - { - authRequired: true, - query: isUsersAutocompleteProps, - response: { - 200: ajv.compile<{ items: object[] }>({ - type: 'object', - properties: { - // autocomplete items shape varies by permissions - items: { type: 'array' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['items', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { selector: selectorRaw } = this.queryParams; - - const selector: { exceptions: Required['username'][]; conditions: Filter; term: string } = JSON.parse(selectorRaw); - - try { - if (selector?.conditions) { - const canViewFullInfo = await hasPermissionAsync(this.userId, 'view-full-other-user-info'); - const allowedFields = canViewFullInfo ? [...Object.keys(defaultFields), ...Object.keys(fullFields)] : Object.keys(defaultFields); - - if (!isValidQuery(selector.conditions, allowedFields, ['$and', '$ne', '$exists'])) { - throw new Error('error-invalid-query'); - } - } - } catch (e) { - return API.v1.failure(e); - } - - return API.v1.success( - await findUsersToAutocomplete({ - uid: this.userId, - selector, - }), - ); - }, -); - -API.v1 - .post( - 'users.removeOtherTokens', - { - authRequired: true, - response: { - 200: voidSuccessResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - await Users.removeNonLoginTokensExcept(this.userId, this.token); - return API.v1.success(); - }, - ) - .post( - 'users.resetE2EKey', - { - authRequired: true, - twoFactorRequired: true, - twoFactorOptions: { disableRememberMe: true }, - body: ajv.compile<{ userId?: string; username?: string; user?: string }>({ - type: 'object', - properties: { - userId: { type: 'string' }, - username: { type: 'string' }, - user: { type: 'string' }, - }, - additionalProperties: false, - }), - response: { - 200: voidSuccessResponse, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - if ('userId' in this.bodyParams || 'username' in this.bodyParams || 'user' in this.bodyParams) { - const user = await getUserFromParams(this.bodyParams); - if (!user) { - throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); - } - - if (!(await hasPermissionAsync(this.userId, 'edit-other-user-e2ee'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } - - if (!(await resetUserE2EEncriptionKey(user._id, true))) { - return API.v1.failure(); - } - - return API.v1.success(); - } - await resetUserE2EEncriptionKey(this.userId, false); - return API.v1.success(); - }, - ) - .post( - 'users.resetTOTP', - { - authRequired: true, - twoFactorRequired: true, - twoFactorOptions: { disableRememberMe: true }, - body: ajv.compile<{ userId?: string; username?: string; user?: string }>({ - type: 'object', - properties: { - userId: { type: 'string' }, - username: { type: 'string' }, - user: { type: 'string' }, - }, - additionalProperties: false, - }), - response: { - 200: voidSuccessResponse, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - if ('userId' in this.bodyParams || 'username' in this.bodyParams || 'user' in this.bodyParams) { - if (!(await hasPermissionAsync(this.userId, 'edit-other-user-totp'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } - - if (!settings.get('Accounts_TwoFactorAuthentication_Enabled')) { - throw new Meteor.Error('error-two-factor-not-enabled', 'Two factor authentication is not enabled'); - } - - const user = await getUserFromParams(this.bodyParams); - if (!user) { - throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); - } - - await resetTOTP(user._id, true); - - return API.v1.success(); - } - await resetTOTP(this.userId, false); - return API.v1.success(); - }, - ); - -API.v1 - .get( - 'users.listTeams', - { - authRequired: true, - query: isUsersListTeamsProps, - response: { - 200: ajv.compile<{ teams: unknown[] }>({ - type: 'object', - properties: { - teams: { type: 'array' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['teams', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - check( - this.queryParams, - Match.ObjectIncluding({ - userId: Match.Maybe(String), - }), - ); - - const { userId } = this.queryParams; - - // If the caller has permission to view all teams, there's no need to filter the teams - const adminId = (await hasPermissionAsync(this.userId, 'view-all-teams')) ? undefined : this.userId; - - const teams = await Team.findBySubscribedUserIds(userId, adminId); - - return API.v1.success({ - teams, - }); - }, - ) - .post( - 'users.logout', - { - authRequired: true, - body: isUserLogoutParamsPOST, - response: { - 200: ajv.compile<{ message: string }>({ - type: 'object', - properties: { - message: { type: 'string' }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['message', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - const userId = this.bodyParams.userId || this.userId; - - if (userId !== this.userId && !(await hasPermissionAsync(this.userId, 'logout-other-user'))) { - return API.v1.forbidden(); - } - - // this method logs the user out automatically, if successful returns 1, otherwise 0 - if (!(await Users.unsetLoginTokens(userId))) { - throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); - } - - await Sessions.logoutAllByUserId(userId, this.userId); - - void notifyOnUserChange({ clientAction: 'updated', id: userId, diff: { 'services.resume.loginTokens': [] } }); - - return API.v1.success({ - message: `User ${userId} has been logged out!`, - }); - }, - ); - -const statusType = { type: 'string', enum: ['online', 'offline', 'away', 'busy'] } as const; - -API.v1 - .get( - 'users.getPresence', - { - authRequired: true, - query: isUsersGetPresenceParamsGET, - response: { - 200: ajv.compile<{ presence: UserStatus; connectionStatus?: string; lastLogin?: Date }>({ - type: 'object', - properties: { - presence: statusType, - connectionStatus: { type: 'string', nullable: true }, - lastLogin: { type: 'string', nullable: true }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['presence', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - if (isUserFromParams(this.queryParams, this.userId, this.user)) { - const user = await Users.findOneById(this.userId); - return API.v1.success({ - presence: (user?.status || 'offline') as UserStatus, - connectionStatus: user?.statusConnection || 'offline', - ...(user?.lastLogin && { lastLogin: user?.lastLogin }), - }); - } - - const user = await getUserFromParams(this.queryParams); - - return API.v1.success({ - presence: user.status || ('offline' as UserStatus), - }); - }, - ) - .post( - 'users.setStatus', - { - authRequired: true, - rateLimiterOptions: { - numRequestsAllowed: 5, - intervalTimeInMS: 60000, - }, - body: ajv.compile<{ - status?: Exclude; - message?: string; - expiresAt?: string; - userId?: string; - username?: string; - user?: string; - }>({ - type: 'object', - properties: { - status: { type: 'string', enum: ['online', 'away', 'offline', 'busy'] }, - message: { type: 'string', nullable: true }, - expiresAt: { type: 'string', nullable: true }, - userId: { type: 'string' }, - username: { type: 'string' }, - user: { type: 'string' }, - }, - additionalProperties: false, - }), - response: { - 200: voidSuccessResponse, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - }, - async function action() { - check( - this.bodyParams, - Match.OneOf( - Match.ObjectIncluding({ - status: Match.Maybe(String), - message: String, - }), - Match.ObjectIncluding({ - status: String, - message: Match.Maybe(String), - }), - ), - ); - - const user = await (async () => { - if (isUserFromParams(this.bodyParams, this.userId, this.user)) { - return Users.findOneById(this.userId); - } - if (await hasPermissionAsync(this.userId, 'edit-other-user-info')) { - return getUserFromParams(this.bodyParams); - } - })(); - - if (!user) { - return API.v1.forbidden(); - } - - const { status, message, expiresAt } = this.bodyParams; - - if (message && !settings.get('Accounts_AllowUserStatusMessageChange')) { - throw new Meteor.Error('error-not-allowed', 'Change status is not allowed', { - method: 'users.setStatus', - }); - } - - const statusExpiresAt = expiresAt ? new Date(expiresAt) : undefined; - if (statusExpiresAt && Number.isNaN(statusExpiresAt.getTime())) { - throw new Meteor.Error('error-invalid-date', 'Invalid expiresAt date string', { - method: 'users.setStatus', - }); - } - - if (statusExpiresAt && statusExpiresAt.getTime() <= Date.now()) { - throw new Meteor.Error('error-invalid-date', 'expiresAt must be a future date', { - method: 'users.setStatus', - }); - } - - // If status is missing (message-only update), keep the user's chosen status (statusDefault), - // not the computed status โ€” otherwise a transient auto-away/offline gets pinned as a manual claim. - const effectiveStatus = status || user.statusDefault || ('online' as UserStatus); - - if (effectiveStatus === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) { - throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { - method: 'users.setStatus', - }); - } - - await Presence.setStatus(user._id, effectiveStatus, message, statusExpiresAt); - - return API.v1.success(); - }, - ) - .get( - 'users.getStatus', - { - authRequired: true, - query: isUsersGetStatusParamsGET, - response: { - 200: ajv.compile<{ _id: string; status: string; connectionStatus?: string; statusSource?: string; statusExpiresAt?: string }>({ - type: 'object', - properties: { - _id: { type: 'string' }, - status: statusType, - connectionStatus: { type: 'string', nullable: true }, - statusSource: { type: 'string', nullable: true }, - statusExpiresAt: { type: 'string', nullable: true }, - success: { type: 'boolean', enum: [true] }, - }, - required: ['_id', 'status', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - if (isUserFromParams(this.queryParams, this.userId, this.user)) { - return API.v1.success({ - _id: this.userId, - connectionStatus: (this.user.statusConnection || 'offline') as 'online' | 'offline' | 'away' | 'busy', - status: (this.user.status || 'offline') as 'online' | 'offline' | 'away' | 'busy', - ...(this.user.statusSource && { statusSource: this.user.statusSource }), - ...(this.user.statusExpiresAt && { statusExpiresAt: this.user.statusExpiresAt.toISOString() }), - }); - } - - const user = await getUserFromParams(this.queryParams); - - return API.v1.success({ - _id: user._id, - status: (user.status || 'offline') as 'online' | 'offline' | 'away' | 'busy', - ...(user.statusSource && { statusSource: user.statusSource }), - ...(user.statusExpiresAt && { statusExpiresAt: user.statusExpiresAt.toISOString() }), - }); - }, - ); - -settings.watch('Rate_Limiter_Limit_RegisterUser', (value) => { - const userRegisterRoute = '/api/v1/users.registerpost'; - - API.v1.updateRateLimiterDictionaryForRoute(userRegisterRoute, value); -}); - -type UsersEndpoints = ExtractRoutesFromAPI; - -declare module '@rocket.chat/rest-typings' { - // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface - interface Endpoints extends UsersEndpoints {} -} diff --git a/apps/meteor/app/api/server/v1/webdav.ts b/apps/meteor/app/api/server/v1/webdav.ts deleted file mode 100644 index 0a7fedf500bcd..0000000000000 --- a/apps/meteor/app/api/server/v1/webdav.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { api } from '@rocket.chat/core-services'; -import type { IWebdavAccount, IWebdavAccountIntegration } from '@rocket.chat/core-typings'; -import { WebdavAccounts } from '@rocket.chat/models'; -import { ajv, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse } from '@rocket.chat/rest-typings'; -import type { DeleteResult } from 'mongodb'; - -import type { ExtractRoutesFromAPI } from '../ApiClass'; -import { API } from '../api'; -import { findWebdavAccountsByUserId } from '../lib/webdav'; - -const webdavGetMyAccountsEndpoints = API.v1.get( - 'webdav.getMyAccounts', - { - authRequired: true, - response: { - 200: ajv.compile<{ - accounts: IWebdavAccountIntegration[]; - }>({ - type: 'object', - properties: { - accounts: { - type: 'array', - items: { - type: 'object', - properties: { - _id: { - type: 'string', - }, - serverURL: { - type: 'string', - }, - username: { - type: 'string', - }, - name: { - type: 'string', - }, - }, - required: ['_id', 'serverURL', 'username', 'name'], - additionalProperties: false, - }, - }, - success: { - type: 'boolean', - description: 'Indicates if the request was successful.', - }, - }, - required: ['success', 'accounts'], - additionalProperties: false, - }), - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - return API.v1.success({ - accounts: await findWebdavAccountsByUserId({ uid: this.userId }), - }); - }, -); - -type POSTRemoveWebdavAccount = { - accountId: IWebdavAccount['_id']; -}; - -const POSTRemoveWebdavAccountSchema = { - type: 'object', - properties: { - accountId: { - type: 'string', - }, - }, - required: ['accountId'], - additionalProperties: false, -}; - -const isPOSTRemoveWebdavAccount = ajv.compile(POSTRemoveWebdavAccountSchema); - -const webdavRemoveAccountEndpoints = API.v1.post( - 'webdav.removeWebdavAccount', - { - authRequired: true, - validateParams: isPOSTRemoveWebdavAccount, - body: isPOSTRemoveWebdavAccount, - response: { - 200: ajv.compile<{ - result: DeleteResult; - }>({ - type: 'object', - properties: { - result: { - type: 'object', - properties: { - acknowledged: { - type: 'boolean', - }, - deletedCount: { - type: 'integer', - }, - }, - required: ['acknowledged', 'deletedCount'], - additionalProperties: false, - }, - success: { - type: 'boolean', - description: 'Indicates if the request was successful.', - }, - }, - required: ['result', 'success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { accountId } = this.bodyParams; - - const removed = await WebdavAccounts.removeByUserAndId(accountId, this.userId); - if (removed) { - void api.broadcast('notify.webdav', this.userId, { - type: 'removed', - account: { _id: accountId }, - }); - } - - return API.v1.success({ result: removed }); - }, -); - -type WebdavGetMyAccountsEndpoints = ExtractRoutesFromAPI; - -type WebdavRemoveAccountEndpoints = ExtractRoutesFromAPI; - -export type WebdavEndpoints = WebdavGetMyAccountsEndpoints | WebdavRemoveAccountEndpoints; - -declare module '@rocket.chat/rest-typings' { - // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface - interface Endpoints extends WebdavGetMyAccountsEndpoints {} - // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface - interface Endpoints extends WebdavRemoveAccountEndpoints {} -} diff --git a/apps/meteor/app/apple/server/index.ts b/apps/meteor/app/apple/server/index.ts deleted file mode 100644 index 5643ad67a54e2..0000000000000 --- a/apps/meteor/app/apple/server/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './appleOauthRegisterService'; -import './loginHandler'; diff --git a/apps/meteor/app/apple/server/loginHandler.spec.ts b/apps/meteor/app/apple/server/loginHandler.spec.ts deleted file mode 100644 index ffa50a952d4cf..0000000000000 --- a/apps/meteor/app/apple/server/loginHandler.spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { Accounts } from 'meteor/accounts-base'; - -import { settings } from '../../settings/server'; -import { handleIdentityToken } from '../lib/handleIdentityToken'; - -jest.mock( - 'meteor/accounts-base', - () => ({ - Accounts: { - registerLoginHandler: jest.fn(), - updateOrCreateUserFromExternalService: jest.fn(), - LoginCancelledError: { numericError: 400 }, - }, - }), - { virtual: true }, -); - -jest.mock( - 'meteor/meteor', - () => ({ - Meteor: { - Error: class extends Error { - constructor( - public error: number, - public reason: string, - ) { - super(reason); - } - }, - }, - }), - { virtual: true }, -); - -jest.mock('../../settings/server', () => ({ - settings: { - get: jest.fn(), - }, -})); - -jest.mock('../lib/handleIdentityToken', () => ({ - handleIdentityToken: jest.fn(), -})); - -describe('Apple OAuth loginHandler', () => { - let loginHandlerCallback: Parameters[1]; - - beforeAll(() => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - require('./loginHandler'); - loginHandlerCallback = jest.mocked(Accounts.registerLoginHandler).mock.calls[0][1]; - }); - - beforeEach(() => { - jest.clearAllMocks(); - jest.mocked(settings.get).mockImplementation((key) => { - if (key === 'Accounts_OAuth_Apple') return true; - if (key === 'Accounts_OAuth_Apple_id') return 'com.yourcompany.app'; - return null; - }); - }); - - it('should not use the client-provided email if Apple does not provide one', async () => { - jest.mocked(handleIdentityToken).mockResolvedValue({ - id: 'apple-sub-123', - }); - - const maliciousLoginRequest = { - identityToken: 'valid.token.without_email', - email: 'alice@email.tld', - fullName: { givenName: 'Alice', familyName: 'Sender' }, - }; - - jest.mocked(Accounts.updateOrCreateUserFromExternalService).mockResolvedValue({ userId: 'new-user-id' }); - - await loginHandlerCallback(maliciousLoginRequest); - - expect(Accounts.updateOrCreateUserFromExternalService).toHaveBeenCalledWith( - 'apple', - { id: 'apple-sub-123' }, - { profile: { name: 'Alice Sender' } }, - ); - }); - - it('should successfully pass the email if Apple natively provides it in the signed JWT', async () => { - jest.mocked(handleIdentityToken).mockResolvedValue({ - id: 'apple-sub-123', - email: 'legit@email.tld', - }); - - const legitLoginRequest = { - identityToken: 'valid.token.with_email', - fullName: { givenName: 'John', familyName: 'Doe' }, - }; - - jest.mocked(Accounts.updateOrCreateUserFromExternalService).mockResolvedValue({ userId: 'user-id' }); - - await loginHandlerCallback(legitLoginRequest); - - expect(Accounts.updateOrCreateUserFromExternalService).toHaveBeenCalledWith( - 'apple', - { id: 'apple-sub-123', email: 'legit@email.tld' }, - { profile: { name: 'John Doe' } }, - ); - }); - - it('should pass empty client id to token validation when setting is not configured', async () => { - jest.mocked(settings.get).mockImplementation((key) => { - if (key === 'Accounts_OAuth_Apple') return true; - if (key === 'Accounts_OAuth_Apple_id') return ''; - return null; - }); - - jest.mocked(handleIdentityToken).mockResolvedValue({ id: 'apple-sub-123' }); - jest.mocked(Accounts.updateOrCreateUserFromExternalService).mockResolvedValue({ userId: 'user-id' }); - - await loginHandlerCallback({ - identityToken: 'valid.token.with_mobile_default_audience', - fullName: { givenName: 'Mobile', familyName: 'User' }, - }); - - expect(handleIdentityToken).toHaveBeenCalledWith('valid.token.with_mobile_default_audience', ''); - }); -}); diff --git a/apps/meteor/app/apple/server/loginHandler.ts b/apps/meteor/app/apple/server/loginHandler.ts deleted file mode 100644 index 96ccd53323b32..0000000000000 --- a/apps/meteor/app/apple/server/loginHandler.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings/server'; -import { handleIdentityToken } from '../lib/handleIdentityToken'; - -Accounts.registerLoginHandler('apple', async (loginRequest) => { - if (!loginRequest.identityToken) { - return; - } - - if (!settings.get('Accounts_OAuth_Apple')) { - return; - } - - const { identityToken, fullName } = loginRequest; - - try { - const clientId = settings.get('Accounts_OAuth_Apple_id') || ''; - - const serviceData = await handleIdentityToken(identityToken, clientId); - - const profile: { name?: string } = {}; - - const { givenName, familyName } = fullName || {}; - if (givenName && familyName) { - profile.name = `${givenName} ${familyName}`; - } - - const result = await Accounts.updateOrCreateUserFromExternalService('apple', serviceData, { profile }); - - if (result?.userId === undefined) { - return { - type: 'apple', - error: new Meteor.Error(Accounts.LoginCancelledError.numericError, 'User creation failed from Apple response token'), - }; - } - - return result; - } catch (error: any) { - return { - type: 'apple', - error: new Meteor.Error(Accounts.LoginCancelledError.numericError, error.message), - }; - } -}); diff --git a/apps/meteor/app/apps/server/bridges/api.ts b/apps/meteor/app/apps/server/bridges/api.ts index 4e117855615ed..6323aa3c2ae19 100644 --- a/apps/meteor/app/apps/server/bridges/api.ts +++ b/apps/meteor/app/apps/server/bridges/api.ts @@ -9,7 +9,7 @@ import { Meteor } from 'meteor/meteor'; import { WebApp } from 'meteor/webapp'; import { apiServer } from './router'; -import { authenticationMiddleware } from '../../../api/server/middlewares/authentication'; +import { authenticationMiddleware } from '../../../../server/api/v1/middlewares/authentication'; WebApp.rawConnectHandlers.use(apiServer); diff --git a/apps/meteor/app/apps/server/bridges/cloud.ts b/apps/meteor/app/apps/server/bridges/cloud.ts index 83d9f98311c1b..e3f108e0c7e8c 100644 --- a/apps/meteor/app/apps/server/bridges/cloud.ts +++ b/apps/meteor/app/apps/server/bridges/cloud.ts @@ -2,7 +2,7 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import { CloudWorkspaceBridge } from '@rocket.chat/apps/dist/server/bridges/CloudWorkspaceBridge'; import type { IWorkspaceToken } from '@rocket.chat/apps-engine/definition/cloud/IWorkspaceToken'; -import { getWorkspaceAccessTokenWithScope } from '../../../cloud/server'; +import { getWorkspaceAccessTokenWithScope } from '../../../../server/lib/cloud'; export class AppCloudBridge extends CloudWorkspaceBridge { constructor(private readonly orch: IAppServerOrchestrator) { diff --git a/apps/meteor/app/apps/server/bridges/commands.ts b/apps/meteor/app/apps/server/bridges/commands.ts index 36eaaed5f047d..b53d7ebfd1195 100644 --- a/apps/meteor/app/apps/server/bridges/commands.ts +++ b/apps/meteor/app/apps/server/bridges/commands.ts @@ -6,7 +6,7 @@ import type { IMessage, RequiredField, SlashCommand, SlashCommandCallbackParams import { Utilities } from '../../../../ee/lib/misc/Utilities'; import { parseParameters } from '../../../../lib/utils/parseParameters'; -import { slashCommands } from '../../../utils/server/slashCommand'; +import { slashCommands } from '../../../../server/lib/utils/slashCommand'; export class AppCommandsBridge extends CommandBridge { disabledCommands: Map; diff --git a/apps/meteor/app/apps/server/bridges/contact.ts b/apps/meteor/app/apps/server/bridges/contact.ts index fa00a839c6a4e..b8705e83b9a37 100644 --- a/apps/meteor/app/apps/server/bridges/contact.ts +++ b/apps/meteor/app/apps/server/bridges/contact.ts @@ -2,8 +2,8 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import { ContactBridge } from '@rocket.chat/apps/dist/server/bridges/ContactBridge'; import type { ILivechatContact } from '@rocket.chat/apps-engine/definition/livechat'; -import { addContactEmail } from '../../../livechat/server/lib/contacts/addContactEmail'; -import { verifyContactChannel } from '../../../livechat/server/lib/contacts/verifyContactChannel'; +import { addContactEmail } from '../../../../server/lib/omnichannel/contacts/addContactEmail'; +import { verifyContactChannel } from '../../../../server/lib/omnichannel/contacts/verifyContactChannel'; export class AppContactBridge extends ContactBridge { constructor(private readonly orch: IAppServerOrchestrator) { diff --git a/apps/meteor/app/apps/server/bridges/email.ts b/apps/meteor/app/apps/server/bridges/email.ts index 8f984b7bb6ce2..8a5d7a97d55f9 100644 --- a/apps/meteor/app/apps/server/bridges/email.ts +++ b/apps/meteor/app/apps/server/bridges/email.ts @@ -2,8 +2,8 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import { EmailBridge } from '@rocket.chat/apps/dist/server/bridges/EmailBridge'; import type { IEmail } from '@rocket.chat/apps-engine/definition/email'; -import * as Mailer from '../../../mailer/server/api'; -import { settings } from '../../../settings/server'; +import * as Mailer from '../../../../server/lib/notifications/email/api'; +import { settings } from '../../../../server/settings'; export class AppEmailBridge extends EmailBridge { constructor(private readonly orch: IAppServerOrchestrator) { diff --git a/apps/meteor/app/apps/server/bridges/http.ts b/apps/meteor/app/apps/server/bridges/http.ts index f3bb6c502a426..4ecbcbbcb3ca3 100644 --- a/apps/meteor/app/apps/server/bridges/http.ts +++ b/apps/meteor/app/apps/server/bridges/http.ts @@ -5,7 +5,7 @@ import type { IHttpResponse } from '@rocket.chat/apps-engine/definition/accessor import { serverFetch as fetch, type ExtendedFetchOptions } from '@rocket.chat/server-fetch'; import { censorUrl } from '@rocket.chat/tools'; -import { settings } from '../../../settings/server'; +import { settings } from '../../../../server/settings'; const isGetOrHead = (method: string): boolean => ['GET', 'HEAD'].includes(method.toUpperCase()); diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 81a6d8551f82e..2a397e502b9ad 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -18,16 +18,16 @@ import { registerGuest } from '@rocket.chat/omni-core'; import { deasyncPromise } from '../../../../server/deasync/deasync'; import { callbacks } from '../../../../server/lib/callbacks'; -import { closeRoom } from '../../../livechat/server/lib/closeRoom'; -import { setCustomFields } from '../../../livechat/server/lib/custom-fields'; -import { getRoomMessages } from '../../../livechat/server/lib/getRoomMessages'; -import type { ILivechatMessage } from '../../../livechat/server/lib/localTypes'; -import { updateMessage, sendMessage } from '../../../livechat/server/lib/messages'; -import { resolveVisitor } from '../../../livechat/server/lib/resolveVisitor'; -import { createRoom } from '../../../livechat/server/lib/rooms'; -import { online } from '../../../livechat/server/lib/service-status'; -import { transfer } from '../../../livechat/server/lib/transfer'; -import { settings } from '../../../settings/server'; +import { closeRoom } from '../../../../server/lib/omnichannel/closeRoom'; +import { setCustomFields } from '../../../../server/lib/omnichannel/custom-fields'; +import { getRoomMessages } from '../../../../server/lib/omnichannel/getRoomMessages'; +import type { ILivechatMessage } from '../../../../server/lib/omnichannel/localTypes'; +import { updateMessage, sendMessage } from '../../../../server/lib/omnichannel/messages'; +import { resolveVisitor } from '../../../../server/lib/omnichannel/resolveVisitor'; +import { createRoom } from '../../../../server/lib/omnichannel/rooms'; +import { online } from '../../../../server/lib/omnichannel/service-status'; +import { transfer } from '../../../../server/lib/omnichannel/transfer'; +import { settings } from '../../../../server/settings'; declare module '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator' { interface IExtraRoomParams { diff --git a/apps/meteor/app/apps/server/bridges/messages.ts b/apps/meteor/app/apps/server/bridges/messages.ts index b5be15ae7ca92..61d23f1542b6c 100644 --- a/apps/meteor/app/apps/server/bridges/messages.ts +++ b/apps/meteor/app/apps/server/bridges/messages.ts @@ -7,11 +7,11 @@ import { api } from '@rocket.chat/core-services'; import type { IMessage } from '@rocket.chat/core-typings'; import { Users, Subscriptions } from '@rocket.chat/models'; -import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; -import { updateMessage } from '../../../lib/server/functions/updateMessage'; -import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; -import notifications from '../../../notifications/server/lib/Notifications'; -import { executeSetReaction } from '../../../reactions/server/setReaction'; +import { deleteMessage } from '../../../../server/lib/messages/deleteMessage'; +import { updateMessage } from '../../../../server/lib/messages/updateMessage'; +import { executeSetReaction } from '../../../../server/lib/messaging/reactions/setReaction'; +import notifications from '../../../../server/lib/notifications/core/lib/Notifications'; +import { executeSendMessage } from '../../../../server/meteor-methods/messages/sendMessage'; export class AppMessageBridge extends MessageBridge { constructor(private readonly orch: IAppServerOrchestrator) { diff --git a/apps/meteor/app/apps/server/bridges/outboundCommunication.ts b/apps/meteor/app/apps/server/bridges/outboundCommunication.ts index 3ba7218a601c0..ba0f88dae0894 100644 --- a/apps/meteor/app/apps/server/bridges/outboundCommunication.ts +++ b/apps/meteor/app/apps/server/bridges/outboundCommunication.ts @@ -6,7 +6,7 @@ import type { IOutboundPhoneMessageProvider, } from '@rocket.chat/apps-engine/definition/outboundCommunication'; -import { getOutboundService } from '../../../livechat/server/lib/outboundcommunication'; +import { getOutboundService } from '../../../../server/lib/omnichannel/outboundcommunication'; export class OutboundCommunicationBridge extends OutboundMessageBridge { constructor(private readonly orch: IAppServerOrchestrator) { diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index 1a2963a005676..76c2dbcbe1737 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -9,13 +9,13 @@ import type { ISubscription, IUser as ICoreUser, IRoom as ICoreRoom, IMessage as import { Subscriptions, Users, Rooms, Messages } from '@rocket.chat/models'; import type { FindOptions, Sort } from 'mongodb'; -import { createDirectMessage } from '../../../../server/methods/createDirectMessage'; -import { createDiscussion } from '../../../discussion/server/methods/createDiscussion'; -import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom'; -import { deleteRoom } from '../../../lib/server/functions/deleteRoom'; -import { removeUserFromRoom } from '../../../lib/server/functions/removeUserFromRoom'; -import { createChannelMethod } from '../../../lib/server/methods/createChannel'; -import { createPrivateGroupMethod } from '../../../lib/server/methods/createPrivateGroup'; +import { addUserToRoom } from '../../../../server/lib/rooms/addUserToRoom'; +import { deleteRoom } from '../../../../server/lib/rooms/deleteRoom'; +import { removeUserFromRoom } from '../../../../server/lib/rooms/removeUserFromRoom'; +import { createDirectMessage } from '../../../../server/meteor-methods/messages/createDirectMessage'; +import { createDiscussion } from '../../../../server/meteor-methods/messages/createDiscussion'; +import { createChannelMethod } from '../../../../server/meteor-methods/rooms/createChannel'; +import { createPrivateGroupMethod } from '../../../../server/meteor-methods/rooms/createPrivateGroup'; const rawRoomProjection: FindOptions['projection'] = { _id: 1, diff --git a/apps/meteor/app/apps/server/bridges/settings.ts b/apps/meteor/app/apps/server/bridges/settings.ts index 145f81fc129ae..1900a8093bce0 100644 --- a/apps/meteor/app/apps/server/bridges/settings.ts +++ b/apps/meteor/app/apps/server/bridges/settings.ts @@ -4,8 +4,8 @@ import type { IReadSettingPermission } from '@rocket.chat/apps-engine/definition import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import { Settings } from '@rocket.chat/models'; +import { notifyOnSettingChanged, notifyOnSettingChangedById } from '../../../../server/lib/notifyListener'; import { updateAuditedByApp } from '../../../../server/settings/lib/auditedSettingUpdates'; -import { notifyOnSettingChanged, notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; export class AppSettingBridge extends ServerSettingBridge { constructor(private readonly orch: IAppServerOrchestrator) { diff --git a/apps/meteor/app/apps/server/bridges/uploads.ts b/apps/meteor/app/apps/server/bridges/uploads.ts index 929e70743c557..0637e29e74a36 100644 --- a/apps/meteor/app/apps/server/bridges/uploads.ts +++ b/apps/meteor/app/apps/server/bridges/uploads.ts @@ -4,9 +4,9 @@ import type { IUpload } from '@rocket.chat/apps-engine/definition/uploads'; import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails'; import { determineFileType } from '../../../../ee/lib/misc/determineFileType'; -import { FileUpload } from '../../../file-upload/server'; -import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; -import { sendFileLivechatMessage } from '../../../livechat/server/methods/sendFileLivechatMessage'; +import { FileUpload } from '../../../../server/lib/media/file-upload'; +import { sendFileMessage } from '../../../../server/meteor-methods/messages/sendFileMessage'; +import { sendFileLivechatMessage } from '../../../../server/meteor-methods/omnichannel/sendFileLivechatMessage'; const getUploadDetails = (details: IUploadDetails): Partial => { if (details.visitorToken) { diff --git a/apps/meteor/app/apps/server/bridges/users.ts b/apps/meteor/app/apps/server/bridges/users.ts index 4a49f6b613e9d..4c345c9a443f2 100644 --- a/apps/meteor/app/apps/server/bridges/users.ts +++ b/apps/meteor/app/apps/server/bridges/users.ts @@ -6,13 +6,13 @@ import type { PresenceSource, UserStatus } from '@rocket.chat/core-typings'; import { Subscriptions, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; -import { checkUsernameAvailability } from '../../../lib/server/functions/checkUsernameAvailability'; -import { deleteUser } from '../../../lib/server/functions/deleteUser'; -import { getUserCreatedByApp } from '../../../lib/server/functions/getUserCreatedByApp'; -import { setStatusText } from '../../../lib/server/functions/setStatusText'; -import { setUserActiveStatus } from '../../../lib/server/functions/setUserActiveStatus'; -import { setUserAvatar } from '../../../lib/server/functions/setUserAvatar'; -import { notifyOnUserChange, notifyOnUserChangeById } from '../../../lib/server/lib/notifyListener'; +import { notifyOnUserChange, notifyOnUserChangeById } from '../../../../server/lib/notifyListener'; +import { checkUsernameAvailability } from '../../../../server/lib/users/checkUsernameAvailability'; +import { deleteUser } from '../../../../server/lib/users/deleteUser'; +import { getUserCreatedByApp } from '../../../../server/lib/users/getUserCreatedByApp'; +import { setStatusText } from '../../../../server/lib/users/setStatusText'; +import { setUserActiveStatus } from '../../../../server/lib/users/setUserActiveStatus'; +import { setUserAvatar } from '../../../../server/lib/users/setUserAvatar'; export class AppUserBridge extends UserBridge { constructor(private readonly orch: IAppServerOrchestrator) { diff --git a/apps/meteor/app/apps/server/converters/codecs/enums.ts b/apps/meteor/app/apps/server/converters/codecs/enums.ts new file mode 100644 index 0000000000000..a4f6efe659578 --- /dev/null +++ b/apps/meteor/app/apps/server/converters/codecs/enums.ts @@ -0,0 +1,124 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { SettingType } from '@rocket.chat/apps-engine/definition/settings'; +import { UserStatusConnection, UserType } from '@rocket.chat/apps-engine/definition/users'; +import * as z from 'zod'; + +/** + * Bidirectional enum mappings between Rocket.Chat's stored string values and the + * Apps-Engine enums, expressed as Zod codecs (`decode`: Rocket.Chat -> Apps-Engine, + * `encode`: Apps-Engine -> Rocket.Chat). + * + * These are behaviour-preserving replacements for the private `_convert*ToApp`/`_convert*ToEnum` + * helpers currently living inside the converter classes. The `decode` transforms reproduce the + * exact fallback semantics of those helpers (including passing unknown values through, upper-cased + * where the legacy code did so). The `encode` transforms mirror the reverse converters, which today + * assign the Apps-Engine value straight back onto the Rocket.Chat document without translation. + * + * Note: the contextual `console.warn` calls that depend on data unavailable to a pure enum mapping + * (e.g. the affected user's id/username in the status-connection warning) are intentionally left in + * the converter layer and will be wired back in as each converter is migrated. + */ + +/** + * Rocket.Chat `IUser['type']` <-> Apps-Engine `UserType`. + * Mirrors `AppUsersConverter._convertUserTypeToEnum`. + */ +export const UserTypeCodec = z.codec(z.string().optional(), z.string(), { + decode: (type): string => { + switch (type) { + case 'user': + return UserType.USER; + case 'bot': + return UserType.BOT; + case 'app': + return UserType.APP; + case '': + case undefined: + return UserType.UNKNOWN; + default: + // eslint-disable-next-line no-console + console.warn(`A new user type has been added that the Apps don't know about? "${type}"`); + return type.toUpperCase(); + } + }, + // The reverse converter assigns `user.type` straight back onto the document. + encode: (type): string => type, +}); + +/** + * Rocket.Chat `IUser['statusConnection']` <-> Apps-Engine `UserStatusConnection`. + * Mirrors the value mapping of `AppUsersConverter._convertStatusConnectionToEnum`. The legacy + * `console.warn` (which includes the affected user's id/username) stays in the converter, since + * that context is not available to a standalone codec. + */ +export const UserStatusConnectionCodec = z.codec(z.string().optional(), z.string(), { + decode: (status): string => { + switch (status) { + case 'offline': + return UserStatusConnection.OFFLINE; + case 'online': + return UserStatusConnection.ONLINE; + case 'away': + return UserStatusConnection.AWAY; + case 'busy': + return UserStatusConnection.BUSY; + case undefined: + // This is needed for Livechat guests and Rocket.Cat user. + return UserStatusConnection.UNDEFINED; + default: + return !status ? UserStatusConnection.OFFLINE : status.toUpperCase(); + } + }, + encode: (status): string => status, +}); + +/** + * Rocket.Chat `IRoom['t']` <-> Apps-Engine `RoomType`. + * Mirrors `AppRoomsConverter._convertTypeToApp`. Unknown type characters pass through unchanged. + */ +export const RoomTypeCodec = z.codec(z.string(), z.string(), { + decode: (typeChar): string => { + switch (typeChar) { + case 'c': + return RoomType.CHANNEL; + case 'p': + return RoomType.PRIVATE_GROUP; + case 'd': + return RoomType.DIRECT_MESSAGE; + case 'l': + return RoomType.LIVE_CHAT; + default: + return typeChar; + } + }, + // `RoomType` values are identical to the stored `t` characters, so the reverse is an identity. + encode: (type): string => type, +}); + +/** + * Rocket.Chat `ISetting['type']` <-> Apps-Engine `SettingType`. + * Mirrors `AppSettingsConverter._convertTypeToApp`. Unknown types pass through unchanged. + */ +export const SettingTypeCodec = z.codec(z.string(), z.string(), { + decode: (type): string => { + switch (type) { + case 'boolean': + return SettingType.BOOLEAN; + case 'code': + return SettingType.CODE; + case 'color': + return SettingType.COLOR; + case 'font': + return SettingType.FONT; + case 'int': + return SettingType.NUMBER; + case 'select': + return SettingType.SELECT; + case 'string': + return SettingType.STRING; + default: + return type; + } + }, + encode: (type): string => type, +}); diff --git a/apps/meteor/app/apps/server/converters/codecs/index.ts b/apps/meteor/app/apps/server/converters/codecs/index.ts new file mode 100644 index 0000000000000..323810a6991e5 --- /dev/null +++ b/apps/meteor/app/apps/server/converters/codecs/index.ts @@ -0,0 +1 @@ +export { RoomTypeCodec, SettingTypeCodec, UserStatusConnectionCodec, UserTypeCodec } from './enums'; diff --git a/apps/meteor/app/apps/server/converters/departments.js b/apps/meteor/app/apps/server/converters/departments.js deleted file mode 100644 index 3fbfd07e9e99b..0000000000000 --- a/apps/meteor/app/apps/server/converters/departments.js +++ /dev/null @@ -1,67 +0,0 @@ -import { LivechatDepartment } from '@rocket.chat/models'; - -import { transformMappedData } from './transformMappedData'; - -export class AppDepartmentsConverter { - constructor(orch) { - this.orch = orch; - } - - async convertById(id) { - const department = await LivechatDepartment.findOneById(id); - - return this.convertDepartment(department); - } - - async convertDepartment(department) { - if (!department) { - return undefined; - } - - const map = { - id: '_id', - name: 'name', - email: 'email', - updatedAt: '_updatedAt', - enabled: 'enabled', - numberOfAgents: 'numAgents', - showOnOfflineForm: 'showOnOfflineForm', - description: 'description', - offlineMessageChannelName: 'offlineMessageChannelName', - requestTagBeforeClosingChat: 'requestTagBeforeClosingChat', - chatClosingTags: 'chatClosingTags', - abandonedRoomsCloseCustomMessage: 'abandonedRoomsCloseCustomMessage', - waitingQueueMessage: 'waitingQueueMessage', - departmentsAllowedToForward: 'departmentsAllowedToForward', - showOnRegistration: 'showOnRegistration', - }; - - return transformMappedData(department, map); - } - - convertAppDepartment(department) { - if (!department) { - return undefined; - } - - const newDepartment = { - _id: department.id, - name: department.name, - email: department.email, - _updatedAt: department.updatedAt, - enabled: department.enabled, - numAgents: department.numberOfAgents, - showOnOfflineForm: department.showOnOfflineForm, - showOnRegistration: department.showOnRegistration, - description: department.description, - offlineMessageChannelName: department.offlineMessageChannelName, - requestTagBeforeClosingChat: department.requestTagBeforeClosingChat, - chatClosingTags: department.chatClosingTags, - abandonedRoomsCloseCustomMessage: department.abandonedRoomsCloseCustomMessage, - waitingQueueMessage: department.waitingQueueMessage, - departmentsAllowedToForward: department.departmentsAllowedToForward, - }; - - return Object.assign(newDepartment, department._unmappedProperties_); - } -} diff --git a/apps/meteor/app/apps/server/converters/departments.ts b/apps/meteor/app/apps/server/converters/departments.ts new file mode 100644 index 0000000000000..4501d927452d2 --- /dev/null +++ b/apps/meteor/app/apps/server/converters/departments.ts @@ -0,0 +1,84 @@ +import type { IAppDepartmentsConverter, IAppServerOrchestrator, IAppsDepartment } from '@rocket.chat/apps'; +import type { ILivechatDepartment } from '@rocket.chat/core-typings'; +import { LivechatDepartment } from '@rocket.chat/models'; + +import { transformMappedData } from './transformMappedData'; + +export class AppDepartmentsConverter implements IAppDepartmentsConverter { + constructor(protected readonly orch: IAppServerOrchestrator) { + this.orch = orch; + } + + async convertById(id: ILivechatDepartment['_id']): Promise { + const department = await LivechatDepartment.findOneById(id); + + return this.convertDepartment(department); + } + + async convertDepartment(department: undefined | null): Promise; + + async convertDepartment(department: ILivechatDepartment): Promise; + + async convertDepartment(department: ILivechatDepartment | undefined | null): Promise; + + async convertDepartment(department: ILivechatDepartment | undefined | null): Promise { + if (!department) { + return undefined; + } + + const map = { + id: '_id', + name: 'name', + email: 'email', + updatedAt: '_updatedAt', + enabled: 'enabled', + numberOfAgents: 'numAgents', + showOnOfflineForm: 'showOnOfflineForm', + description: 'description', + offlineMessageChannelName: 'offlineMessageChannelName', + requestTagBeforeClosingChat: 'requestTagBeforeClosingChat', + chatClosingTags: 'chatClosingTags', + abandonedRoomsCloseCustomMessage: 'abandonedRoomsCloseCustomMessage', + waitingQueueMessage: 'waitingQueueMessage', + departmentsAllowedToForward: 'departmentsAllowedToForward', + showOnRegistration: 'showOnRegistration', + } as const; + + return transformMappedData(department, map) as unknown as Promise; + } + + convertAppDepartment(department: undefined | null): undefined; + + convertAppDepartment(department: IAppsDepartment): ILivechatDepartment; + + convertAppDepartment(department: IAppsDepartment | undefined | null): ILivechatDepartment | undefined; + + convertAppDepartment(department: IAppsDepartment | undefined | null): ILivechatDepartment | undefined { + if (!department) { + return undefined; + } + + const newDepartment = { + _id: department.id, + name: department.name, + email: department.email, + _updatedAt: department.updatedAt, + enabled: department.enabled, + numAgents: department.numberOfAgents, + showOnOfflineForm: department.showOnOfflineForm, + showOnRegistration: department.showOnRegistration, + description: department.description, + offlineMessageChannelName: department.offlineMessageChannelName, + requestTagBeforeClosingChat: department.requestTagBeforeClosingChat, + chatClosingTags: department.chatClosingTags, + abandonedRoomsCloseCustomMessage: department.abandonedRoomsCloseCustomMessage, + waitingQueueMessage: department.waitingQueueMessage, + departmentsAllowedToForward: department.departmentsAllowedToForward, + }; + + return Object.assign( + newDepartment, + (department as { _unmappedProperties_?: Record })._unmappedProperties_, + ) as unknown as ILivechatDepartment; + } +} diff --git a/apps/meteor/app/apps/server/converters/messages.js b/apps/meteor/app/apps/server/converters/messages.js deleted file mode 100644 index 332ab585d32e7..0000000000000 --- a/apps/meteor/app/apps/server/converters/messages.js +++ /dev/null @@ -1,341 +0,0 @@ -import { isMessageFromVisitor } from '@rocket.chat/core-typings'; -import { Messages, Rooms, Users } from '@rocket.chat/models'; -import { Random } from '@rocket.chat/random'; -import { removeEmpty } from '@rocket.chat/tools'; - -import { cachedFunction } from './cachedFunction'; -import { convertMessageFiles } from './convertMessageFiles'; -import { transformMappedData } from './transformMappedData'; - -export class AppMessagesConverter { - mem = new WeakMap(); - - constructor(orch) { - this.orch = orch; - } - - async convertById(msgId) { - const msg = await Messages.findOneById(msgId); - - return this.convertMessage(msg); - } - - async convertMessageRaw(msgObj) { - if (!msgObj) { - return undefined; - } - - const { attachments, ...message } = msgObj; - const getAttachments = async () => this._convertAttachmentsToApp(attachments, msgObj.file); - - const map = { - id: '_id', - threadId: 'tmid', - reactions: 'reactions', - parseUrls: 'parseUrls', - text: 'msg', - createdAt: 'ts', - updatedAt: '_updatedAt', - editedAt: 'editedAt', - emoji: 'emoji', - avatarUrl: 'avatar', - alias: 'alias', - file: 'file', - files: 'files', - customFields: 'customFields', - groupable: 'groupable', - token: 'token', - blocks: 'blocks', - roomId: 'rid', - editor: 'editedBy', - attachments: getAttachments, - sender: 'u', - threadMsgCount: 'tcount', - type: 't', - }; - - return transformMappedData(message, map); - } - - async convertMessage(msgObj, cacheObj = msgObj) { - if (!msgObj) { - return undefined; - } - - const cache = - this.mem.get(cacheObj) ?? - new Map([ - ['room', cachedFunction(this.orch.getConverters().get('rooms').convertById.bind(this.orch.getConverters().get('rooms')))], - [ - 'user.convertById', - cachedFunction(this.orch.getConverters().get('users').convertById.bind(this.orch.getConverters().get('users'))), - ], - [ - 'user.convertToApp', - cachedFunction(this.orch.getConverters().get('users').convertToApp.bind(this.orch.getConverters().get('users'))), - ], - ]); - - this.mem.set(cacheObj, cache); - - const { attachments, file: mainFile } = msgObj; - - const map = { - id: '_id', - threadId: 'tmid', - reactions: 'reactions', - parseUrls: 'parseUrls', - text: 'msg', - createdAt: 'ts', - updatedAt: '_updatedAt', - editedAt: 'editedAt', - emoji: 'emoji', - avatarUrl: 'avatar', - alias: 'alias', - file: 'file', - customFields: 'customFields', - groupable: 'groupable', - token: 'token', - blocks: 'blocks', - type: 't', - files: async (message) => convertMessageFiles(message.files, attachments), - room: async (message) => { - const result = await cache.get('room')(message.rid); - delete message.rid; - return result; - }, - editor: async (message) => { - const { editedBy } = message; - delete message.editedBy; - - if (!editedBy) { - return undefined; - } - - return cache.get('user.convertById')(editedBy._id); - }, - attachments: async (message) => { - const result = await this._convertAttachmentsToApp(message.attachments, mainFile); - delete message.attachments; - return result; - }, - sender: async (message) => { - if (!message.u || !message.u._id) { - return undefined; - } - - // When the message contains token, means the message is from the visitor(omnichannel) - const user = await (isMessageFromVisitor(msgObj) - ? cache.get('user.convertToApp')(message.u) - : cache.get('user.convertById')(message.u._id)); - - delete message.u; - - /** - * Old System Messages from visitor doesn't have the `token` field, to not return - * `sender` as undefined, so we need to add this fallback here. - */ - - return user || cache.get('user.convertToApp')(message.u); - }, - }; - - return transformMappedData(msgObj, map); - } - - async convertAppMessage(message, isPartial = false) { - if (!message) { - return undefined; - } - - let rid; - if (message.room?.id) { - const room = await Rooms.findOneById(message.room.id, { projection: { _id: 1 } }); - rid = room?._id; - } - - if (!rid && !isPartial) { - throw new Error('Invalid room provided on the message.'); - } - - let u; - if (message.sender?.id) { - const user = await Users.findOneById(message.sender.id); - - if (user) { - u = { - _id: user._id, - username: user.username, - name: user.name, - }; - } else { - u = { - _id: message.sender.id, - username: message.sender.username, - name: message.sender.name, - }; - } - } - - let editedBy; - if (message.editor) { - const editor = await Users.findOneById(message.editor.id); - editedBy = { - _id: editor._id, - username: editor.username, - }; - } - - const attachments = this._convertAppAttachments(message.attachments); - - let _id = message.id; - let ts = message.createdAt; - - if (!isPartial) { - if (!message.id) { - _id = Random.id(); - } - - if (!message.createdAt) { - ts = new Date(); - } - } - - const newMessage = { - _id, - ...('threadId' in message && { tmid: message.threadId }), - rid, - u, - msg: message.text, - ts, - _updatedAt: message.updatedAt, - ...(editedBy && { editedBy }), - ...('editedAt' in message && { editedAt: message.editedAt }), - ...('emoji' in message && { emoji: message.emoji }), - ...('avatarUrl' in message && { avatar: message.avatarUrl }), - ...('alias' in message && { alias: message.alias }), - ...('customFields' in message && { customFields: message.customFields }), - ...('groupable' in message && { groupable: message.groupable }), - ...(attachments && { attachments }), - ...('reactions' in message && { reactions: message.reactions }), - ...('parseUrls' in message && { parseUrls: message.parseUrls }), - ...('blocks' in message && { blocks: message.blocks }), - ...('token' in message && { token: message.token }), - }; - - if (isPartial) { - Object.entries(newMessage).forEach(([key, value]) => { - if (typeof value === 'undefined') { - delete newMessage[key]; - } - }); - } else { - Object.assign(newMessage, message._unmappedProperties_); - } - - return newMessage; - } - - _convertAppAttachments(attachments) { - if (typeof attachments === 'undefined' || !Array.isArray(attachments)) { - return undefined; - } - - return attachments.map((attachment) => - removeEmpty({ - collapsed: attachment.collapsed, - color: attachment.color, - text: attachment.text, - ts: attachment.timestamp ? attachment.timestamp.toJSON() : attachment.timestamp, - message_link: attachment.timestampLink, - thumb_url: attachment.thumbnailUrl, - author_name: attachment.author ? attachment.author.name : undefined, - author_link: attachment.author ? attachment.author.link : undefined, - author_icon: attachment.author ? attachment.author.icon : undefined, - title: attachment.title ? attachment.title.value : undefined, - title_link: attachment.title ? attachment.title.link : undefined, - title_link_download: attachment.title ? attachment.title.displayDownloadLink : undefined, - image_dimensions: attachment.imageDimensions, - image_preview: attachment.imagePreview, - image_url: attachment.imageUrl, - image_type: attachment.imageType, - image_size: attachment.imageSize, - audio_url: attachment.audioUrl, - audio_type: attachment.audioType, - audio_size: attachment.audioSize, - video_url: attachment.videoUrl, - video_type: attachment.videoType, - video_size: attachment.videoSize, - fields: attachment.fields, - button_alignment: attachment.actionButtonsAlignment, - actions: attachment.actions, - type: attachment.type, - description: attachment.description, - ...attachment._unmappedProperties_, - }), - ); - } - - async _convertAttachmentsToApp(attachments, mainFile) { - if (typeof attachments === 'undefined' || !Array.isArray(attachments)) { - return undefined; - } - - const map = { - collapsed: 'collapsed', - color: 'color', - text: 'text', - timestampLink: 'message_link', - thumbnailUrl: 'thumb_url', - imageDimensions: 'image_dimensions', - imagePreview: 'image_preview', - imageUrl: 'image_url', - imageType: 'image_type', - imageSize: 'image_size', - audioUrl: 'audio_url', - audioType: 'audio_type', - audioSize: 'audio_size', - videoUrl: 'video_url', - videoType: 'video_type', - videoSize: 'video_size', - fields: 'fields', - actionButtonsAlignment: 'button_alignment', - actions: 'actions', - type: 'type', - description: 'description', - author: (attachment) => { - const { author_name: name, author_link: link, author_icon: icon } = attachment; - - delete attachment.author_name; - delete attachment.author_link; - delete attachment.author_icon; - - return { name, link, icon }; - }, - title: (attachment) => { - const { title: value, title_link: link, title_link_download: displayDownloadLink } = attachment; - - delete attachment.title; - delete attachment.title_link; - delete attachment.title_link_download; - - return { value, link, displayDownloadLink }; - }, - timestamp: (attachment) => { - const result = new Date(attachment.ts); - delete attachment.ts; - return result; - }, - fileId: (attachment) => { - // If the attachment is missing the fileId, but there's only one file in the message, use that file's ID - if (!attachment.fileId && attachment.type === 'file' && mainFile?._id && attachments.length === 1) { - return mainFile._id; - } - - return attachment.fileId; - }, - }; - - return Promise.all(attachments.map(async (attachment) => transformMappedData(attachment, map))); - } -} diff --git a/apps/meteor/app/apps/server/converters/messages.ts b/apps/meteor/app/apps/server/converters/messages.ts new file mode 100644 index 0000000000000..0e5fa981e5ee4 --- /dev/null +++ b/apps/meteor/app/apps/server/converters/messages.ts @@ -0,0 +1,378 @@ +import type { IAppMessagesConverter, IAppServerOrchestrator, IAppsMessage, IAppsMesssageRaw } from '@rocket.chat/apps'; +import { isMessageFromVisitor } from '@rocket.chat/core-typings'; +import type { IMessage } from '@rocket.chat/core-typings'; +import { Messages, Rooms, Users } from '@rocket.chat/models'; +import { Random } from '@rocket.chat/random'; +import { removeEmpty } from '@rocket.chat/tools'; + +import { cachedFunction } from './cachedFunction'; +import { convertMessageFiles } from './convertMessageFiles'; +import { transformMappedData } from './transformMappedData'; + +// The stored message documents and the app-side payloads carry many optional/dynamic fields that are +// awkward to express against the base `IMessage`/`IAppsMessage` types. The legacy converter accessed +// them dynamically; until this converter is migrated to a codec we treat the transform inputs as +// loosely-typed records to preserve that behaviour verbatim. +type MessageData = Record; + +export class AppMessagesConverter implements IAppMessagesConverter { + private mem = new WeakMap>(); + + constructor(protected readonly orch: IAppServerOrchestrator) { + this.orch = orch; + } + + async convertById(msgId: IMessage['_id']): Promise { + const msg = await Messages.findOneById(msgId); + + return this.convertMessage(msg); + } + + convertMessageRaw(msgObj: IMessage): Promise; + + convertMessageRaw(msgObj: IMessage | undefined | null): Promise; + + async convertMessageRaw(msgObj: IMessage | undefined | null): Promise { + if (!msgObj) { + return undefined; + } + + const { attachments, ...message } = msgObj; + const getAttachments = async () => this._convertAttachmentsToApp(attachments, (msgObj as MessageData).file); + + const map = { + id: '_id', + threadId: 'tmid', + reactions: 'reactions', + parseUrls: 'parseUrls', + text: 'msg', + createdAt: 'ts', + updatedAt: '_updatedAt', + editedAt: 'editedAt', + emoji: 'emoji', + avatarUrl: 'avatar', + alias: 'alias', + file: 'file', + files: 'files', + customFields: 'customFields', + groupable: 'groupable', + token: 'token', + blocks: 'blocks', + roomId: 'rid', + editor: 'editedBy', + attachments: getAttachments, + sender: 'u', + threadMsgCount: 'tcount', + type: 't', + } as const; + + return transformMappedData(message, map) as unknown as Promise; + } + + convertMessage(msgObj: undefined | null): Promise; + + convertMessage(msgObj: IMessage, cacheObj?: object): Promise; + + convertMessage(msgObj: IMessage | undefined | null, cacheObj?: object): Promise; + + async convertMessage( + msgObj: IMessage | undefined | null, + cacheObj: object | undefined = msgObj ?? undefined, + ): Promise { + if (!msgObj) { + return undefined; + } + + const cache = + this.mem.get(cacheObj as object) ?? + new Map([ + ['room', cachedFunction(this.orch.getConverters().get('rooms').convertById.bind(this.orch.getConverters().get('rooms')))], + [ + 'user.convertById', + cachedFunction(this.orch.getConverters().get('users').convertById.bind(this.orch.getConverters().get('users'))), + ], + [ + 'user.convertToApp', + cachedFunction(this.orch.getConverters().get('users').convertToApp.bind(this.orch.getConverters().get('users'))), + ], + ]); + + this.mem.set(cacheObj as object, cache); + + const { attachments, file: mainFile } = msgObj as MessageData; + + const map = { + id: '_id', + threadId: 'tmid', + reactions: 'reactions', + parseUrls: 'parseUrls', + text: 'msg', + createdAt: 'ts', + updatedAt: '_updatedAt', + editedAt: 'editedAt', + emoji: 'emoji', + avatarUrl: 'avatar', + alias: 'alias', + file: 'file', + customFields: 'customFields', + groupable: 'groupable', + token: 'token', + blocks: 'blocks', + type: 't', + files: async (message: MessageData) => convertMessageFiles(message.files, attachments), + room: async (message: MessageData) => { + const result = await cache.get('room')(message.rid); + delete message.rid; + return result; + }, + editor: async (message: MessageData) => { + const { editedBy } = message; + delete message.editedBy; + + if (!editedBy) { + return undefined; + } + + return cache.get('user.convertById')(editedBy._id); + }, + attachments: async (message: MessageData) => { + const result = await this._convertAttachmentsToApp(message.attachments, mainFile); + delete message.attachments; + return result; + }, + sender: async (message: MessageData) => { + if (!message.u?._id) { + return undefined; + } + + // Keep a reference to the original sender before it is deleted below, so the fallback still has it. + const sender = message.u; + + // When the message contains token, means the message is from the visitor(omnichannel) + const user = await (isMessageFromVisitor(msgObj) + ? cache.get('user.convertToApp')(sender) + : cache.get('user.convertById')(sender._id)); + + delete message.u; + + /** + * Old System Messages from visitor doesn't have the `token` field, to not return + * `sender` as undefined, so we need to add this fallback here. + */ + + return user || cache.get('user.convertToApp')(sender); + }, + } as const; + + return transformMappedData(msgObj, map) as unknown as Promise; + } + + convertAppMessage(message: undefined | null): Promise; + + convertAppMessage(message: IAppsMessage): Promise; + + convertAppMessage(message: IAppsMessage, isPartial: boolean): Promise>; + + async convertAppMessage(message: any, isPartial = false): Promise | undefined> { + if (!message) { + return undefined; + } + + let rid; + if (message.room?.id) { + const room = await Rooms.findOneById(message.room.id, { projection: { _id: 1 } }); + rid = room?._id; + } + + if (!rid && !isPartial) { + throw new Error('Invalid room provided on the message.'); + } + + let u; + if (message.sender?.id) { + const user = await Users.findOneById(message.sender.id); + + if (user) { + u = { + _id: user._id, + username: user.username, + name: user.name, + }; + } else { + u = { + _id: message.sender.id, + username: message.sender.username, + name: message.sender.name, + }; + } + } + + let editedBy; + if (message.editor) { + const editor = await Users.findOneById(message.editor.id); + // Fall back to the editor data carried on the app payload when the user no longer exists, + // mirroring the sender handling above instead of dereferencing a possibly-null lookup. + editedBy = editor + ? { + _id: editor._id, + username: editor.username, + } + : { + _id: message.editor.id, + username: message.editor.username, + }; + } + + const attachments = this._convertAppAttachments(message.attachments); + + let _id = message.id; + let ts = message.createdAt; + + if (!isPartial) { + if (!message.id) { + _id = Random.id(); + } + + if (!message.createdAt) { + ts = new Date(); + } + } + + const newMessage: MessageData = { + _id, + ...('threadId' in message && { tmid: message.threadId }), + rid, + u, + msg: message.text, + ts, + _updatedAt: message.updatedAt, + ...(editedBy && { editedBy }), + ...('editedAt' in message && { editedAt: message.editedAt }), + ...('emoji' in message && { emoji: message.emoji }), + ...('avatarUrl' in message && { avatar: message.avatarUrl }), + ...('alias' in message && { alias: message.alias }), + ...('customFields' in message && { customFields: message.customFields }), + ...('groupable' in message && { groupable: message.groupable }), + ...(attachments && { attachments }), + ...('reactions' in message && { reactions: message.reactions }), + ...('parseUrls' in message && { parseUrls: message.parseUrls }), + ...('blocks' in message && { blocks: message.blocks }), + ...('token' in message && { token: message.token }), + }; + + if (isPartial) { + Object.entries(newMessage).forEach(([key, value]) => { + if (typeof value === 'undefined') { + delete newMessage[key]; + } + }); + } else { + Object.assign(newMessage, message._unmappedProperties_); + } + + return newMessage as unknown as Partial; + } + + _convertAppAttachments(attachments: any) { + if (typeof attachments === 'undefined' || !Array.isArray(attachments)) { + return undefined; + } + + return attachments.map((attachment) => + removeEmpty({ + collapsed: attachment.collapsed, + color: attachment.color, + text: attachment.text, + ts: attachment.timestamp ? attachment.timestamp.toJSON() : attachment.timestamp, + message_link: attachment.timestampLink, + thumb_url: attachment.thumbnailUrl, + author_name: attachment.author ? attachment.author.name : undefined, + author_link: attachment.author ? attachment.author.link : undefined, + author_icon: attachment.author ? attachment.author.icon : undefined, + title: attachment.title ? attachment.title.value : undefined, + title_link: attachment.title ? attachment.title.link : undefined, + title_link_download: attachment.title ? attachment.title.displayDownloadLink : undefined, + image_dimensions: attachment.imageDimensions, + image_preview: attachment.imagePreview, + image_url: attachment.imageUrl, + image_type: attachment.imageType, + image_size: attachment.imageSize, + audio_url: attachment.audioUrl, + audio_type: attachment.audioType, + audio_size: attachment.audioSize, + video_url: attachment.videoUrl, + video_type: attachment.videoType, + video_size: attachment.videoSize, + fields: attachment.fields, + button_alignment: attachment.actionButtonsAlignment, + actions: attachment.actions, + type: attachment.type, + description: attachment.description, + ...attachment._unmappedProperties_, + }), + ); + } + + async _convertAttachmentsToApp(attachments: any, mainFile: any) { + if (typeof attachments === 'undefined' || !Array.isArray(attachments)) { + return undefined; + } + + const map = { + collapsed: 'collapsed', + color: 'color', + text: 'text', + timestampLink: 'message_link', + thumbnailUrl: 'thumb_url', + imageDimensions: 'image_dimensions', + imagePreview: 'image_preview', + imageUrl: 'image_url', + imageType: 'image_type', + imageSize: 'image_size', + audioUrl: 'audio_url', + audioType: 'audio_type', + audioSize: 'audio_size', + videoUrl: 'video_url', + videoType: 'video_type', + videoSize: 'video_size', + fields: 'fields', + actionButtonsAlignment: 'button_alignment', + actions: 'actions', + type: 'type', + description: 'description', + author: (attachment: MessageData) => { + const { author_name: name, author_link: link, author_icon: icon } = attachment; + + delete attachment.author_name; + delete attachment.author_link; + delete attachment.author_icon; + + return { name, link, icon }; + }, + title: (attachment: MessageData) => { + const { title: value, title_link: link, title_link_download: displayDownloadLink } = attachment; + + delete attachment.title; + delete attachment.title_link; + delete attachment.title_link_download; + + return { value, link, displayDownloadLink }; + }, + timestamp: (attachment: MessageData) => { + const result = new Date(attachment.ts); + delete attachment.ts; + return result; + }, + fileId: (attachment: MessageData) => { + // If the attachment is missing the fileId, but there's only one file in the message, use that file's ID + if (!attachment.fileId && attachment.type === 'file' && mainFile?._id && attachments.length === 1) { + return mainFile._id; + } + + return attachment.fileId; + }, + } as const; + + return Promise.all(attachments.map(async (attachment) => transformMappedData(attachment, map))); + } +} diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js deleted file mode 100644 index 38c4f25678e24..0000000000000 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ /dev/null @@ -1,480 +0,0 @@ -import { secureFieldsMapper } from '@rocket.chat/apps/dist/lib/SecureFields'; -import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; -import { LivechatVisitors, Rooms, LivechatDepartment, Users, LivechatContacts } from '@rocket.chat/models'; - -import { transformMappedData } from './transformMappedData'; - -export class AppRoomsConverter { - constructor(orch) { - this.orch = orch; - } - - async convertById(roomId) { - const room = await Rooms.findOneById(roomId); - - return this.convertRoom(room); - } - - async convertByName(roomName) { - const room = await Rooms.findOneByName(roomName); - - return this.convertRoom(room); - } - - convertRoomRaw(room) { - if (!room) { - return undefined; - } - - const mapUserLookup = (user) => - user && { - _id: user._id ?? user.id, - ...(user.username && { username: user.username }), - ...(user.name && { name: user.name }), - }; - - const map = { - id: '_id', - displayName: 'fname', - slugifiedName: 'name', - members: 'members', - userIds: 'uids', - usernames: 'usernames', - messageCount: 'msgs', - createdAt: 'ts', - updatedAt: '_updatedAt', - closedAt: 'closedAt', - lastModifiedAt: 'lm', - customFields: 'customFields', - livechatData: 'livechatData', - isWaitingResponse: 'waitingResponse', - isOpen: 'open', - description: 'description', - source: 'source', - closer: 'closer', - teamId: 'teamId', - isTeamMain: 'teamMain', - isDefault: 'default', - isReadOnly: 'ro', - contactId: 'contactId', - departmentId: 'departmentId', - parentRoomId: 'prid', - isFederated: 'federated', - federation: 'federation', - visitor: (data) => { - const { v } = data; - if (!v) { - return undefined; - } - - delete data.v; - - const { _id: id, ...rest } = v; - - return { - id, - ...rest, - }; - }, - displaySystemMessages: (data) => { - const { sysMes } = data; - delete data.sysMes; - return typeof sysMes === 'undefined' ? true : sysMes; - }, - type: (data) => { - const result = this._convertTypeToApp(data.t); - delete data.t; - return result; - }, - creator: (data) => { - if (!data.u) { - return undefined; - } - const creator = mapUserLookup(data.u); - delete data.u; - return creator; - }, - closedBy: (data) => { - if (!data.closedBy) { - return undefined; - } - const { closedBy } = data; - delete data.closedBy; - return mapUserLookup(closedBy); - }, - servedBy: (data) => { - if (!data.servedBy) { - return undefined; - } - const { servedBy } = data; - delete data.servedBy; - return mapUserLookup(servedBy); - }, - responseBy: (data) => { - if (!data.responseBy) { - return undefined; - } - const { responseBy } = data; - delete data.responseBy; - return mapUserLookup(responseBy); - }, - }; - - return transformMappedData(room, map); - } - - async __getCreator(user) { - if (!user) { - return; - } - - const creator = await Users.findOneById(user, { projection: { _id: 1, username: 1, name: 1 } }); - if (!creator) { - return; - } - - return { - _id: creator._id, - username: creator.username, - name: creator.name, - }; - } - - async __getVisitor({ visitor: roomVisitor, visitorChannelInfo }) { - if (!roomVisitor) { - return; - } - - const visitor = await LivechatVisitors.findOneEnabledById(roomVisitor.id); - if (!visitor) { - return; - } - - const { lastMessageTs, phone } = visitorChannelInfo; - - return { - _id: visitor._id, - username: visitor.username, - token: visitor.token, - status: visitor.status || 'online', - activity: visitor.activity, - ...(lastMessageTs && { lastMessageTs }), - ...(phone && { phone }), - }; - } - - async __getUserIdAndUsername(userObj) { - if (!userObj?.id) { - return; - } - - const user = await Users.findOneById(userObj.id, { projection: { _id: 1, username: 1 } }); - if (!user) { - return; - } - - return { - _id: user._id, - username: user.username, - }; - } - - async __getRoomCloser(room, v) { - if (!room.closedBy) { - return; - } - - if (room.closer === 'user') { - const user = await Users.findOneById(room.closedBy.id, { projection: { _id: 1, username: 1 } }); - if (!user) { - return; - } - - return { - _id: user._id, - username: user.username, - }; - } - - if (room.closer === 'visitor' && v) { - return { - _id: v._id, - username: v.username, - }; - } - } - - // TODO do we really need this? - async __getContactId({ contact }) { - if (!contact?._id) { - return; - } - const contactFromDb = await LivechatContacts.findOneEnabledById(contact._id, { projection: { _id: 1 } }); - return contactFromDb?._id; - } - - // TODO do we really need this? - async __getDepartment({ department }) { - if (!department) { - return; - } - const dept = await LivechatDepartment.findOneById(department.id, { projection: { _id: 1 } }); - return dept?._id; - } - - async convertAppRoom(room, isPartial = false) { - if (!room) { - return undefined; - } - - const u = await this.__getCreator(room.creator?.id); - - const v = await this.__getVisitor(room); - - const departmentId = await this.__getDepartment(room); - - const servedBy = await this.__getUserIdAndUsername(room.servedBy); - - const closedBy = await this.__getRoomCloser(room, v); - - const contactId = await this.__getContactId(room); - - const newRoom = { - ...(room.id && { _id: room.id }), - ...(typeof room.type !== 'undefined' && { t: room.type }), - ...(typeof room.createdAt !== 'undefined' && { ts: room.createdAt }), - ...(typeof room.messageCount !== 'undefined' && { msgs: room.messageCount || 0 }), - ...(typeof room.updatedAt !== 'undefined' && { _updatedAt: room.updatedAt }), - ...(room.displayName && { fname: room.displayName }), - ...(room.type !== 'd' && room.slugifiedName && { name: room.slugifiedName }), - ...(room.members && { members: room.members }), - ...(typeof room.isDefault !== 'undefined' && { default: room.isDefault }), - ...(typeof room.isReadOnly !== 'undefined' && { ro: room.isReadOnly }), - ...(typeof room.displaySystemMessages !== 'undefined' && { sysMes: room.displaySystemMessages }), - ...(u && { u }), - ...(v && { v }), - ...(departmentId && { departmentId }), - ...(servedBy && { servedBy }), - ...(closedBy && { closedBy }), - ...(room.userIds && { uids: room.userIds }), - ...(typeof room.isWaitingResponse !== 'undefined' && { waitingResponse: !!room.isWaitingResponse }), - ...(typeof room.isOpen !== 'undefined' && { open: !!room.isOpen }), - ...(room.closedAt && { closedAt: room.closedAt }), - ...(room.lastModifiedAt && { lm: room.lastModifiedAt }), - ...(room.customFields && { customFields: room.customFields }), - ...(room.livechatData && { livechatData: room.livechatData }), - ...(typeof room.isFederated !== 'undefined' && { federated: room.isFederated }), - ...(typeof room.federation !== 'undefined' && { federation: room.federation }), - ...(typeof room.parentRoom !== 'undefined' && { prid: room.parentRoom.id }), - ...(contactId && { contactId }), - ...(room._USERNAMES && { _USERNAMES: room._USERNAMES }), - ...(room.source && { - source: { - ...room.source, - }, - }), - }; - - if (!isPartial) { - Object.assign(newRoom, room._unmappedProperties_); - } - - return newRoom; - } - - async convertRoom(originalRoom) { - if (!originalRoom) { - return undefined; - } - - const map = { - id: '_id', - displayName: 'fname', - slugifiedName: 'name', - members: 'members', - userIds: 'uids', - messageCount: 'msgs', - createdAt: 'ts', - updatedAt: '_updatedAt', - closedAt: 'closedAt', - lastModifiedAt: 'lm', - customFields: 'customFields', - livechatData: 'livechatData', - isWaitingResponse: 'waitingResponse', - isOpen: 'open', - _USERNAMES: '_USERNAMES', - description: 'description', - source: 'source', - closer: 'closer', - teamId: 'teamId', - isTeamMain: 'teamMain', - isFederated: 'federated', - federation: 'federation', - isDefault: (room) => { - const result = !!room.default; - delete room.default; - return result; - }, - isReadOnly: (room) => { - const result = !!room.ro; - delete room.ro; - return result; - }, - displaySystemMessages: (room) => { - const { sysMes } = room; - - if (typeof sysMes === 'undefined') { - return true; - } - - delete room.sysMes; - return sysMes; - }, - type: (room) => { - const result = this._convertTypeToApp(room.t); - delete room.t; - return result; - }, - creator: async (room) => { - const { u } = room; - - if (!u) { - return undefined; - } - - delete room.u; - - return this.orch.getConverters().get('users').convertById(u._id); - }, - visitor: (room) => { - const { v } = room; - - if (!v) { - return undefined; - } - - return this.orch.getConverters().get('visitors').convertById(v._id); - }, - contact: (room) => { - const { contactId } = room; - - if (!contactId) { - return undefined; - } - - return this.orch.getConverters().get('contacts').convertById(contactId); - }, - // Note: room.v is not just visitor, it also contains channel related visitor data - // so we need to pass this data to the converter - // So suppose you have a contact whom we're contacting using SMS via 2 phone no's, - // let's call X and Y. Then if the contact sends a message using X phone number, - // then room.v.phoneNo would be X and correspondingly we'll store the timestamp of - // the last message from this visitor from X phone no on room.v.lastMessageTs - visitorChannelInfo: (room) => { - const { v } = room; - - if (!v) { - return undefined; - } - - const { lastMessageTs, phone } = v; - - return { - ...(phone && { phone }), - ...(lastMessageTs && { lastMessageTs }), - }; - }, - department: async (room) => { - const { departmentId } = room; - - if (!departmentId) { - return undefined; - } - - delete room.departmentId; - - return this.orch.getConverters().get('departments').convertById(departmentId); - }, - closedBy: async (room) => { - const { closedBy } = room; - - if (!closedBy) { - return undefined; - } - - delete room.closedBy; - if (originalRoom.closer === 'user') { - return this.orch.getConverters().get('users').convertById(closedBy._id); - } - - return this.orch.getConverters().get('visitors').convertById(closedBy._id); - }, - servedBy: async (room) => { - const { servedBy } = room; - - if (!servedBy) { - return undefined; - } - - delete room.servedBy; - - return this.orch.getConverters().get('users').convertById(servedBy._id); - }, - responseBy: async (room) => { - const { responseBy } = room; - - if (!responseBy) { - return undefined; - } - - delete room.responseBy; - - return this.orch.getConverters().get('users').convertById(responseBy._id); - }, - parentRoom: async (room) => { - const { prid } = room; - - if (!prid) { - return undefined; - } - - delete room.prid; - - return this.orch.getConverters().get('rooms').convertById(prid); - }, - ...secureFieldsMapper((room) => { - if (!room.abacAttributes) { - return undefined; - } - - const value = [ - { - permission: 'abac.read', - name: 'abacAttributes', - value: room.abacAttributes, - }, - ]; - - delete room.abacAttributes; - return value; - }), - }; - - return transformMappedData(originalRoom, map); - } - - _convertTypeToApp(typeChar) { - switch (typeChar) { - case 'c': - return RoomType.CHANNEL; - case 'p': - return RoomType.PRIVATE_GROUP; - case 'd': - return RoomType.DIRECT_MESSAGE; - case 'l': - return RoomType.LIVE_CHAT; - default: - return typeChar; - } - } -} diff --git a/apps/meteor/app/apps/server/converters/rooms.ts b/apps/meteor/app/apps/server/converters/rooms.ts new file mode 100644 index 0000000000000..76dd49d6f4441 --- /dev/null +++ b/apps/meteor/app/apps/server/converters/rooms.ts @@ -0,0 +1,505 @@ +import type { IAppRoomsConverter, IAppServerOrchestrator, IAppsRoom, IAppsLivechatRoom, IAppsRoomRaw } from '@rocket.chat/apps'; +import { secureFieldsMapper } from '@rocket.chat/apps/dist/lib/SecureFields'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IRoom } from '@rocket.chat/core-typings'; +import { LivechatVisitors, Rooms, LivechatDepartment, Users, LivechatContacts } from '@rocket.chat/models'; + +import { transformMappedData } from './transformMappedData'; + +// The stored room documents carry many optional/livechat-only fields that are not part of the base +// `IRoom` union. The legacy converter accessed them dynamically; until this converter is migrated to +// a codec we treat the transform inputs as loosely-typed records to preserve that behaviour verbatim. +type RoomData = Record; + +export class AppRoomsConverter implements IAppRoomsConverter { + constructor(protected readonly orch: IAppServerOrchestrator) { + this.orch = orch; + } + + async convertById(roomId: IRoom['_id']): Promise { + const room = await Rooms.findOneById(roomId); + + return this.convertRoom(room); + } + + async convertByName(roomName: IRoom['name']): Promise { + const room = await Rooms.findOneByName(roomName as string); + + return this.convertRoom(room); + } + + convertRoomRaw(room: IRoom): Promise; + + convertRoomRaw(room: IRoom | undefined | null): Promise; + + convertRoomRaw(room: IRoom | undefined | null): Promise { + if (!room) { + return Promise.resolve(undefined); + } + + const mapUserLookup = (user: any) => + user && { + _id: user._id ?? user.id, + ...(user.username && { username: user.username }), + ...(user.name && { name: user.name }), + }; + + const map = { + id: '_id', + displayName: 'fname', + slugifiedName: 'name', + members: 'members', + userIds: 'uids', + usernames: 'usernames', + messageCount: 'msgs', + createdAt: 'ts', + updatedAt: '_updatedAt', + closedAt: 'closedAt', + lastModifiedAt: 'lm', + customFields: 'customFields', + livechatData: 'livechatData', + isWaitingResponse: 'waitingResponse', + isOpen: 'open', + description: 'description', + source: 'source', + closer: 'closer', + teamId: 'teamId', + isTeamMain: 'teamMain', + isDefault: 'default', + isReadOnly: 'ro', + contactId: 'contactId', + departmentId: 'departmentId', + parentRoomId: 'prid', + isFederated: 'federated', + federation: 'federation', + visitor: (data: RoomData) => { + const { v } = data; + if (!v) { + return undefined; + } + + delete data.v; + + const { _id: id, ...rest } = v; + + return { + id, + ...rest, + }; + }, + displaySystemMessages: (data: RoomData) => { + const { sysMes } = data; + delete data.sysMes; + return typeof sysMes === 'undefined' ? true : sysMes; + }, + type: (data: RoomData) => { + const result = this._convertTypeToApp(data.t); + delete data.t; + return result; + }, + creator: (data: RoomData) => { + if (!data.u) { + return undefined; + } + const creator = mapUserLookup(data.u); + delete data.u; + return creator; + }, + closedBy: (data: RoomData) => { + if (!data.closedBy) { + return undefined; + } + const { closedBy } = data; + delete data.closedBy; + return mapUserLookup(closedBy); + }, + servedBy: (data: RoomData) => { + if (!data.servedBy) { + return undefined; + } + const { servedBy } = data; + delete data.servedBy; + return mapUserLookup(servedBy); + }, + responseBy: (data: RoomData) => { + if (!data.responseBy) { + return undefined; + } + const { responseBy } = data; + delete data.responseBy; + return mapUserLookup(responseBy); + }, + } as const; + + return transformMappedData(room, map) as unknown as Promise; + } + + async __getCreator(user: string | undefined) { + if (!user) { + return; + } + + const creator = await Users.findOneById(user, { projection: { _id: 1, username: 1, name: 1 } }); + if (!creator) { + return; + } + + return { + _id: creator._id, + username: creator.username, + name: creator.name, + }; + } + + async __getVisitor({ visitor: roomVisitor, visitorChannelInfo }: any) { + if (!roomVisitor) { + return; + } + + const visitor = await LivechatVisitors.findOneEnabledById(roomVisitor.id); + if (!visitor) { + return; + } + + const { lastMessageTs, phone } = visitorChannelInfo || {}; + + return { + _id: visitor._id, + username: visitor.username, + token: visitor.token, + status: visitor.status || 'online', + activity: visitor.activity, + ...(lastMessageTs && { lastMessageTs }), + ...(phone && { phone }), + }; + } + + async __getUserIdAndUsername(userObj: any) { + if (!userObj?.id) { + return; + } + + const user = await Users.findOneById(userObj.id, { projection: { _id: 1, username: 1 } }); + if (!user) { + return; + } + + return { + _id: user._id, + username: user.username, + }; + } + + async __getRoomCloser(room: any, v: any) { + if (!room.closedBy) { + return; + } + + if (room.closer === 'user') { + const user = await Users.findOneById(room.closedBy.id, { projection: { _id: 1, username: 1 } }); + if (!user) { + return; + } + + return { + _id: user._id, + username: user.username, + }; + } + + if (room.closer === 'visitor' && v) { + return { + _id: v._id, + username: v.username, + }; + } + } + + // TODO do we really need this? + async __getContactId({ contact }: any) { + if (!contact?._id) { + return; + } + const contactFromDb = await LivechatContacts.findOneEnabledById(contact._id, { projection: { _id: 1 } }); + return contactFromDb?._id; + } + + // TODO do we really need this? + async __getDepartment({ department }: any) { + if (!department) { + return; + } + const dept = await LivechatDepartment.findOneById(department.id, { projection: { _id: 1 } }); + return dept?._id; + } + + async convertAppRoom(room: undefined | null): Promise; + + async convertAppRoom(room: IAppsRoom): Promise; + + async convertAppRoom(room: IAppsRoom, isPartial: boolean): Promise>; + + async convertAppRoom(room: IAppsRoom | undefined | null, isPartial?: boolean): Promise | undefined>; + + async convertAppRoom(room: any, isPartial = false): Promise | undefined> { + if (!room) { + return undefined; + } + + const u = await this.__getCreator(room.creator?.id); + + const v = await this.__getVisitor(room); + + const departmentId = await this.__getDepartment(room); + + const servedBy = await this.__getUserIdAndUsername(room.servedBy); + + const closedBy = await this.__getRoomCloser(room, v); + + const contactId = await this.__getContactId(room); + + const newRoom = { + ...(room.id && { _id: room.id }), + ...(typeof room.type !== 'undefined' && { t: room.type }), + ...(typeof room.createdAt !== 'undefined' && { ts: room.createdAt }), + ...(typeof room.messageCount !== 'undefined' && { msgs: room.messageCount || 0 }), + ...(typeof room.updatedAt !== 'undefined' && { _updatedAt: room.updatedAt }), + ...(room.displayName && { fname: room.displayName }), + ...(room.type !== 'd' && room.slugifiedName && { name: room.slugifiedName }), + ...(room.members && { members: room.members }), + ...(typeof room.isDefault !== 'undefined' && { default: room.isDefault }), + ...(typeof room.isReadOnly !== 'undefined' && { ro: room.isReadOnly }), + ...(typeof room.displaySystemMessages !== 'undefined' && { sysMes: room.displaySystemMessages }), + ...(u && { u }), + ...(v && { v }), + ...(departmentId && { departmentId }), + ...(servedBy && { servedBy }), + ...(closedBy && { closedBy }), + ...(room.userIds && { uids: room.userIds }), + ...(typeof room.isWaitingResponse !== 'undefined' && { waitingResponse: !!room.isWaitingResponse }), + ...(typeof room.isOpen !== 'undefined' && { open: !!room.isOpen }), + ...(room.closedAt && { closedAt: room.closedAt }), + ...(room.lastModifiedAt && { lm: room.lastModifiedAt }), + ...(room.customFields && { customFields: room.customFields }), + ...(room.livechatData && { livechatData: room.livechatData }), + ...(typeof room.isFederated !== 'undefined' && { federated: room.isFederated }), + ...(typeof room.federation !== 'undefined' && { federation: room.federation }), + ...(typeof room.parentRoom !== 'undefined' && { prid: room.parentRoom.id }), + ...(contactId && { contactId }), + ...(room._USERNAMES && { _USERNAMES: room._USERNAMES }), + ...(room.source && { + source: { + ...room.source, + }, + }), + }; + + if (!isPartial) { + Object.assign(newRoom, room._unmappedProperties_); + } + + return newRoom as unknown as Partial; + } + + convertRoom(originalRoom: undefined | null): Promise; + + convertRoom(originalRoom: IRoom): Promise; + + convertRoom(originalRoom: IRoom | undefined | null): Promise; + + convertRoom(originalRoom: IRoom | undefined | null): Promise { + if (!originalRoom) { + return Promise.resolve(undefined); + } + + const map = { + id: '_id', + displayName: 'fname', + slugifiedName: 'name', + members: 'members', + userIds: 'uids', + messageCount: 'msgs', + createdAt: 'ts', + updatedAt: '_updatedAt', + closedAt: 'closedAt', + lastModifiedAt: 'lm', + customFields: 'customFields', + livechatData: 'livechatData', + isWaitingResponse: 'waitingResponse', + isOpen: 'open', + _USERNAMES: '_USERNAMES', + description: 'description', + source: 'source', + closer: 'closer', + teamId: 'teamId', + isTeamMain: 'teamMain', + isFederated: 'federated', + federation: 'federation', + isDefault: (room: RoomData) => { + const result = !!room.default; + delete room.default; + return result; + }, + isReadOnly: (room: RoomData) => { + const result = !!room.ro; + delete room.ro; + return result; + }, + displaySystemMessages: (room: RoomData) => { + const { sysMes } = room; + + if (typeof sysMes === 'undefined') { + return true; + } + + delete room.sysMes; + return sysMes; + }, + type: (room: RoomData) => { + const result = this._convertTypeToApp(room.t); + delete room.t; + return result; + }, + creator: async (room: RoomData) => { + const { u } = room; + + if (!u) { + return undefined; + } + + delete room.u; + + return this.orch.getConverters().get('users').convertById(u._id); + }, + visitor: (room: RoomData) => { + const { v } = room; + + if (!v) { + return undefined; + } + + return this.orch.getConverters().get('visitors').convertById(v._id); + }, + contact: (room: RoomData) => { + const { contactId } = room; + + if (!contactId) { + return undefined; + } + + return this.orch.getConverters().get('contacts').convertById(contactId); + }, + // Note: room.v is not just visitor, it also contains channel related visitor data + // so we need to pass this data to the converter + // So suppose you have a contact whom we're contacting using SMS via 2 phone no's, + // let's call X and Y. Then if the contact sends a message using X phone number, + // then room.v.phoneNo would be X and correspondingly we'll store the timestamp of + // the last message from this visitor from X phone no on room.v.lastMessageTs + visitorChannelInfo: (room: RoomData) => { + const { v } = room; + + if (!v) { + return undefined; + } + + const { lastMessageTs, phone } = v; + + return { + ...(phone && { phone }), + ...(lastMessageTs && { lastMessageTs }), + }; + }, + department: async (room: RoomData) => { + const { departmentId } = room; + + if (!departmentId) { + return undefined; + } + + delete room.departmentId; + + return this.orch.getConverters().get('departments').convertById(departmentId); + }, + closedBy: async (room: RoomData) => { + const { closedBy } = room; + + if (!closedBy) { + return undefined; + } + + delete room.closedBy; + if ((originalRoom as RoomData).closer === 'user') { + return this.orch.getConverters().get('users').convertById(closedBy._id); + } + + return this.orch.getConverters().get('visitors').convertById(closedBy._id); + }, + servedBy: async (room: RoomData) => { + const { servedBy } = room; + + if (!servedBy) { + return undefined; + } + + delete room.servedBy; + + return this.orch.getConverters().get('users').convertById(servedBy._id); + }, + responseBy: async (room: RoomData) => { + const { responseBy } = room; + + if (!responseBy) { + return undefined; + } + + delete room.responseBy; + + return this.orch.getConverters().get('users').convertById(responseBy._id); + }, + parentRoom: async (room: RoomData) => { + const { prid } = room; + + if (!prid) { + return undefined; + } + + delete room.prid; + + return this.orch.getConverters().get('rooms').convertById(prid); + }, + ...secureFieldsMapper((room: RoomData) => { + if (!room.abacAttributes) { + return undefined; + } + + const value = [ + { + permission: 'abac.read', + name: 'abacAttributes', + value: room.abacAttributes, + }, + ]; + + delete room.abacAttributes; + return value; + }), + } as const; + + return transformMappedData(originalRoom, map) as unknown as Promise; + } + + _convertTypeToApp(typeChar: IRoom['t']): RoomType | IRoom['t'] { + switch (typeChar) { + case 'c': + return RoomType.CHANNEL; + case 'p': + return RoomType.PRIVATE_GROUP; + case 'd': + return RoomType.DIRECT_MESSAGE; + case 'l': + return RoomType.LIVE_CHAT; + default: + return typeChar; + } + } +} diff --git a/apps/meteor/app/apps/server/converters/settings.js b/apps/meteor/app/apps/server/converters/settings.js deleted file mode 100644 index 07b790cb7c592..0000000000000 --- a/apps/meteor/app/apps/server/converters/settings.js +++ /dev/null @@ -1,52 +0,0 @@ -import { SettingType } from '@rocket.chat/apps-engine/definition/settings'; -import { Settings } from '@rocket.chat/models'; - -export class AppSettingsConverter { - constructor(orch) { - this.orch = orch; - } - - async convertById(settingId) { - const setting = await Settings.findOneById(settingId); - - return this.convertToApp(setting); - } - - convertToApp(setting) { - return { - id: setting._id, - type: this._convertTypeToApp(setting.type), - packageValue: setting.packageValue, - values: setting.values, - value: setting.value, - public: setting.public, - hidden: setting.hidden, - group: setting.group, - i18nLabel: setting.i18nLabel, - i18nDescription: setting.i18nDescription, - createdAt: setting.ts, - updatedAt: setting._updatedAt, - }; - } - - _convertTypeToApp(type) { - switch (type) { - case 'boolean': - return SettingType.BOOLEAN; - case 'code': - return SettingType.CODE; - case 'color': - return SettingType.COLOR; - case 'font': - return SettingType.FONT; - case 'int': - return SettingType.NUMBER; - case 'select': - return SettingType.SELECT; - case 'string': - return SettingType.STRING; - default: - return type; - } - } -} diff --git a/apps/meteor/app/apps/server/converters/settings.ts b/apps/meteor/app/apps/server/converters/settings.ts new file mode 100644 index 0000000000000..8c06003200cb7 --- /dev/null +++ b/apps/meteor/app/apps/server/converters/settings.ts @@ -0,0 +1,54 @@ +import type { IAppServerOrchestrator, IAppSettingsConverter, IAppsSetting } from '@rocket.chat/apps'; +import { SettingType } from '@rocket.chat/apps-engine/definition/settings'; +import type { ISetting } from '@rocket.chat/core-typings'; +import { Settings } from '@rocket.chat/models'; + +export class AppSettingsConverter implements IAppSettingsConverter { + constructor(protected readonly orch: IAppServerOrchestrator) { + this.orch = orch; + } + + async convertById(settingId: ISetting['_id']): Promise { + const setting = await Settings.findOneById(settingId); + + return this.convertToApp(setting as ISetting); + } + + convertToApp(setting: ISetting): IAppsSetting { + return { + id: setting._id, + type: this._convertTypeToApp(setting.type), + packageValue: setting.packageValue, + values: setting.values, + value: setting.value, + public: setting.public, + hidden: setting.hidden, + group: setting.group, + i18nLabel: setting.i18nLabel, + i18nDescription: setting.i18nDescription, + createdAt: setting.ts, + updatedAt: setting._updatedAt, + } as unknown as IAppsSetting; + } + + _convertTypeToApp(type: ISetting['type']): SettingType { + switch (type) { + case 'boolean': + return SettingType.BOOLEAN; + case 'code': + return SettingType.CODE; + case 'color': + return SettingType.COLOR; + case 'font': + return SettingType.FONT; + case 'int': + return SettingType.NUMBER; + case 'select': + return SettingType.SELECT; + case 'string': + return SettingType.STRING; + default: + return type as SettingType; + } + } +} diff --git a/apps/meteor/app/apps/server/converters/uploads.js b/apps/meteor/app/apps/server/converters/uploads.js deleted file mode 100644 index 60f85a8aa72f1..0000000000000 --- a/apps/meteor/app/apps/server/converters/uploads.js +++ /dev/null @@ -1,98 +0,0 @@ -import { Uploads } from '@rocket.chat/models'; - -import { transformMappedData } from './transformMappedData'; - -export class AppUploadsConverter { - constructor(orch) { - this.orch = orch; - } - - async convertById(id) { - const upload = await Uploads.findOneById(id); - - return this.convertToApp(upload); - } - - async convertToApp(upload) { - if (!upload) { - return undefined; - } - - const map = { - id: '_id', - name: 'name', - size: 'size', - type: 'type', - store: 'store', - description: 'description', - complete: 'complete', - uploading: 'uploading', - extension: 'extension', - progress: 'progress', - etag: 'etag', - path: 'path', - token: 'token', - url: 'url', - updatedAt: '_updatedAt', - uploadedAt: 'uploadedAt', - room: async (upload) => { - const result = await this.orch.getConverters().get('rooms').convertById(upload.rid); - delete upload.rid; - return result; - }, - user: async (upload) => { - if (!upload.userId) { - return undefined; - } - - const result = await this.orch.getConverters().get('users').convertById(upload.userId); - delete upload.userId; - return result; - }, - visitor: async (upload) => { - if (!upload.visitorToken) { - return undefined; - } - - const result = await this.orch.getConverters().get('visitors').convertByToken(upload.visitorToken); - delete upload.visitorToken; - return result; - }, - }; - - return transformMappedData(upload, map); - } - - convertToRocketChat(upload) { - if (!upload) { - return undefined; - } - - const { id: userId } = upload.user || {}; - const { token: visitorToken } = upload.visitor || {}; - const { id: rid } = upload.room; - - const newUpload = { - _id: upload.id, - name: upload.name, - size: upload.size, - type: upload.type, - extension: upload.extension, - description: upload.description, - store: upload.store, - etag: upload.etag, - complete: upload.complete, - uploading: upload.uploading, - progress: upload.progress, - token: upload.token, - url: upload.url, - _updatedAt: upload.updatedAt, - uploadedAt: upload.uploadedAt, - rid, - userId, - visitorToken, - }; - - return Object.assign(newUpload, upload._unmappedProperties_); - } -} diff --git a/apps/meteor/app/apps/server/converters/uploads.ts b/apps/meteor/app/apps/server/converters/uploads.ts new file mode 100644 index 0000000000000..cd772b0bebaa5 --- /dev/null +++ b/apps/meteor/app/apps/server/converters/uploads.ts @@ -0,0 +1,132 @@ +import type { IAppServerOrchestrator, IAppUploadsConverter, IAppsUpload } from '@rocket.chat/apps'; +import type { IUpload } from '@rocket.chat/core-typings'; +import { Uploads } from '@rocket.chat/models'; + +import { transformMappedData } from './transformMappedData'; +import { getURL } from '../../../../server/lib/utils/getURL'; + +export class AppUploadsConverter implements IAppUploadsConverter { + constructor(protected readonly orch: IAppServerOrchestrator) { + this.orch = orch; + } + + async convertById(id: string): Promise { + const upload = await Uploads.findOneById(id); + + return this.convertToApp(upload); + } + + async convertToApp(upload: undefined | null): Promise; + + async convertToApp(upload: IUpload): Promise; + + async convertToApp(upload: IUpload | undefined | null): Promise; + + async convertToApp(upload: IUpload | undefined | null): Promise { + if (!upload) { + return undefined; + } + + const map = { + // `url`/`path` are no longer persisted on the upload; derive them from the file id+name, + // matching the canonical /file-upload route. Declared before `id`/`name` so the source + // fields are still present on the cloned data when these run. + url: (upload: IUpload) => { + const relativePath = `/file-upload/${upload._id}/${encodeURIComponent(upload.name || '')}`; + delete (upload as { url?: string }).url; + return getURL(relativePath, { cdn: false, full: true }); + }, + path: (upload: IUpload) => { + const relativePath = `/file-upload/${upload._id}/${encodeURIComponent(upload.name || '')}`; + delete (upload as { path?: string }).path; + return relativePath; + }, + id: '_id', + name: 'name', + size: 'size', + type: 'type', + store: 'store', + description: 'description', + complete: 'complete', + uploading: 'uploading', + extension: 'extension', + progress: 'progress', + etag: 'etag', + token: 'token', + updatedAt: '_updatedAt', + uploadedAt: 'uploadedAt', + room: async (upload: IUpload) => { + if (!upload.rid) { + return undefined; + } + + const result = await this.orch.getConverters().get('rooms').convertById(upload.rid); + delete (upload as { rid?: string }).rid; + return result; + }, + user: async (upload: IUpload) => { + if (!upload.userId) { + return undefined; + } + + const result = await this.orch.getConverters().get('users').convertById(upload.userId); + delete (upload as { userId?: string }).userId; + return result; + }, + visitor: async (upload: IUpload) => { + const { visitorToken } = upload as { visitorToken?: string }; + if (!visitorToken) { + return undefined; + } + + const result = await this.orch.getConverters().get('visitors').convertByToken(visitorToken); + delete (upload as { visitorToken?: string }).visitorToken; + return result; + }, + } as const; + + return transformMappedData(upload, map) as unknown as Promise; + } + + convertToRocketChat(upload: undefined | null): undefined; + + convertToRocketChat(upload: IAppsUpload): IUpload; + + convertToRocketChat(upload: IAppsUpload | undefined | null): IUpload | undefined; + + convertToRocketChat(upload: IAppsUpload | undefined | null): IUpload | undefined { + if (!upload) { + return undefined; + } + + const { id: userId } = upload.user || {}; + const { token: visitorToken } = upload.visitor || {}; + const { id: rid } = upload.room || {}; + + const newUpload = { + _id: upload.id, + name: upload.name, + size: upload.size, + type: upload.type, + extension: upload.extension, + description: (upload as { description?: string }).description, + store: upload.store, + etag: upload.etag, + complete: upload.complete, + uploading: upload.uploading, + progress: upload.progress, + token: upload.token, + url: upload.url, + _updatedAt: upload.updatedAt, + uploadedAt: upload.uploadedAt, + rid, + userId, + visitorToken, + }; + + return Object.assign( + newUpload, + (upload as { _unmappedProperties_?: Record })._unmappedProperties_, + ) as unknown as IUpload; + } +} diff --git a/apps/meteor/app/apps/server/converters/users.js b/apps/meteor/app/apps/server/converters/users.js deleted file mode 100644 index 4bca06ab168ed..0000000000000 --- a/apps/meteor/app/apps/server/converters/users.js +++ /dev/null @@ -1,123 +0,0 @@ -import { UserStatusConnection, UserType } from '@rocket.chat/apps-engine/definition/users'; -import { Users } from '@rocket.chat/models'; -import { removeEmpty } from '@rocket.chat/tools'; - -export class AppUsersConverter { - constructor(orch) { - this.orch = orch; - } - - async convertById(userId) { - const user = await Users.findOneById(userId); - - return this.convertToApp(user); - } - - async convertByUsername(username) { - const user = await Users.findOneByUsername(username); - - return this.convertToApp(user); - } - - convertToApp(user) { - if (!user) { - return undefined; - } - - const type = this._convertUserTypeToEnum(user.type); - const statusConnection = this._convertStatusConnectionToEnum(user.username, user._id, user.statusConnection); - - return { - id: user._id, - username: user.username, - emails: user.emails, - type, - isEnabled: user.active, - name: user.name, - roles: user.roles, - bio: user.bio, - status: user.status, - statusText: user.statusText, - statusConnection, - utcOffset: user.utcOffset, - createdAt: user.createdAt, - updatedAt: user._updatedAt, - lastLoginAt: user.lastLogin, - appId: user.appId, - customFields: user.customFields, - isFederated: user.federated, - federation: user.federation, - sipExtension: user.freeSwitchExtension, - settings: { - preferences: { - ...(user?.settings?.preferences?.language && { language: user.settings.preferences.language }), - }, - }, - }; - } - - convertToRocketChat(user) { - if (!user) { - return undefined; - } - - return removeEmpty({ - _id: user.id, - username: user.username, - emails: user.emails, - type: user.type, - active: user.isEnabled, - name: user.name, - roles: user.roles, - bio: user.bio, - status: user.status, - statusConnection: user.statusConnection, - utcOffset: user.utfOffset, - createdAt: user.createdAt, - _updatedAt: user.updatedAt, - lastLogin: user.lastLoginAt, - appId: user.appId, - federated: user.isFederated, - federation: user.federation, - freeSwitchExtension: user.sipExtension, - }); - } - - _convertUserTypeToEnum(type) { - switch (type) { - case 'user': - return UserType.USER; - case 'bot': - return UserType.BOT; - case 'app': - return UserType.APP; - case '': - case undefined: - return UserType.UNKNOWN; - default: - console.warn(`A new user type has been added that the Apps don't know about? "${type}"`); - return type.toUpperCase(); - } - } - - _convertStatusConnectionToEnum(username, userId, status) { - switch (status) { - case 'offline': - return UserStatusConnection.OFFLINE; - case 'online': - return UserStatusConnection.ONLINE; - case 'away': - return UserStatusConnection.AWAY; - case 'busy': - return UserStatusConnection.BUSY; - case undefined: - // This is needed for Livechat guests and Rocket.Cat user. - return UserStatusConnection.UNDEFINED; - default: - console.warn( - `The user ${username} (${userId}) does not have a valid status (offline, online, away, or busy). It is currently: "${status}"`, - ); - return !status ? UserStatusConnection.OFFLINE : status.toUpperCase(); - } - } -} diff --git a/apps/meteor/app/apps/server/converters/users.ts b/apps/meteor/app/apps/server/converters/users.ts new file mode 100644 index 0000000000000..ee11ef58df761 --- /dev/null +++ b/apps/meteor/app/apps/server/converters/users.ts @@ -0,0 +1,146 @@ +import type { IAppServerOrchestrator, IAppUsersConverter, IAppsUser } from '@rocket.chat/apps'; +import { UserStatusConnection, UserType } from '@rocket.chat/apps-engine/definition/users'; +import type { IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; +import { removeEmpty } from '@rocket.chat/tools'; + +export class AppUsersConverter implements IAppUsersConverter { + constructor(protected readonly orch: IAppServerOrchestrator) { + this.orch = orch; + } + + async convertById(userId: IUser['_id']): Promise { + const user = await Users.findOneById(userId); + + return this.convertToApp(user); + } + + async convertByUsername(username: IUser['username']): Promise { + if (!username) { + return undefined; + } + + const user = await Users.findOneByUsername(username); + + return this.convertToApp(user); + } + + convertToApp(user: undefined | null): undefined; + + convertToApp(user: IUser): IAppsUser; + + convertToApp(user: IUser | undefined | null): IAppsUser | undefined; + + convertToApp(user: IUser | undefined | null): IAppsUser | undefined { + if (!user) { + return undefined; + } + + const type = this._convertUserTypeToEnum(user.type); + const statusConnection = this._convertStatusConnectionToEnum(user.username, user._id, user.statusConnection); + + return { + id: user._id, + username: user.username, + emails: user.emails, + type, + isEnabled: user.active, + name: user.name, + roles: user.roles, + bio: user.bio, + status: user.status, + statusText: user.statusText, + statusConnection, + utcOffset: user.utcOffset, + createdAt: user.createdAt, + updatedAt: user._updatedAt, + lastLoginAt: user.lastLogin, + appId: (user as { appId?: string }).appId, + customFields: user.customFields, + isFederated: user.federated, + federation: user.federation, + sipExtension: user.freeSwitchExtension, + settings: { + preferences: { + ...(user?.settings?.preferences?.language && { language: user.settings.preferences.language }), + }, + }, + } as unknown as IAppsUser; + } + + convertToRocketChat(user: undefined | null): undefined; + + convertToRocketChat(user: IAppsUser): IUser; + + convertToRocketChat(user: IAppsUser | undefined | null): IUser | undefined; + + convertToRocketChat(user: IAppsUser | undefined | null): IUser | undefined { + if (!user) { + return undefined; + } + + return removeEmpty({ + _id: user.id, + username: user.username, + emails: user.emails, + type: user.type, + active: user.isEnabled, + name: user.name, + roles: user.roles, + bio: user.bio, + status: user.status, + statusConnection: user.statusConnection, + // Prefer the correct `utcOffset`; keep the historical `utfOffset` typo as a compatibility fallback. + utcOffset: user.utcOffset ?? (user as { utfOffset?: number }).utfOffset, + createdAt: user.createdAt, + _updatedAt: user.updatedAt, + lastLogin: user.lastLoginAt, + appId: user.appId, + federated: user.isFederated, + federation: user.federation, + freeSwitchExtension: user.sipExtension, + }) as unknown as IUser; + } + + _convertUserTypeToEnum(type: IUser['type']): UserType { + switch (type) { + case 'user': + return UserType.USER; + case 'bot': + return UserType.BOT; + case 'app': + return UserType.APP; + case '': + case undefined: + return UserType.UNKNOWN; + default: + console.warn(`A new user type has been added that the Apps don't know about? "${type}"`); + return type.toUpperCase() as UserType; + } + } + + _convertStatusConnectionToEnum( + username: IUser['username'], + userId: IUser['_id'], + status: IUser['statusConnection'], + ): UserStatusConnection { + switch (status) { + case 'offline': + return UserStatusConnection.OFFLINE; + case 'online': + return UserStatusConnection.ONLINE; + case 'away': + return UserStatusConnection.AWAY; + case 'busy': + return UserStatusConnection.BUSY; + case undefined: + // This is needed for Livechat guests and Rocket.Cat user. + return UserStatusConnection.UNDEFINED; + default: + console.warn( + `The user ${username} (${userId}) does not have a valid status (offline, online, away, or busy). It is currently: "${status}"`, + ); + return (!status ? UserStatusConnection.OFFLINE : status.toUpperCase()) as UserStatusConnection; + } + } +} diff --git a/apps/meteor/app/apps/server/converters/visitors.js b/apps/meteor/app/apps/server/converters/visitors.js deleted file mode 100644 index 62d38bc88cc3b..0000000000000 --- a/apps/meteor/app/apps/server/converters/visitors.js +++ /dev/null @@ -1,66 +0,0 @@ -import { LivechatVisitors } from '@rocket.chat/models'; - -import { transformMappedData } from './transformMappedData'; - -// TODO: check if functions from this converter can be async -export class AppVisitorsConverter { - constructor(orch) { - this.orch = orch; - } - - async convertById(id) { - const visitor = await LivechatVisitors.findOneEnabledById(id); - - return this.convertVisitor(visitor); - } - - async convertByToken(token) { - const visitor = await LivechatVisitors.getVisitorByToken(token); - - return this.convertVisitor(visitor); - } - - async convertVisitor(visitor) { - if (!visitor) { - return undefined; - } - - const map = { - id: '_id', - username: 'username', - name: 'name', - department: 'department', - updatedAt: '_updatedAt', - token: 'token', - phone: 'phone', - visitorEmails: 'visitorEmails', - livechatData: 'livechatData', - status: 'status', - activity: 'activity', - externalIds: 'externalIds', - }; - - return transformMappedData(visitor, map); - } - - convertAppVisitor(visitor) { - if (!visitor) { - return undefined; - } - - const newVisitor = { - _id: visitor.id, - username: visitor.username, - name: visitor.name, - token: visitor.token, - phone: visitor.phone, - livechatData: visitor.livechatData, - status: visitor.status || 'online', - ...(visitor.visitorEmails && { visitorEmails: visitor.visitorEmails }), - ...(visitor.department && { department: visitor.department }), - ...(visitor.externalIds && { externalIds: visitor.externalIds }), - }; - - return Object.assign(newVisitor, visitor._unmappedProperties_); - } -} diff --git a/apps/meteor/app/apps/server/converters/visitors.ts b/apps/meteor/app/apps/server/converters/visitors.ts new file mode 100644 index 0000000000000..cdbc10be77871 --- /dev/null +++ b/apps/meteor/app/apps/server/converters/visitors.ts @@ -0,0 +1,83 @@ +import type { IAppServerOrchestrator, IAppVisitorsConverter, IAppsVisitor } from '@rocket.chat/apps'; +import type { ILivechatVisitor } from '@rocket.chat/core-typings'; +import { LivechatVisitors } from '@rocket.chat/models'; + +import { transformMappedData } from './transformMappedData'; + +// TODO: check if functions from this converter can be async +export class AppVisitorsConverter implements IAppVisitorsConverter { + constructor(protected readonly orch: IAppServerOrchestrator) { + this.orch = orch; + } + + async convertById(id: ILivechatVisitor['_id']): Promise { + const visitor = await LivechatVisitors.findOneEnabledById(id); + + return this.convertVisitor(visitor); + } + + async convertByToken(token: string): Promise { + const visitor = await LivechatVisitors.getVisitorByToken(token); + + return this.convertVisitor(visitor); + } + + async convertVisitor(visitor: undefined | null): Promise; + + async convertVisitor(visitor: ILivechatVisitor): Promise; + + async convertVisitor(visitor: ILivechatVisitor | undefined | null): Promise; + + async convertVisitor(visitor: ILivechatVisitor | undefined | null): Promise { + if (!visitor) { + return undefined; + } + + const map = { + id: '_id', + username: 'username', + name: 'name', + department: 'department', + updatedAt: '_updatedAt', + token: 'token', + phone: 'phone', + visitorEmails: 'visitorEmails', + livechatData: 'livechatData', + status: 'status', + activity: 'activity', + externalIds: 'externalIds', + } as const; + + return transformMappedData(visitor, map) as unknown as Promise; + } + + convertAppVisitor(visitor: undefined | null): undefined; + + convertAppVisitor(visitor: IAppsVisitor): ILivechatVisitor; + + convertAppVisitor(visitor: IAppsVisitor | undefined | null): ILivechatVisitor | undefined; + + convertAppVisitor(visitor: IAppsVisitor | undefined | null): ILivechatVisitor | undefined { + if (!visitor) { + return undefined; + } + + const newVisitor = { + _id: visitor.id, + username: visitor.username, + name: visitor.name, + token: visitor.token, + phone: visitor.phone, + livechatData: visitor.livechatData, + status: visitor.status || 'online', + ...(visitor.visitorEmails && { visitorEmails: visitor.visitorEmails }), + ...(visitor.department && { department: visitor.department }), + ...(visitor.externalIds && { externalIds: visitor.externalIds }), + }; + + return Object.assign( + newVisitor, + (visitor as { _unmappedProperties_?: Record })._unmappedProperties_, + ) as unknown as ILivechatVisitor; + } +} diff --git a/apps/meteor/app/assets/server/assets.ts b/apps/meteor/app/assets/server/assets.ts deleted file mode 100644 index c2008c6b8cbe6..0000000000000 --- a/apps/meteor/app/assets/server/assets.ts +++ /dev/null @@ -1,480 +0,0 @@ -import crypto from 'node:crypto'; -import type { ServerResponse, IncomingMessage } from 'node:http'; - -import type { IRocketChatAssets, IRocketChatAsset, ISetting } from '@rocket.chat/core-typings'; -import { Settings } from '@rocket.chat/models'; -import type { NextHandleFunction } from 'connect'; -import sizeOf from 'image-size'; -import { Meteor } from 'meteor/meteor'; -import { WebApp, WebAppInternals } from 'meteor/webapp'; -import sharp from 'sharp'; - -import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; -import { RocketChatFile } from '../../file/server'; -import { notifyOnSettingChangedById } from '../../lib/server/lib/notifyListener'; -import { settings, settingsRegistry } from '../../settings/server'; -import { getExtension } from '../../utils/lib/mimeTypes'; -import { getURL } from '../../utils/server/getURL'; - -const RocketChatAssetsInstance = new RocketChatFile.GridFS({ - name: 'assets', -}); - -type IRocketChatAssetsConfig = Record }>; - -const assets: IRocketChatAssetsConfig = { - logo: { - label: 'logo (svg, png, jpg)', - defaultUrl: 'images/logo/logo.svg', - constraints: { - type: 'image', - extensions: ['svg', 'png', 'jpg', 'jpeg'], - }, - wizard: { - step: 3, - order: 2, - }, - }, - logo_dark: { - label: 'logo - dark theme (svg, png, jpg)', - defaultUrl: 'images/logo/logo_dark.svg', - constraints: { - type: 'image', - extensions: ['svg', 'png', 'jpg', 'jpeg'], - }, - }, - background: { - label: 'login background (svg, png, jpg)', - constraints: { - type: 'image', - extensions: ['svg', 'png', 'jpg', 'jpeg'], - }, - }, - background_dark: { - label: 'login background - dark theme (svg, png, jpg)', - constraints: { - type: 'image', - extensions: ['svg', 'png', 'jpg', 'jpeg'], - }, - }, - favicon_ico: { - label: 'favicon (ico)', - defaultUrl: 'favicon.ico', - constraints: { - type: 'image', - extensions: ['ico'], - }, - }, - favicon: { - label: 'favicon (svg)', - defaultUrl: 'images/logo/icon.svg', - constraints: { - type: 'image', - extensions: ['svg'], - }, - }, - favicon_16: { - label: 'favicon 16x16 (png)', - defaultUrl: 'images/logo/favicon-16x16.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 16, - height: 16, - }, - }, - favicon_32: { - label: 'favicon 32x32 (png)', - defaultUrl: 'images/logo/favicon-32x32.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 32, - height: 32, - }, - }, - favicon_192: { - label: 'android-chrome 192x192 (png)', - defaultUrl: 'images/logo/android-chrome-192x192.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 192, - height: 192, - }, - }, - favicon_512: { - label: 'android-chrome 512x512 (png)', - defaultUrl: 'images/logo/android-chrome-512x512.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 512, - height: 512, - }, - }, - touchicon_180: { - label: 'apple-touch-icon 180x180 (png)', - defaultUrl: 'images/logo/apple-touch-icon.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 180, - height: 180, - }, - }, - touchicon_180_pre: { - label: 'apple-touch-icon-precomposed 180x180 (png)', - defaultUrl: 'images/logo/apple-touch-icon-precomposed.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 180, - height: 180, - }, - }, - tile_70: { - label: 'mstile 70x70 (png)', - defaultUrl: 'images/logo/mstile-70x70.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 70, - height: 70, - }, - }, - tile_144: { - label: 'mstile 144x144 (png)', - defaultUrl: 'images/logo/mstile-144x144.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 144, - height: 144, - }, - }, - tile_150: { - label: 'mstile 150x150 (png)', - defaultUrl: 'images/logo/mstile-150x150.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 150, - height: 150, - }, - }, - tile_310_square: { - label: 'mstile 310x310 (png)', - defaultUrl: 'images/logo/mstile-310x310.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 310, - height: 310, - }, - }, - tile_310_wide: { - label: 'mstile 310x150 (png)', - defaultUrl: 'images/logo/mstile-310x150.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 310, - height: 150, - }, - }, - safari_pinned: { - label: 'safari pinned tab (svg)', - defaultUrl: 'images/logo/safari-pinned-tab.svg', - constraints: { - type: 'image', - extensions: ['svg'], - }, - }, - livechat_widget_logo: { - label: 'widget logo (svg, png, jpg)', - constraints: { - type: 'image', - extensions: ['svg', 'png', 'jpg', 'jpeg'], - }, - settingOptions: { - section: 'Livechat', - group: 'Omnichannel', - invalidValue: { - defaultUrl: undefined, - }, - enableQuery: { _id: 'Livechat_enabled', value: true }, - enterprise: true, - modules: ['livechat-enterprise'], - sorter: 999 + 1, - }, - }, -}; - -function getAssetByKey(key: string) { - return assets[key as keyof IRocketChatAssets]; -} - -class RocketChatAssetsClass { - get assets(): IRocketChatAssets { - return assets; - } - - public async setAssetWithBuffer( - file: Buffer, - contentType: string, - asset: string, - ): Promise<{ - key: string; - value: IRocketChatAsset; - }> { - const assetInstance = getAssetByKey(asset); - if (!assetInstance) { - throw new Meteor.Error('error-invalid-asset', 'Invalid asset', { - function: 'RocketChat.Assets.setAsset', - }); - } - - const extension = getExtension(contentType); - if (assetInstance.constraints.extensions.includes(extension) === false) { - throw new Meteor.Error('error-invalid-file-type', `Invalid file type: ${contentType}`, { - function: 'RocketChat.Assets.setAsset', - }); - } - - if (assetInstance.constraints.width || assetInstance.constraints.height) { - const dimensions = sizeOf(file); - if (assetInstance.constraints.width && assetInstance.constraints.width !== dimensions.width) { - throw new Meteor.Error('error-invalid-file-width', 'Invalid file width', { - function: 'Invalid file width', - }); - } - if (assetInstance.constraints.height && assetInstance.constraints.height !== dimensions.height) { - throw new Meteor.Error('error-invalid-file-height'); - } - } - - const rs = RocketChatFile.bufferToStream(file); - await RocketChatAssetsInstance.deleteFile(asset); - - const ws = RocketChatAssetsInstance.createWriteStream(asset, contentType); - - return new Promise((resolve) => { - ws.on('end', () => { - return setTimeout(async () => { - const key = `Assets_${asset}`; - const value = { - url: `assets/${asset}.${extension}`, - defaultUrl: assetInstance.defaultUrl, - } as IRocketChatAsset; - - await RocketChatAssets.processAsset(key, value); - resolve({ - key, - value, - }); - }, 200); - }); - - rs.pipe(ws); - }); - } - - public async unsetAsset(asset: string) { - if (!getAssetByKey(asset)) { - throw new Meteor.Error('error-invalid-asset', 'Invalid asset', { - function: 'RocketChat.Assets.unsetAsset', - }); - } - - await RocketChatAssetsInstance.deleteFile(asset); - const key = `Assets_${asset}`; - const value = { - defaultUrl: getAssetByKey(asset).defaultUrl, - }; - - await RocketChatAssets.processAsset(key, value); - - return { - key, - value, - }; - } - - public refreshClients(): boolean { - return process.emit('message', { - refresh: 'client', - }); - } - - public async processAsset(settingKey: string, settingValue: any): Promise | undefined> { - if (settingKey.indexOf('Assets_') !== 0) { - return; - } - - const assetKey = settingKey.replace(/^Assets_/, ''); - const assetValue = getAssetByKey(assetKey); - - if (!assetValue) { - return; - } - - if (!settingValue?.url) { - assetValue.cache = undefined; - return; - } - - const file = await RocketChatAssetsInstance.getFile(assetKey); - if (!file) { - assetValue.cache = undefined; - return; - } - - const hash = crypto.createHash('sha1').update(file.buffer).digest('hex'); - const extension = settingValue.url.split('.').pop(); - - assetValue.cache = { - path: `assets/${assetKey}.${extension}`, - cacheable: false, - sourceMapUrl: undefined, - where: 'client', - type: 'asset', - content: file.buffer, - extension, - url: `/assets/${assetKey}.${extension}?${hash}`, - size: file.length, - uploadDate: file.uploadDate, - contentType: file.contentType, - hash, - }; - - return assetValue.cache; - } - - public getURL(assetName: string, options = { cdn: false, full: true }): string { - const asset = settings.get(assetName); - const url = asset.url || asset.defaultUrl; - - return getURL(url as string, options); - } -} - -export const RocketChatAssets = new RocketChatAssetsClass(); - -export async function addAssetToSetting(asset: string, value: IRocketChatAsset, options?: Partial): Promise { - const key = `Assets_${asset}`; - - await settingsRegistry.add( - key, - { - defaultUrl: value.defaultUrl, - }, - { - type: 'asset', - group: 'Assets', - fileConstraints: value.constraints, - i18nLabel: value.label, - asset, - public: true, - ...options, - }, - ); - - const currentValue = settings.get(key); - - if (currentValue && typeof currentValue === 'object' && currentValue.defaultUrl !== getAssetByKey(asset).defaultUrl) { - currentValue.defaultUrl = getAssetByKey(asset).defaultUrl; - - (await Settings.updateValueById(key, currentValue)).modifiedCount && void notifyOnSettingChangedById(key); - } -} - -void (async () => { - for (const key of Object.keys(assets)) { - const { wizard, settingOptions, ...value } = getAssetByKey(key); - await addAssetToSetting(key, value, { ...settingOptions, wizard }); - } -})(); - -Meteor.startup(() => { - setTimeout(() => { - process.emit('message', { - refresh: 'client', - }); - }, 200); -}); - -export const refreshClients = async (userId: string) => { - if (!userId) { - throw new Error('Invalid user'); - } - - const _hasPermission = await hasPermissionAsync(userId, 'manage-assets'); - if (!_hasPermission) { - throw new Error('Managing assets not allowed'); - } - - return RocketChatAssets.refreshClients(); -}; - -const listener = (req: IncomingMessage, res: ServerResponse, next: NextHandleFunction) => { - if (!req.url) { - return; - } - const params = { - asset: decodeURIComponent(req.url.replace(/^\//, '').replace(/\?.*$/, '')).replace(/\.[^.]*$/, ''), - }; - - const asset = getAssetByKey(params.asset); - const file = asset?.cache; - - const format = req.url.replace(/.*\.([a-z]+)(?:$|\?.*)/i, '$1'); - - if (asset && Array.isArray(asset.constraints.extensions) && !asset.constraints.extensions.includes(format)) { - res.writeHead(403); - res.end(); - return; - } - if (!file) { - const defaultUrl = asset?.defaultUrl; - if (defaultUrl) { - const assetUrl = format && ['png', 'svg'].includes(format) ? defaultUrl.replace(/(svg|png)$/, format) : defaultUrl; - req.url = `/${assetUrl}`; - WebAppInternals.staticFilesMiddleware((WebAppInternals as Record).staticFilesByArch, req, res, next); - } else { - res.writeHead(404); - res.end(); - } - - return; - } - - const reqModifiedHeader = req.headers['if-modified-since']; - if (reqModifiedHeader) { - if (reqModifiedHeader === file.uploadDate?.toUTCString()) { - res.setHeader('Last-Modified', reqModifiedHeader); - res.writeHead(304); - res.end(); - return; - } - } - - res.setHeader('Cache-Control', 'public, max-age=0'); - res.setHeader('Expires', '-1'); - - if (format && format !== file.extension && ['png', 'jpg', 'jpeg'].includes(format)) { - res.setHeader('Content-Type', `image/${format}`); - sharp(file.content) - .toFormat(format as any) - .pipe(res); - return; - } - - res.setHeader('Last-Modified', file.uploadDate?.toUTCString() || new Date().toUTCString()); - if (file.contentType) res.setHeader('Content-Type', file.contentType); - if (file.size) res.setHeader('Content-Length', file.size); - res.writeHead(200); - res.end(file.content); -}; - -WebApp.connectHandlers.use('/assets/', listener); diff --git a/apps/meteor/app/authentication/server/hooks/login.ts b/apps/meteor/app/authentication/server/hooks/login.ts deleted file mode 100644 index 191639e208dee..0000000000000 --- a/apps/meteor/app/authentication/server/hooks/login.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Accounts } from 'meteor/accounts-base'; - -import { callbacks } from '../../../../server/lib/callbacks'; -import { settings } from '../../../settings/server'; -import type { ILoginAttempt } from '../ILoginAttempt'; -import { logFailedLoginAttempts } from '../lib/logLoginAttempts'; -import { saveFailedLoginAttempts, saveSuccessfulLogin } from '../lib/restrictLoginAttempts'; - -const ignoredErrorTypes = ['totp-required', 'error-login-blocked-for-user']; - -Accounts.onLoginFailure(async (login: ILoginAttempt) => { - // do not save specific failed login attempts - if ( - settings.get('Block_Multiple_Failed_Logins_Enabled') && - login.error?.error && - !ignoredErrorTypes.includes(String(login.error.error)) - ) { - await saveFailedLoginAttempts(login); - } - - logFailedLoginAttempts(login); -}); - -callbacks.add('afterValidateLogin', (login: ILoginAttempt) => { - if (!settings.get('Block_Multiple_Failed_Logins_Enabled')) { - return; - } - - return saveSuccessfulLogin(login); -}); diff --git a/apps/meteor/app/authentication/server/index.ts b/apps/meteor/app/authentication/server/index.ts deleted file mode 100644 index 5ff4af4eef118..0000000000000 --- a/apps/meteor/app/authentication/server/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './hooks/login'; -import './startup'; diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js deleted file mode 100644 index 0d3e12e7c6e49..0000000000000 --- a/apps/meteor/app/authentication/server/startup/index.js +++ /dev/null @@ -1,532 +0,0 @@ -import { Apps, AppEvents } from '@rocket.chat/apps'; -import { User } from '@rocket.chat/core-services'; -import { Roles, Settings, Users } from '@rocket.chat/models'; -import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; -import { getLoginExpirationInDays, removeEmpty } from '@rocket.chat/tools'; -import { Accounts } from 'meteor/accounts-base'; -import { Match } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; - -import { parseCSV } from '../../../../lib/utils/parseCSV'; -import { safeHtmlDots } from '../../../../lib/utils/safeHtmlDots'; -import { callbacks } from '../../../../server/lib/callbacks'; -import { beforeCreateUserCallback } from '../../../../server/lib/callbacks/beforeCreateUserCallback'; -import { getClientAddress } from '../../../../server/lib/getClientAddress'; -import { getMaxLoginTokens } from '../../../../server/lib/getMaxLoginTokens'; -import { i18n } from '../../../../server/lib/i18n'; -import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; -import { getNewUserRoles } from '../../../../server/services/user/lib/getNewUserRoles'; -import { getAvatarSuggestionForUser } from '../../../lib/server/functions/getAvatarSuggestionForUser'; -import { joinDefaultChannels } from '../../../lib/server/functions/joinDefaultChannels'; -import { setAvatarFromServiceWithValidation } from '../../../lib/server/functions/setUserAvatar'; -import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; -import * as Mailer from '../../../mailer/server/api'; -import { settings } from '../../../settings/server'; -import { getBaseUserFields } from '../../../utils/server/functions/getBaseUserFields'; -import { isValidAttemptByUser, isValidLoginAttemptByIp } from '../lib/restrictLoginAttempts'; - -Accounts.config({ - forbidClientAccountCreation: true, -}); -/** - * Accounts calls `_initServerPublications` and holds the `_defaultPublishFields`, without Object.assign its not possible - * to extend the projection - * - * the idea is to send all required fields to the client during login - * we tried `defaultFieldsSelector` , but it changes all Meteor.userAsync projections which is undesirable - * - * - * we are removing the status here because meteor send 'offline' - */ -Object.assign(Accounts._defaultPublishFields.projection, (({ status, ...rest }) => rest)(getBaseUserFields(true))); - -// Override Meteor's _expireTokens to ensure the correct login expiration is used. -// If loginExpirationInDays is not set (e.g., startup was disrupted before settings watcher fired), -// read the setting directly from MongoDB before proceeding with token cleanup. -// This prevents tokens from being incorrectly deleted using Meteor's 90-day default. -const { _expireTokens } = Accounts; -Accounts._expireTokens = async function (oldestValidDate, userId) { - if (!Accounts._options.loginExpirationInDays) { - const loginExpiration = await Settings.getValueById('Accounts_LoginExpiration'); - Accounts._options.loginExpirationInDays = getLoginExpirationInDays(loginExpiration); - } - return _expireTokens.call(Accounts, oldestValidDate, userId); -}; - -Meteor.startup(() => { - settings.watchMultiple(['Accounts_LoginExpiration', 'Site_Name', 'From_Email'], () => { - Accounts._options.loginExpirationInDays = getLoginExpirationInDays(settings.get('Accounts_LoginExpiration')); - - Accounts.emailTemplates.siteName = settings.get('Site_Name'); - - Accounts.emailTemplates.from = `${settings.get('Site_Name')} <${settings.get('From_Email')}>`; - }); -}); - -Accounts.emailTemplates.userToActivate = { - subject() { - const subject = i18n.t('Accounts_Admin_Email_Approval_Needed_Subject_Default'); - const siteName = settings.get('Site_Name'); - - return `[${siteName}] ${subject}`; - }, - - html(options = {}) { - const email = options.reason - ? 'Accounts_Admin_Email_Approval_Needed_With_Reason_Default' - : 'Accounts_Admin_Email_Approval_Needed_Default'; - - return Mailer.replace(i18n.t(email), { - name: escapeHTML(options.name), - email: escapeHTML(options.email), - reason: escapeHTML(options.reason), - }); - }, -}; - -Accounts.emailTemplates.userActivated = { - subject({ active, username }) { - const activated = username ? 'Activated' : 'Approved'; - const action = active ? activated : 'Deactivated'; - const subject = `Accounts_Email_${action}_Subject`; - const siteName = settings.get('Site_Name'); - - return `[${siteName}] ${i18n.t(subject)}`; - }, - - html({ active, name, username }) { - const activated = username ? 'Activated' : 'Approved'; - const action = active ? activated : 'Deactivated'; - - return Mailer.replace(i18n.t(`Accounts_Email_${action}`), { - name: escapeHTML(name), - }); - }, -}; - -let verifyEmailTemplate = ''; -let enrollAccountTemplate = ''; -let resetPasswordTemplate = ''; -Meteor.startup(() => { - Mailer.getTemplateWrapped('Verification_Email', (value) => { - verifyEmailTemplate = value; - }); - Mailer.getTemplateWrapped('Accounts_Enrollment_Email', (value) => { - enrollAccountTemplate = value; - }); - Mailer.getTemplateWrapped('Forgot_Password_Email', (value) => { - resetPasswordTemplate = value; - }); -}); - -Accounts.emailTemplates.verifyEmail.html = function (userModel, url) { - const name = safeHtmlDots(userModel.name); - - return Mailer.replace(verifyEmailTemplate, { Verification_Url: url, name }); -}; - -Accounts.emailTemplates.verifyEmail.subject = function () { - const subject = settings.get('Verification_Email_Subject'); - return Mailer.replace(subject || ''); -}; - -Accounts.urls.resetPassword = function (token) { - return Meteor.absoluteUrl(`reset-password/${token}`); -}; - -Accounts.emailTemplates.resetPassword.subject = function (userModel) { - return Mailer.replace(settings.get('Forgot_Password_Email_Subject') || '', { - name: userModel.name, - }); -}; - -Accounts.emailTemplates.resetPassword.html = function (userModel, url) { - return Mailer.replacekey( - Mailer.replace(resetPasswordTemplate, { - name: userModel.name, - }), - 'Forgot_Password_Url', - url, - ); -}; - -Accounts.emailTemplates.enrollAccount.subject = function (user) { - const subject = settings.get('Accounts_Enrollment_Email_Subject'); - return Mailer.replace(subject, user); -}; - -Accounts.emailTemplates.enrollAccount.html = function (user = {} /* , url*/) { - return Mailer.replace(enrollAccountTemplate, { - name: escapeHTML(user.name), - email: user.emails && user.emails[0] && escapeHTML(user.emails[0].address), - }); -}; - -const getLinkedInName = ({ firstName, lastName }) => { - const { preferredLocale, localized: firstNameLocalized } = firstName; - const { localized: lastNameLocalized } = lastName; - - // LinkedIn new format - if (preferredLocale && firstNameLocalized && preferredLocale.language && preferredLocale.country) { - const locale = `${preferredLocale.language}_${preferredLocale.country}`; - - if (firstNameLocalized[locale] && lastNameLocalized[locale]) { - return `${firstNameLocalized[locale]} ${lastNameLocalized[locale]}`; - } - if (firstNameLocalized[locale]) { - return firstNameLocalized[locale]; - } - } - - // LinkedIn old format - if (!lastName) { - return firstName; - } - return `${firstName} ${lastName}`; -}; - -const validateEmailDomain = (user) => { - if (user.type === 'visitor') { - return true; - } - - let domainWhiteList = settings.get('Accounts_AllowedDomainsList'); - if (_.isEmpty(domainWhiteList?.trim())) { - return true; - } - - domainWhiteList = domainWhiteList.split(',').map((domain) => domain.trim()); - - if (user.emails && user.emails.length > 0) { - const email = user.emails[0].address; - const inWhiteList = domainWhiteList.some((domain) => email.match(`@${escapeRegExp(domain)}$`)); - - if (!inWhiteList) { - throw new Meteor.Error('error-invalid-domain'); - } - } - - return true; -}; - -const onCreateUserAsync = async function (options, user = {}) { - if (!options.skipBeforeCreateUserCallback) { - await beforeCreateUserCallback.run(options, user); - } - user.status = 'offline'; - - user.active = user.active !== undefined ? user.active : !settings.get('Accounts_ManuallyApproveNewUsers'); - if (settings.get('Accounts_ManuallyApproveNewUsers') && !user.active) { - user.inactiveReason = 'pending_approval'; - } - - if (!user.name) { - if (options.profile) { - if (options.profile.name) { - user.name = options.profile.name; - } else if (options.profile.firstName) { - // LinkedIn format - user.name = getLinkedInName(options.profile); - } - } - } - - if (user.services) { - const verified = settings.get('Accounts_Verify_Email_For_External_Accounts'); - - for (const service of Object.values(user.services)) { - const suggestedName = service.name || service.username; - if (!user.name && suggestedName) { - user.name = suggestedName; - } - - if (!user.emails && service.email) { - user.emails = [ - { - address: service.email, - verified, - }, - ]; - } - } - } - - if (!options.skipAdminEmail && !user.active) { - const destinations = []; - const usersInRole = await Roles.findUsersInRole('admin'); - await usersInRole.forEach((adminUser) => { - if (Array.isArray(adminUser.emails)) { - adminUser.emails.forEach((email) => { - destinations.push(`${adminUser.name}<${email.address}>`); - }); - } - }); - - const email = { - to: destinations, - from: settings.get('From_Email'), - subject: Accounts.emailTemplates.userToActivate.subject(), - html: Accounts.emailTemplates.userToActivate.html({ - ...options, - name: options.name || options.profile?.name, - email: options.email || user.emails[0].address, - }), - }; - - await Mailer.send(email); - } - - if (!options.skipOnCreateUserCallback) { - await callbacks.run('onCreateUser', options, user); - } - - if (!options.skipEmailValidation && !validateEmailDomain(user)) { - throw new Meteor.Error(403, 'User validation failed'); - } - - return removeEmpty(user); -}; - -Accounts.onCreateUser(function (...args) { - // Depends on meteor support for Async - return onCreateUserAsync.call(this, ...args); -}); - -const { insertUserDoc } = Accounts; - -Accounts.insertUserDoc = async function (options, user) { - const globalRoles = new Set(); - - if (Match.test(options.globalRoles, [String]) && options.globalRoles.length > 0) { - options.globalRoles.map((role) => globalRoles.add(role)); - } - - if (Match.test(user.globalRoles, [String]) && user.globalRoles.length > 0) { - user.globalRoles.map((role) => globalRoles.add(role)); - } - - delete user.globalRoles; - - // for some reason, the name is not being set in the user object but is being set in the options object - if (options.name && typeof options.name === 'string') { - user.name = options.name; - } - - if (user.services && !user.services.password && !options.skipAuthServiceDefaultRoles) { - const defaultAuthServiceRoles = parseCSV(settings.get('Accounts_Registration_AuthenticationServices_Default_Roles') || ''); - - if (defaultAuthServiceRoles.length > 0) { - defaultAuthServiceRoles.map((role) => globalRoles.add(role)); - } - } - - const arrayGlobalRoles = [...globalRoles]; - const roles = options.skipNewUserRolesSetting ? arrayGlobalRoles : getNewUserRoles(arrayGlobalRoles); - - if (!user.type) { - user.type = 'user'; - } - - if ( - settings.get('Accounts_TwoFactorAuthentication_Enabled') && - settings.get('Accounts_TwoFactorAuthentication_By_Email_Enabled') && - settings.get('Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In') - ) { - user.services = user.services || {}; - user.services.email2fa = { - enabled: true, - changedAt: new Date(), - }; - } - - // Make sure that the user has the field 'roles' - if (!user.roles) { - user.roles = []; - } - - const _id = await insertUserDoc.call(Accounts, options, user); - - user = await Users.findOne({ - _id, - }); - - /** - * if settings shows setup wizard to be pending - * and no admin's been found, - * and existing role list doesn't include admin - * create this user admin. - * count this as the completion of setup wizard step 1. - */ - if (!options.skipAdminCheck) { - const hasAdmin = await Users.findOneByRolesAndType('admin', 'user', { projection: { _id: 1 } }); - if (!roles.includes('admin') && !hasAdmin) { - roles.push('admin'); - if (settings.get('Show_Setup_Wizard') === 'pending') { - // TODO: audit - (await Settings.updateValueById('Show_Setup_Wizard', 'in_progress')).modifiedCount && - void notifyOnSettingChangedById('Show_Setup_Wizard'); - } - } - } - - await addUserRolesAsync(_id, roles); - - // Make user's roles to be present on callback - user = await Users.findOneById(_id, { projection: { username: 1, type: 1, roles: 1 } }); - - if (user.username) { - if (options.joinDefaultChannels !== false) { - await joinDefaultChannels(_id, options.joinDefaultChannelsSilenced); - } - - if (!options.skipAfterCreateUserCallback && user.type !== 'visitor') { - setImmediate(() => { - return callbacks.run('afterCreateUser', user); - }); - } - if (!options.skipDefaultAvatar && settings.get('Accounts_SetDefaultAvatar') === true) { - const avatarSuggestions = await getAvatarSuggestionForUser(user); - for (const service of Object.keys(avatarSuggestions)) { - const avatarData = avatarSuggestions[service]; - if (service !== 'gravatar') { - await setAvatarFromServiceWithValidation(_id, avatarData.blob, '', service); - break; - } - } - } - } - - if (!options.skipAppsEngineEvent) { - // `post` triggered events don't need to wait for the promise to resolve - Apps.self?.triggerEvent(AppEvents.IPostUserCreated, { user, performedBy: options.performedBy }).catch((e) => { - Apps.self?.getRocketChatLogger().error({ msg: 'Error while executing post user created event', err: e }); - }); - } - - return _id; -}; - -const validateLoginAttemptAsync = async function (login) { - login = await callbacks.run('beforeValidateLogin', login); - - if (!(await isValidLoginAttemptByIp(getClientAddress(login.connection)))) { - throw new Meteor.Error('error-login-blocked-for-ip', 'Login has been temporarily blocked For IP', { - function: 'Accounts.validateLoginAttempt', - }); - } - - if (!(await isValidAttemptByUser(login))) { - throw new Meteor.Error('error-login-blocked-for-user', 'Login has been temporarily blocked For User', { - function: 'Accounts.validateLoginAttempt', - }); - } - - if (login.allowed !== true) { - return login.allowed; - } - - if (login.user.type === 'visitor') { - return true; - } - - if (login.user.type === 'app') { - throw new Meteor.Error('error-app-user-is-not-allowed-to-login', 'App user is not allowed to login', { - function: 'Accounts.validateLoginAttempt', - }); - } - - if (!!login.user.active !== true) { - throw new Meteor.Error('error-user-is-not-activated', 'User is not activated', { - function: 'Accounts.validateLoginAttempt', - }); - } - - if (!login.user.roles || !Array.isArray(login.user.roles)) { - throw new Meteor.Error('error-user-has-no-roles', 'User has no roles', { - function: 'Accounts.validateLoginAttempt', - }); - } - - if (login.user.roles.includes('admin') === false && login.type === 'password' && settings.get('Accounts_EmailVerification') === true) { - const validEmail = login.user.emails.filter((email) => email.verified === true); - if (validEmail.length === 0) { - throw new Meteor.Error('error-invalid-email', 'Invalid email __email__'); - } - } - - login = await callbacks.run('onValidateLogin', login); - - await Users.updateLastLoginById(login.user._id); - setImmediate(() => { - return callbacks.run('afterValidateLogin', login); - }); - - /** - * Trigger the event only when the - * user does login in Rocket.chat - */ - if (login.type !== 'resume') { - // App IPostUserLoggedIn event hook - await Apps.self?.triggerEvent(AppEvents.IPostUserLoggedIn, login.user); - } - - return true; -}; - -Accounts.validateLoginAttempt(function (...args) { - // Depends on meteor support for Async - return validateLoginAttemptAsync.call(this, ...args); -}); - -Accounts.validateNewUser((user) => { - if (user.type === 'visitor') { - return true; - } - - if ( - settings.get('Accounts_Registration_AuthenticationServices_Enabled') === false && - settings.get('LDAP_Enable') === false && - !(user.services && user.services.password) - ) { - throw new Meteor.Error('registration-disabled-authentication-services', 'User registration is disabled for authentication services'); - } - - return true; -}); - -Accounts.validateNewUser((user) => { - if (user.type === 'visitor') { - return true; - } - - let domainWhiteList = settings.get('Accounts_AllowedDomainsList'); - if (_.isEmpty(domainWhiteList?.trim())) { - return true; - } - - domainWhiteList = domainWhiteList.split(',').map((domain) => domain.trim()); - - if (user.emails && user.emails.length > 0) { - const email = user.emails[0].address; - const inWhiteList = domainWhiteList.some((domain) => email.match(`@${escapeRegExp(domain)}$`)); - - if (inWhiteList === false) { - throw new Meteor.Error('error-invalid-domain'); - } - } - - return true; -}); - -Accounts.onLogin(async ({ user }) => { - if (!user || !user.services || !user.services.resume || !user.services.resume.loginTokens || !user._id) { - return; - } - - if (user.services.resume.loginTokens.length < getMaxLoginTokens()) { - return; - } - - await User.ensureLoginTokensLimit(user._id); -}); diff --git a/apps/meteor/app/authorization/server/functions/hasPermission.ts b/apps/meteor/app/authorization/server/functions/hasPermission.ts deleted file mode 100644 index 12cdfcd35bd77..0000000000000 --- a/apps/meteor/app/authorization/server/functions/hasPermission.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Authorization } from '@rocket.chat/core-services'; -import type { IUser, IPermission, IRoom } from '@rocket.chat/core-typings'; - -export const hasAllPermissionAsync = async ( - userId: IUser['_id'], - permissions: IPermission['_id'][], - scope?: IRoom['_id'], -): Promise => Authorization.hasAllPermission(userId, permissions, scope); -export const hasPermissionAsync = async (userId: IUser['_id'], permissionId: IPermission['_id'], scope?: IRoom['_id']): Promise => - Authorization.hasPermission(userId, permissionId, scope); -export const hasAtLeastOnePermissionAsync = async ( - userId: IUser['_id'], - permissions: IPermission['_id'][], - scope?: IRoom['_id'], -): Promise => Authorization.hasAtLeastOnePermission(userId, permissions, scope); diff --git a/apps/meteor/app/authorization/server/index.ts b/apps/meteor/app/authorization/server/index.ts deleted file mode 100644 index c74062e774b41..0000000000000 --- a/apps/meteor/app/authorization/server/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { roomAccessAttributes, canAccessRoomAsync } from './functions/canAccessRoom'; -import { getRoles } from './functions/getRoles'; -import { getUsersInRole } from './functions/getUsersInRole'; -import { subscriptionHasRole } from './functions/hasRole'; -import './methods/addPermissionToRole'; -import './methods/addUserToRole'; -import './methods/removeRoleFromPermission'; -import './streamer/permissions'; - -export { getRoles, getUsersInRole, subscriptionHasRole, canAccessRoomAsync, roomAccessAttributes }; diff --git a/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts b/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts deleted file mode 100644 index 3f5f7bfc1a263..0000000000000 --- a/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { License } from '@rocket.chat/core-services'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Permissions } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener'; -import { CONSTANTS, AuthorizationUtils } from '../../lib'; -import { hasPermissionAsync } from '../functions/hasPermission'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'authorization:addPermissionToRole'(permissionId: string, role: string): void; - } -} - -Meteor.methods({ - async 'authorization:addPermissionToRole'(permissionId, role) { - if (role === 'guest' && !AuthorizationUtils.hasRestrictionsToRole(role) && (await License.hasValidLicense())) { - AuthorizationUtils.addRolePermissionWhiteList(role, await License.getGuestPermissions()); - } - - if (AuthorizationUtils.isPermissionRestrictedForRole(permissionId, role)) { - throw new Meteor.Error('error-action-not-allowed', 'Permission is restricted', { - method: 'authorization:addPermissionToRole', - action: 'Adding_permission', - }); - } - - const uid = Meteor.userId(); - const permission = await Permissions.findOneById(permissionId); - - if (!permission) { - throw new Meteor.Error('error-invalid-permission', 'Permission does not exist', { - method: 'authorization:addPermissionToRole', - action: 'Adding_permission', - }); - } - - if ( - !uid || - !(await hasPermissionAsync(uid, 'access-permissions')) || - (permission.level === CONSTANTS.SETTINGS_LEVEL && !(await hasPermissionAsync(uid, 'access-setting-permissions'))) - ) { - throw new Meteor.Error('error-action-not-allowed', 'Adding permission is not allowed', { - method: 'authorization:addPermissionToRole', - action: 'Adding_permission', - }); - } - - // for setting-based-permissions, authorize the group access as well - if (permission.groupPermissionId) { - await Permissions.addRole(permission.groupPermissionId, role); - void notifyOnPermissionChangedById(permission.groupPermissionId); - } - - await Permissions.addRole(permission._id, role); - - void notifyOnPermissionChangedById(permission._id); - }, -}); diff --git a/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts b/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts deleted file mode 100644 index 68ca11ef9fb5f..0000000000000 --- a/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Permissions } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener'; -import { CONSTANTS } from '../../lib'; -import { hasPermissionAsync } from '../functions/hasPermission'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'authorization:removeRoleFromPermission'(permissionId: string, role: string): void; - } -} - -Meteor.methods({ - async 'authorization:removeRoleFromPermission'(permissionId, role) { - const uid = Meteor.userId(); - const permission = await Permissions.findOneById(permissionId); - - if (!permission) { - throw new Meteor.Error('error-permission-not-found', 'Permission not found', { - method: 'authorization:removeRoleFromPermission', - }); - } - - if ( - !uid || - !(await hasPermissionAsync(uid, 'access-permissions')) || - (permission.level === CONSTANTS.SETTINGS_LEVEL && !(await hasPermissionAsync(uid, 'access-setting-permissions'))) - ) { - throw new Meteor.Error('error-action-not-allowed', 'Removing permission is not allowed', { - method: 'authorization:removeRoleFromPermission', - action: 'Removing_permission', - }); - } - - // for setting based permissions, revoke the group permission once all setting permissions - // related to this group have been removed - if (permission.groupPermissionId) { - await Permissions.removeRole(permission.groupPermissionId, role); - void notifyOnPermissionChangedById(permission.groupPermissionId); - } - - await Permissions.removeRole(permission._id, role); - void notifyOnPermissionChangedById(permission._id); - }, -}); diff --git a/apps/meteor/app/autotranslate/server/autotranslate.ts b/apps/meteor/app/autotranslate/server/autotranslate.ts deleted file mode 100644 index 2f91e02463d58..0000000000000 --- a/apps/meteor/app/autotranslate/server/autotranslate.ts +++ /dev/null @@ -1,400 +0,0 @@ -import type { - IMessage, - IRoom, - MessageAttachment, - IProviderMetadata, - ISupportedLanguage, - ITranslationResult, -} from '@rocket.chat/core-typings'; -import { Logger } from '@rocket.chat/logger'; -import { Messages, Subscriptions } from '@rocket.chat/models'; -import { escapeHTML } from '@rocket.chat/string-helpers'; -import { isTruthy } from '@rocket.chat/tools'; -import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; - -import { callbacks } from '../../../server/lib/callbacks'; -import { notifyOnMessageChange } from '../../lib/server/lib/notifyListener'; -import { Markdown } from '../../markdown/server'; -import { settings } from '../../settings/server'; - -const translationLogger = new Logger('AutoTranslate'); - -const Providers = Symbol('Providers'); -const Provider = Symbol('Provider'); - -/** - * This class allows translation providers to - * register,load and also returns the active provider. - */ -export class TranslationProviderRegistry { - static [Providers]: { [k: string]: AutoTranslate } = {}; - - static enabled = false; - - static [Provider]: string | null = null; - - /** - * Registers the translation provider into the registry. - * @param {*} provider - */ - static registerProvider(provider: AutoTranslate): void { - // get provider information - const metadata = provider._getProviderMetadata(); - if (!metadata) { - translationLogger.error('Provider metadata is not defined'); - return; - } - - TranslationProviderRegistry[Providers][metadata.name] = provider; - } - - /** - * Return the active Translation provider - */ - static getActiveProvider(): AutoTranslate | null { - if (!TranslationProviderRegistry.enabled) { - return null; - } - const provider = TranslationProviderRegistry[Provider]; - if (!provider) { - return null; - } - - return TranslationProviderRegistry[Providers][provider]; - } - - static async getSupportedLanguages(target: string): Promise { - return TranslationProviderRegistry.enabled ? TranslationProviderRegistry.getActiveProvider()?.getSupportedLanguages(target) : undefined; - } - - static async translateMessage(message: IMessage, room: IRoom, targetLanguage?: string): Promise { - if (!TranslationProviderRegistry.enabled) { - return null; - } - - const provider = TranslationProviderRegistry.getActiveProvider(); - if (!provider) { - return null; - } - - return provider.translateMessage(message, { room, targetLanguage }); - } - - static getProviders(): AutoTranslate[] { - return Object.values(TranslationProviderRegistry[Providers]); - } - - static setCurrentProvider(provider: string): void { - if (provider === TranslationProviderRegistry[Provider]) { - return; - } - - TranslationProviderRegistry[Provider] = provider; - - TranslationProviderRegistry.registerCallbacks(); - } - - static setEnable(enabled: boolean): void { - TranslationProviderRegistry.enabled = enabled; - - TranslationProviderRegistry.registerCallbacks(); - } - - static registerCallbacks(): void { - if (!TranslationProviderRegistry.enabled) { - callbacks.remove('afterSaveMessage', 'autotranslate'); - return; - } - - const provider = TranslationProviderRegistry.getActiveProvider(); - if (!provider) { - return; - } - - callbacks.add( - 'afterSaveMessage', - (message, { room }) => provider.translateMessage(message, { room }), - callbacks.priority.MEDIUM, - 'autotranslate', - ); - } -} - -/** - * Generic auto translate base implementation. - * This class provides generic parts of implementation for - * tokenization, detokenization, call back register and unregister. - * @abstract - * @class - */ -export abstract class AutoTranslate { - name: string; - - languages: string[]; - - supportedLanguages: { - [language: string]: ISupportedLanguage[]; - }; - - /** - * Encapsulate the api key and provider settings. - * @constructor - */ - constructor() { - this.name = ''; - this.languages = []; - this.supportedLanguages = {}; - } - - /** - * Extracts non-translatable parts of a message - * @param {object} message - * @return {object} message - */ - tokenize(message: IMessage): IMessage { - if (!message.tokens || !Array.isArray(message.tokens)) { - message.tokens = []; - } - message = this.tokenizeEmojis(message); - message = this.tokenizeCode(message); - message = this.tokenizeURLs(message); - message = this.tokenizeMentions(message); - return message; - } - - tokenizeEmojis(message: IMessage): IMessage { - let count = message.tokens?.length || 0; - message.msg = message.msg.replace(/:[+\w\d]+:/g, (match) => { - const token = `{${count++}}`; - message.tokens?.push({ - token, - text: match, - }); - return token; - }); - - return message; - } - - tokenizeURLs(message: IMessage): IMessage { - let count = message.tokens?.length || 0; - - const schemes = 'http,https'; - - // Support ![alt text](http://image url) and [text](http://link) - message.msg = message.msg.replace( - new RegExp(`(!?\\[)([^\\]]+)(\\]\\((?:${schemes}):\\/\\/[^\\)]+\\))`, 'gm'), - (_match, pre, text, post) => { - const pretoken = `{${count++}}`; - message.tokens?.push({ - token: pretoken, - text: pre, - }); - - const posttoken = `{${count++}}`; - message.tokens?.push({ - token: posttoken, - text: post, - }); - - return pretoken + text + posttoken; - }, - ); - - // Support - message.msg = message.msg.replace( - new RegExp(`((?:<|<)(?:${schemes}):\\/\\/[^\\|]+\\|)(.+?)(?=>|>)((?:>|>))`, 'gm'), - (_match, pre, text, post) => { - const pretoken = `{${count++}}`; - message.tokens?.push({ - token: pretoken, - text: pre, - }); - - const posttoken = `{${count++}}`; - message.tokens?.push({ - token: posttoken, - text: post, - }); - - return pretoken + text + posttoken; - }, - ); - - return message; - } - - tokenizeCode(message: IMessage): IMessage { - let count = message.tokens?.length || 0; - message.html = message.msg; - message = Markdown.parseMessageNotEscaped(message); - - // Some parsers (e. g. Marked) wrap the complete message in a

- this is unnecessary and should be ignored with respect to translations - const regexWrappedParagraph = new RegExp('^\\s*

|

\\s*$', 'gm'); - message.msg = message.msg.replace(regexWrappedParagraph, ''); - - for (const [tokenIndex, value] of message.tokens?.entries() ?? []) { - const { token } = value; - if (token.indexOf('notranslate') === -1) { - const newToken = `{${count++}}`; - message.msg = message.msg.replace(token, newToken); - message.tokens ? (message.tokens[tokenIndex].token = newToken) : undefined; - } - } - - return message; - } - - tokenizeMentions(message: IMessage): IMessage { - let count = message.tokens?.length || 0; - - if (message.mentions && message.mentions.length > 0) { - message.mentions.forEach((mention) => { - message.msg = message.msg.replace(new RegExp(`(@${mention.username})`, 'gm'), (match) => { - const token = `{${count++}}`; - message.tokens?.push({ - token, - text: match, - }); - return token; - }); - }); - } - - if (message.channels && message.channels.length > 0) { - message.channels.forEach((channel) => { - message.msg = message.msg.replace(new RegExp(`(#${channel.name})`, 'gm'), (match) => { - const token = `{${count++}}`; - message.tokens?.push({ - token, - text: match, - }); - return token; - }); - }); - } - - return message; - } - - deTokenize(message: IMessage): string { - if (message.tokens && message.tokens?.length > 0) { - for (const { token, text, noHtml } of message.tokens) { - message.msg = message.msg.replace(token, () => noHtml || text); - } - } - return message.msg; - } - - /** - * Triggers the translation of the prepared (tokenized) message - * and persists the result - * @public - * @param {object} message - * @param {object} room - * @param {object} targetLanguage - * @returns {object} unmodified message object. - */ - async translateMessage(message: IMessage, { room, targetLanguage }: { room: IRoom; targetLanguage?: string }): Promise { - let targetLanguages: string[]; - if (targetLanguage) { - targetLanguages = [targetLanguage]; - } else { - targetLanguages = (await Subscriptions.getAutoTranslateLanguagesByRoomAndNotUser(room._id, message.u?._id)).filter(isTruthy); - } - if (message.msg) { - setImmediate(async () => { - let targetMessage = Object.assign({}, message); - targetMessage.html = escapeHTML(String(targetMessage.msg)); - targetMessage = this.tokenize(targetMessage); - - const translations = await this._translateMessage(targetMessage, targetLanguages); - if (!_.isEmpty(translations)) { - await Messages.addTranslations(message._id, translations, TranslationProviderRegistry[Provider] || ''); - this.notifyTranslatedMessage(message._id); - } - }); - } - - if (message.attachments && message.attachments.length > 0) { - setImmediate(async () => { - for (const [index, attachment] of message.attachments?.entries() ?? []) { - if (attachment.description || attachment.text) { - // Removes the initial link `[ ](quoterl)` from quote message before translation - const translatedText = attachment?.text?.replace(/\[(.*?)\]\(.*?\)/g, '$1') || attachment?.text; - const attachmentMessage = { ...attachment, text: translatedText }; - const translations = await this._translateAttachmentDescriptions(attachmentMessage, targetLanguages); - - if (!_.isEmpty(translations)) { - await Messages.addAttachmentTranslations(message._id, String(index), translations); - this.notifyTranslatedMessage(message._id); - } - } - } - }); - } - return Messages.findOneById(message._id); - } - - private notifyTranslatedMessage(messageId: string): void { - void notifyOnMessageChange({ - id: messageId, - }); - } - - /** - * Returns metadata information about the service provider which is used by - * the generic implementation - * @abstract - * @protected - * @returns { name, displayName, settings } - }; - */ - abstract _getProviderMetadata(): IProviderMetadata; - - /** - * Provides the possible languages _from_ which a message can be translated into a target language - * @abstract - * @protected - * @param {string} target - the language into which shall be translated - * @returns [{ language, name }] - */ - abstract getSupportedLanguages(target: string): Promise; - - /** - * Performs the actual translation of a message, - * usually by sending a REST API call to the service provider. - * @abstract - * @protected - * @param {object} message - * @param {object} targetLanguages - * @return {object} - */ - abstract _translateMessage(message: IMessage, targetLanguages: string[]): Promise; - - /** - * Performs the actual translation of an attachment (precisely its description), - * usually by sending a REST API call to the service provider. - * @abstract - * @param {object} attachment - * @param {object} targetLanguages - * @returns {object} translated messages for each target language - */ - abstract _translateAttachmentDescriptions(attachment: MessageAttachment, targetLanguages: string[]): Promise; -} - -Meteor.startup(() => { - /** Register the active service provider on the 'AfterSaveMessage' callback. - * So the registered provider will be invoked when a message is saved. - * All the other inactive service provider must be deactivated. - */ - settings.watch('AutoTranslate_ServiceProvider', (providerName) => { - TranslationProviderRegistry.setCurrentProvider(providerName); - }); - - // Get Auto Translate Active flag - settings.watch('AutoTranslate_Enabled', (value) => { - TranslationProviderRegistry.setEnable(value); - }); -}); diff --git a/apps/meteor/app/autotranslate/server/functions/getSupportedLanguages.ts b/apps/meteor/app/autotranslate/server/functions/getSupportedLanguages.ts deleted file mode 100644 index f5331d6fff697..0000000000000 --- a/apps/meteor/app/autotranslate/server/functions/getSupportedLanguages.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { TranslationProviderRegistry } from '..'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { settings } from '../../../settings/server'; - -export const getSupportedLanguages = async (userId: string, targetLanguage: string) => { - if (!settings.get('AutoTranslate_Enabled')) { - throw new Meteor.Error('error-autotranslate-disabled', 'Auto-Translate is disabled'); - } - - if (!(await hasPermissionAsync(userId, 'auto-translate'))) { - throw new Meteor.Error('error-action-not-allowed', 'Auto-Translate is not allowed', { - method: 'autoTranslate.getSupportedLanguages', - }); - } - - return TranslationProviderRegistry.getSupportedLanguages(targetLanguage); -}; diff --git a/apps/meteor/app/autotranslate/server/functions/saveSettings.ts b/apps/meteor/app/autotranslate/server/functions/saveSettings.ts deleted file mode 100644 index 7c38560622a38..0000000000000 --- a/apps/meteor/app/autotranslate/server/functions/saveSettings.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Subscriptions, Rooms } from '@rocket.chat/models'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { notifyOnSubscriptionChangedById } from '../../../lib/server/lib/notifyListener'; - -export const saveAutoTranslateSettings = async ( - userId: string, - rid: string, - field: string, - value: string, - options: { defaultLanguage: string }, -) => { - if (!(await hasPermissionAsync(userId, 'auto-translate'))) { - throw new Meteor.Error('error-action-not-allowed', 'Auto-Translate is not allowed', { - method: 'autoTranslate.saveSettings', - }); - } - - check(rid, String); - check(field, String); - check(value, String); - - if (['autoTranslate', 'autoTranslateLanguage'].indexOf(field) === -1) { - throw new Meteor.Error('error-invalid-settings', 'Invalid settings field', { - method: 'saveAutoTranslateSettings', - }); - } - - const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); - if (!subscription) { - throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription', { - method: 'saveAutoTranslateSettings', - }); - } - - let shouldNotifySubscriptionChanged = false; - - switch (field) { - case 'autoTranslate': - const room = await Rooms.findE2ERoomById(rid, { projection: { _id: 1 } }); - if (room && value === '1') { - throw new Meteor.Error('error-e2e-enabled', 'Enabling auto-translation in E2E encrypted rooms is not allowed', { - method: 'saveAutoTranslateSettings', - }); - } - - const updateAutoTranslateResponse = await Subscriptions.updateAutoTranslateById(subscription._id, value === '1'); - if (updateAutoTranslateResponse.modifiedCount) { - shouldNotifySubscriptionChanged = true; - } - - if (!subscription.autoTranslateLanguage && options.defaultLanguage) { - const updateAutoTranslateLanguageResponse = await Subscriptions.updateAutoTranslateLanguageById( - subscription._id, - options.defaultLanguage, - ); - if (updateAutoTranslateLanguageResponse.modifiedCount) { - shouldNotifySubscriptionChanged = true; - } - } - - break; - case 'autoTranslateLanguage': - const updateAutoTranslateLanguage = await Subscriptions.updateAutoTranslateLanguageById(subscription._id, value); - if (updateAutoTranslateLanguage.modifiedCount) { - shouldNotifySubscriptionChanged = true; - } - break; - } - - if (shouldNotifySubscriptionChanged) { - void notifyOnSubscriptionChangedById(subscription._id); - } - - return true; -}; diff --git a/apps/meteor/app/autotranslate/server/functions/translateMessage.ts b/apps/meteor/app/autotranslate/server/functions/translateMessage.ts deleted file mode 100644 index 8c0545369dcca..0000000000000 --- a/apps/meteor/app/autotranslate/server/functions/translateMessage.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { IMessage } from '@rocket.chat/core-typings'; -import { Rooms } from '@rocket.chat/models'; - -import { TranslationProviderRegistry } from '..'; - -export const translateMessage = async (targetLanguage?: string, message?: IMessage) => { - if (!TranslationProviderRegistry.enabled) { - return; - } - if (!message?.rid) { - return; - } - - const room = await Rooms.findOneById(message?.rid); - let translatedMessage; - - if (message && room) { - translatedMessage = await TranslationProviderRegistry.translateMessage(message, room, targetLanguage); - } - - if (!translatedMessage) { - return; - } - - return translatedMessage; -}; diff --git a/apps/meteor/app/autotranslate/server/index.ts b/apps/meteor/app/autotranslate/server/index.ts deleted file mode 100644 index e98a30265b12d..0000000000000 --- a/apps/meteor/app/autotranslate/server/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * This file contains the exported members of the package shall be re-used. - * @module AutoTranslate, TranslationProviderRegistry - */ - -import { TranslationProviderRegistry } from './autotranslate'; -import './permissions'; -import './methods/getSupportedLanguages'; -import './methods/saveSettings'; -import './methods/translateMessage'; -import './googleTranslate'; -import './deeplTranslate'; -import './libreTranslate'; -import './msTranslate'; -import './methods/getProviderUiMetadata'; - -export { TranslationProviderRegistry }; diff --git a/apps/meteor/app/autotranslate/server/methods/getSupportedLanguages.ts b/apps/meteor/app/autotranslate/server/methods/getSupportedLanguages.ts deleted file mode 100644 index 2e853057336a0..0000000000000 --- a/apps/meteor/app/autotranslate/server/methods/getSupportedLanguages.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { ISupportedLanguage } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; -import { Meteor } from 'meteor/meteor'; - -import { getSupportedLanguages } from '../functions/getSupportedLanguages'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'autoTranslate.getSupportedLanguages'(targetLanguage: string): ISupportedLanguage[] | undefined; - } -} - -Meteor.methods({ - async 'autoTranslate.getSupportedLanguages'(targetLanguage) { - const userId = Meteor.userId(); - - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'getSupportedLanguages', - }); - } - - return getSupportedLanguages(userId, targetLanguage); - }, -}); - -DDPRateLimiter.addRule( - { - type: 'method', - name: 'autoTranslate.getSupportedLanguages', - userId(/* userId*/) { - return true; - }, - }, - 5, - 60000, -); diff --git a/apps/meteor/app/autotranslate/server/methods/saveSettings.ts b/apps/meteor/app/autotranslate/server/methods/saveSettings.ts deleted file mode 100644 index fa5badb0e1af5..0000000000000 --- a/apps/meteor/app/autotranslate/server/methods/saveSettings.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { saveAutoTranslateSettings } from '../functions/saveSettings'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'autoTranslate.saveSettings'(rid: string, field: string, value: string, options: { defaultLanguage: string }): boolean; - } -} - -Meteor.methods({ - async 'autoTranslate.saveSettings'(rid, field, value, options) { - methodDeprecationLogger.method('autoTranslate.saveSettings', '9.0.0', '/v1/autotranslate.saveSettings'); - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'saveAutoTranslateSettings', - }); - } - - return saveAutoTranslateSettings(userId, rid, field, value, options); - }, -}); diff --git a/apps/meteor/app/autotranslate/server/methods/translateMessage.ts b/apps/meteor/app/autotranslate/server/methods/translateMessage.ts deleted file mode 100644 index 673f8dcf153e7..0000000000000 --- a/apps/meteor/app/autotranslate/server/methods/translateMessage.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { IMessage } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Messages, Rooms } from '@rocket.chat/models'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { canAccessRoomAsync } from '../../../authorization/server'; -import { translateMessage } from '../functions/translateMessage'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'autoTranslate.translateMessage'(message: IMessage | undefined, targetLanguage: string): Promise; - } -} - -Meteor.methods({ - async 'autoTranslate.translateMessage'(message, targetLanguage) { - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'autoTranslate.translateMessage', - }); - } - check(message?._id, String); - check(targetLanguage, String); - const msg = await Messages.findOneById(message._id); - if (!msg) { - throw new Meteor.Error('error-message-not-found', 'Message not found'); - } - const room = await Rooms.findOneById(msg.rid); - if (!room || !(await canAccessRoomAsync(room, { _id: userId }))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } - return translateMessage(targetLanguage, msg); - }, -}); diff --git a/apps/meteor/app/bot-helpers/server/index.ts b/apps/meteor/app/bot-helpers/server/index.ts deleted file mode 100644 index 6db9cc24c6b4b..0000000000000 --- a/apps/meteor/app/bot-helpers/server/index.ts +++ /dev/null @@ -1,214 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { UserStatus } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Rooms, Users } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; -import type { Filter, FindCursor } from 'mongodb'; - -import { removeUserFromRoomMethod } from '../../../server/methods/removeUserFromRoom'; -import { hasRoleAsync } from '../../authorization/server/functions/hasRole'; -import { addUserToRole } from '../../authorization/server/methods/addUserToRole'; -import { removeUserFromRole } from '../../authorization/server/methods/removeUserFromRole'; -import { addUsersToRoomMethod } from '../../lib/server/methods/addUsersToRoom'; -import { settings } from '../../settings/server'; - -/** - * BotHelpers helps bots - * "private" properties use meteor collection cursors, so they stay reactive - * "public" properties use getters to fetch and filter collections as array - */ -class BotHelpers { - private queries: { - online: Filter; - users: Filter; - }; - - private userFields: Record; - - private _allUsers: FindCursor; - - private _onlineUsers: FindCursor; - - constructor() { - this.queries = { - online: { status: { $ne: UserStatus.OFFLINE } }, - users: { roles: { $not: { $all: ['bot'] } } }, - }; - } - - // setup collection cursors with array of fields from setting - setupCursors(fieldsSetting: string | string[]) { - this.userFields = {}; - if (typeof fieldsSetting === 'string') { - fieldsSetting = fieldsSetting.split(','); - } - fieldsSetting.forEach((n) => { - this.userFields[n.trim()] = 1; - }); - this._allUsers = Users.find(this.queries.users, { projection: this.userFields }); - this._onlineUsers = Users.find({ $and: [this.queries.users, this.queries.online] }, { projection: this.userFields }); - } - - // request methods or props as arguments to Meteor.call - async request(prop: keyof this, ...params: unknown[]): Promise { - const p = this[prop]; - - if (typeof p === 'undefined') { - return null; - } - if (typeof p === 'function') { - return p(...params); - } - - return p; - } - - async addUserToRole(userName: string, roleId: string, userId: string): Promise { - await addUserToRole(userId, roleId, userName); - } - - async removeUserFromRole(userName: string, roleId: string, userId: string): Promise { - await removeUserFromRole(userId, roleId, userName); - } - - async addUserToRoom(userName: string, room: string): Promise { - const foundRoom = await Rooms.findOneByIdOrName(room); - - if (!foundRoom) { - throw new Meteor.Error('invalid-channel'); - } - - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'addUserToRoom' }); - } - await addUsersToRoomMethod(userId, { - rid: foundRoom._id, - users: [userName], - }); - } - - async removeUserFromRoom(userName: string, room: string) { - const foundRoom = await Rooms.findOneByIdOrName(room); - - if (!foundRoom) { - throw new Meteor.Error('invalid-channel'); - } - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user'); - } - await removeUserFromRoomMethod(userId, { rid: foundRoom._id, username: userName }); - } - - // generic error whenever property access insufficient to fill request - requestError() { - throw new Meteor.Error('error-not-allowed', 'Bot request not allowed', { - method: 'botRequest', - action: 'bot_request', - }); - } - - // "public" properties accessed by getters - // allUsers / onlineUsers return whichever properties are enabled by settings - get allUsers() { - if (!Object.keys(this.userFields).length) { - this.requestError(); - return false; - } - return this._allUsers.toArray(); - } - - get onlineUsers() { - if (!Object.keys(this.userFields).length) { - this.requestError(); - return false; - } - return this._onlineUsers.toArray(); - } - - get allUsernames() { - return (async () => { - if (!this.userFields.hasOwnProperty('username')) { - this.requestError(); - return false; - } - return (await this._allUsers.toArray()).map((user) => user.username); - })(); - } - - get onlineUsernames() { - return (async () => { - if (!this.userFields.hasOwnProperty('username')) { - this.requestError(); - return false; - } - return (await this._onlineUsers.toArray()).map((user) => user.username); - })(); - } - - get allNames() { - return (async () => { - if (!this.userFields.hasOwnProperty('name')) { - this.requestError(); - return false; - } - return (await this._allUsers.toArray()).map((user) => user.name); - })(); - } - - get onlineNames() { - return (async () => { - if (!this.userFields.hasOwnProperty('name')) { - this.requestError(); - return false; - } - return (await this._onlineUsers.toArray()).map((user) => user.name); - })(); - } - - get allIDs() { - return (async () => { - if (!this.userFields.hasOwnProperty('_id') || !this.userFields.hasOwnProperty('username')) { - this.requestError(); - return false; - } - return (await this._allUsers.toArray()).map((user) => ({ id: user._id, name: user.username })); - })(); - } - - get onlineIDs() { - return (async () => { - if (!this.userFields.hasOwnProperty('_id') || !this.userFields.hasOwnProperty('username')) { - this.requestError(); - return false; - } - return (await this._onlineUsers.toArray()).map((user) => ({ id: user._id, name: user.username })); - })(); - } -} - -// add class to meteor methods -const botHelpers = new BotHelpers(); - -// init cursors with fields setting and update on setting change -settings.watch('BotHelpers_userFields', (value) => { - botHelpers.setupCursors(value); -}); - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - botRequest: (prop: keyof BotHelpers, ...params: unknown[]) => Promise; - } -} - -Meteor.methods({ - async botRequest(...args) { - const userID = Meteor.userId(); - if (userID && (await hasRoleAsync(userID, 'bot'))) { - return botHelpers.request(...args, userID); - } - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'botRequest' }); - }, -}); diff --git a/apps/meteor/app/channel-settings/server/index.ts b/apps/meteor/app/channel-settings/server/index.ts deleted file mode 100644 index 39242cef11109..0000000000000 --- a/apps/meteor/app/channel-settings/server/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import './methods/saveRoomSettings'; - -export { saveRoomTopic } from './functions/saveRoomTopic'; -export { saveRoomName } from './functions/saveRoomName'; diff --git a/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts b/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts deleted file mode 100644 index d787fd16cf653..0000000000000 --- a/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Settings } from '@rocket.chat/models'; -import { Random } from '@rocket.chat/random'; - -import { updateAuditedBySystem } from '../../../../server/settings/lib/auditedSettingUpdates'; -import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; -import { settings } from '../../../settings/server'; -import { userScopes } from '../oauthScopes'; -import { getRedirectUri } from './getRedirectUri'; - -export async function getOAuthAuthorizationUrl() { - const state = Random.id(); - - await updateAuditedBySystem({ - reason: 'getOAuthAuthorizationUrl', - })(Settings.updateValueById, 'Cloud_Workspace_Registration_State', state); - - void notifyOnSettingChangedById('Cloud_Workspace_Registration_State'); - - const cloudUrl = settings.get('Cloud_Url'); - const clientId = settings.get('Cloud_Workspace_Client_Id'); - const redirectUri = getRedirectUri(); - - const scope = userScopes.join(' '); - - return `${cloudUrl}/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&state=${state}`; -} diff --git a/apps/meteor/app/cloud/server/functions/getRedirectUri.ts b/apps/meteor/app/cloud/server/functions/getRedirectUri.ts deleted file mode 100644 index ceb8448e2d8a2..0000000000000 --- a/apps/meteor/app/cloud/server/functions/getRedirectUri.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { settings } from '../../../settings/server'; - -export function getRedirectUri() { - return `${settings.get('Site_Url')}/admin/cloud/oauth-callback`.replace(/\/\/admin+/g, '/admin'); -} diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts deleted file mode 100644 index 2ae63939e9d93..0000000000000 --- a/apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { CloudWorkspaceRegistrationError } from '../../../../../lib/errors/CloudWorkspaceRegistrationError'; -import { SystemLogger } from '../../../../../server/lib/logger/system'; -import { CloudWorkspaceAccessTokenEmptyError, CloudWorkspaceAccessTokenError, isAbortError } from '../getWorkspaceAccessToken'; -import { announcementSync } from './announcementSync'; -import { legacySyncWorkspace } from './legacySyncWorkspace'; -import { syncCloudData } from './syncCloudData'; -import { getCachedSupportedVersionsToken } from '../supportedVersionsToken/supportedVersionsToken'; - -/** - * Syncs the workspace with the cloud - * @returns {Promise} - * @throws {Error} - If there is an unexpected error during sync like a network error - */ -export async function syncWorkspace() { - try { - await announcementSync(); - await syncCloudData(); - } catch (err) { - switch (true) { - case err instanceof CloudWorkspaceRegistrationError: - case err instanceof CloudWorkspaceAccessTokenError: { - // There is no access token, so we can't sync - SystemLogger.info('Workspace does not have a valid access token, sync aborted'); - break; - } - default: { - if (!(err instanceof CloudWorkspaceAccessTokenEmptyError) && !isAbortError(err)) { - SystemLogger.error({ msg: 'Error during workspace sync', err }); - } - SystemLogger.info({ - msg: 'Falling back to legacy sync', - function: 'syncCloudData', - }); - try { - await legacySyncWorkspace(); - } catch (err) { - switch (true) { - case err instanceof CloudWorkspaceRegistrationError: - case err instanceof CloudWorkspaceAccessTokenError: { - // There is no access token, so we can't sync - break; - } - default: { - if (!(err instanceof CloudWorkspaceAccessTokenEmptyError) && !isAbortError(err)) { - SystemLogger.error({ msg: 'Error during fallback workspace sync', err }); - } - throw err; - } - } - } - } - } - } finally { - await getCachedSupportedVersionsToken.reset(); - } -} diff --git a/apps/meteor/app/cloud/server/index.ts b/apps/meteor/app/cloud/server/index.ts deleted file mode 100644 index 78485ccab1fa7..0000000000000 --- a/apps/meteor/app/cloud/server/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { cronJobs } from '@rocket.chat/cron'; -import { Meteor } from 'meteor/meteor'; - -import { connectWorkspace } from './functions/connectWorkspace'; -import { CloudWorkspaceAccessTokenEmptyError, getWorkspaceAccessToken } from './functions/getWorkspaceAccessToken'; -import { getWorkspaceAccessTokenWithScope } from './functions/getWorkspaceAccessTokenWithScope'; -import { retrieveRegistrationStatus } from './functions/retrieveRegistrationStatus'; -import { syncWorkspace } from './functions/syncWorkspace'; -import { SystemLogger } from '../../../server/lib/logger/system'; -import './methods'; - -const licenseCronName = 'Cloud Workspace Sync'; - -Meteor.startup(async () => { - const { workspaceRegistered } = await retrieveRegistrationStatus(); - - if (process.env.REG_TOKEN && process.env.REG_TOKEN !== '' && !workspaceRegistered) { - try { - SystemLogger.info('REG_TOKEN Provided. Attempting to register'); - - if (!(await connectWorkspace(process.env.REG_TOKEN))) { - throw new Error("Couldn't register with token. Please make sure token is valid or hasn't already been used"); - } - - SystemLogger.info('Successfully registered with token provided by REG_TOKEN!'); - } catch (err: any) { - SystemLogger.error({ msg: 'An error occurred registering with token.', err }); - } - } - - setImmediate(async () => { - try { - await syncWorkspace(); - } catch (err: any) { - if (err instanceof CloudWorkspaceAccessTokenEmptyError) { - return; - } - if (err.type && err.type === 'AbortError') { - return; - } - SystemLogger.error({ msg: 'An error occurred syncing workspace.', err }); - } - }); - const minute = Math.floor(Math.random() * 60); - await cronJobs.add(licenseCronName, `${minute} */12 * * *`, async () => { - try { - await syncWorkspace(); - } catch (err: any) { - if (err instanceof CloudWorkspaceAccessTokenEmptyError) { - return; - } - if (err.type && err.type === 'AbortError') { - return; - } - SystemLogger.error({ msg: 'An error occurred syncing workspace.', err }); - } - }); -}); - -export { getWorkspaceAccessToken, getWorkspaceAccessTokenWithScope }; diff --git a/apps/meteor/app/cloud/server/methods.ts b/apps/meteor/app/cloud/server/methods.ts deleted file mode 100644 index ef6e6b34927ea..0000000000000 --- a/apps/meteor/app/cloud/server/methods.ts +++ /dev/null @@ -1,208 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { buildWorkspaceRegistrationData } from './functions/buildRegistrationData'; -import { checkUserHasCloudLogin } from './functions/checkUserHasCloudLogin'; -import { connectWorkspace } from './functions/connectWorkspace'; -import { finishOAuthAuthorization } from './functions/finishOAuthAuthorization'; -import { getOAuthAuthorizationUrl } from './functions/getOAuthAuthorizationUrl'; -import { retrieveRegistrationStatus } from './functions/retrieveRegistrationStatus'; -import { startRegisterWorkspace } from './functions/startRegisterWorkspace'; -import { syncWorkspace } from './functions/syncWorkspace'; -import { userLogout } from './functions/userLogout'; -import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../lib/server/lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'cloud:checkRegisterStatus': () => { - workspaceRegistered: boolean; - workspaceId: string; - uniqueId: string; - token: string; - email: string; - }; - 'cloud:getWorkspaceRegisterData': () => string; - 'cloud:registerWorkspace': () => boolean; - 'cloud:syncWorkspace': () => boolean; - 'cloud:connectWorkspace': (token: string) => boolean | Error; - 'cloud:getOAuthAuthorizationUrl': () => string; - 'cloud:finishOAuthAuthorization': (code: string, state: string) => boolean; - 'cloud:checkUserLoggedIn': () => boolean; - 'cloud:logout': () => Promise; - } -} - -Meteor.methods({ - /** - * @deprecated this method is deprecated and will be removed soon. - * Prefer using cloud.registrationStatus rest api. - */ - async 'cloud:checkRegisterStatus'() { - methodDeprecationLogger.method('cloud:checkRegisterStatus', '9.0.0', '/v1/cloud.registrationStatus'); - const uid = Meteor.userId(); - - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'cloud:checkRegisterStatus', - }); - } - - if (!(await hasPermissionAsync(uid, 'manage-cloud'))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'cloud:checkRegisterStatus', - }); - } - - return retrieveRegistrationStatus(); - }, - async 'cloud:getWorkspaceRegisterData'() { - const uid = Meteor.userId(); - - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'cloud:getWorkspaceRegisterData', - }); - } - - if (!(await hasPermissionAsync(uid, 'manage-cloud'))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'cloud:getWorkspaceRegisterData', - }); - } - - return Buffer.from(JSON.stringify(await buildWorkspaceRegistrationData(undefined))).toString('base64'); - }, - async 'cloud:registerWorkspace'() { - const uid = Meteor.userId(); - - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'cloud:registerWorkspace', - }); - } - - if (!(await hasPermissionAsync(uid, 'manage-cloud'))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'cloud:registerWorkspace', - }); - } - - return startRegisterWorkspace(); - }, - async 'cloud:syncWorkspace'() { - const uid = Meteor.userId(); - - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'cloud:syncWorkspace', - }); - } - - if (!(await hasPermissionAsync(uid, 'manage-cloud'))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'cloud:syncWorkspace', - }); - } - - await syncWorkspace(); - - return true; - }, - async 'cloud:connectWorkspace'(token) { - check(token, String); - - const uid = Meteor.userId(); - - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'cloud:connectWorkspace', - }); - } - - if (!(await hasPermissionAsync(uid, 'manage-cloud'))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'cloud:connectWorkspace', - }); - } - - if (!token) { - throw new Meteor.Error('error-invalid-payload', 'Token is required.', { - method: 'cloud:connectWorkspace', - }); - } - - return connectWorkspace(token); - }, - // Currently unused but will link local account to Rocket.Chat Cloud account. - async 'cloud:getOAuthAuthorizationUrl'() { - const uid = Meteor.userId(); - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'cloud:getOAuthAuthorizationUrl', - }); - } - - if (!(await hasPermissionAsync(uid, 'manage-cloud'))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'cloud:getOAuthAuthorizationUrl', - }); - } - - return getOAuthAuthorizationUrl(); - }, - async 'cloud:finishOAuthAuthorization'(code, state) { - check(code, String); - check(state, String); - - const uid = Meteor.userId(); - - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'cloud:finishOAuthAuthorization', - }); - } - - if (!(await hasPermissionAsync(uid, 'manage-cloud'))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'cloud:finishOAuthAuthorization', - }); - } - - return finishOAuthAuthorization(code, state); - }, - async 'cloud:checkUserLoggedIn'() { - const uid = Meteor.userId(); - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'cloud:checkUserLoggedIn', - }); - } - - if (!(await hasPermissionAsync(uid, 'manage-cloud'))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'cloud:checkUserLoggedIn', - }); - } - - return checkUserHasCloudLogin(uid); - }, - async 'cloud:logout'() { - const uid = Meteor.userId(); - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'cloud:logout', - }); - } - - if (!(await hasPermissionAsync(uid, 'manage-cloud'))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'cloud:logout', - }); - } - - return userLogout(uid); - }, -}); diff --git a/apps/meteor/app/cors/server/cors.ts b/apps/meteor/app/cors/server/cors.ts deleted file mode 100644 index 9464762d57d04..0000000000000 --- a/apps/meteor/app/cors/server/cors.ts +++ /dev/null @@ -1,182 +0,0 @@ -import type http from 'node:http'; -import type { UrlWithParsedQuery } from 'node:url'; -import url from 'node:url'; - -import { Logger } from '@rocket.chat/logger'; -import { Meteor } from 'meteor/meteor'; -import type { StaticFiles } from 'meteor/webapp'; -import { WebApp, WebAppInternals } from 'meteor/webapp'; - -import { getWebAppHash } from '../../../server/configuration/configureBoilerplate'; -import { settings } from '../../settings/server'; - -// Taken from 'connect' types -type NextFunction = (err?: any) => void; - -const logger = new Logger('CORS'); - -declare module 'meteor/webapp' { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace WebApp { - function setInlineScriptsAllowed(allowed: boolean): Promise; - } -} - -let templatePromise: Promise | void; -export async function setInlineScriptsAllowed(allowed: boolean): Promise { - templatePromise = WebAppInternals.setInlineScriptsAllowed(allowed); -} - -WebApp.rawConnectHandlers.use(async (_req: http.IncomingMessage, res: http.ServerResponse, next: NextFunction) => { - if (templatePromise) { - await templatePromise; - templatePromise = void 0; - } - - // XSS Protection for old browsers (IE) - res.setHeader('X-XSS-Protection', '1'); - - // X-Content-Type-Options header to prevent MIME Sniffing - res.setHeader('X-Content-Type-Options', 'nosniff'); - - if (settings.get('Iframe_Restrict_Access')) { - res.setHeader('X-Frame-Options', settings.get('Iframe_X_Frame_Options')); - } - - if (settings.get('Enable_CSP')) { - // eslint-disable-next-line @typescript-eslint/naming-convention - const cdn_prefixes = [ - settings.get('CDN_PREFIX'), - settings.get('CDN_PREFIX_ALL') ? null : settings.get('CDN_JSCSS_PREFIX'), - 'https://cdn.zapier.com', - ] - .filter(Boolean) - .join(' '); - - const inlineHashes = [ - // Hash for `window.close()`, required by the CAS login popup. - "'sha256-jqxtvDkBbRAl9Hpqv68WdNOieepg8tJSYu1xIy7zT34='", - // Hash for /apps/meteor/packages/rocketchat-livechat/assets/demo.html:25 - "'sha256-aui5xYk3Lu1dQcnsPlNZI+qDTdfzdUv3fzsw80VLJgw='", - ] - .filter(Boolean) - .join(' '); - const external = [ - settings.get('Accounts_OAuth_Apple') && 'https://appleid.cdn-apple.com', - settings.get('PiwikAnalytics_enabled') && settings.get('PiwikAnalytics_url'), - settings.get('GoogleAnalytics_enabled') && 'https://www.google-analytics.com', - 'https://zapier.com', - ...settings - .get('Extra_CSP_Domains') - .split(/[ \n\,]/gim) - .filter((e) => Boolean(e.trim())), - ] - .filter(Boolean) - .join(' '); - res.setHeader( - 'Content-Security-Policy', - [ - `default-src 'self' ${cdn_prefixes}`, - 'connect-src *', - `font-src 'self' ${cdn_prefixes} data:`, - 'frame-src *', - 'img-src * data: blob:', - 'media-src * data:', - `script-src 'self' 'unsafe-eval' ${inlineHashes} ${cdn_prefixes} ${external}`, - `style-src 'self' 'unsafe-inline' ${cdn_prefixes}`, - ].join('; '), - ); - } - - return next(); -}); - -const _staticFilesMiddleware = WebAppInternals.staticFilesMiddleware; -declare module 'meteor/webapp' { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace WebApp { - function categorizeRequest( - req: http.IncomingMessage, - ): { arch: string; path: string; url: UrlWithParsedQuery } & Record; - } -} - -let cachingVersion = ''; -export function setCachingVersion(value: string): void { - cachingVersion = value.trim(); -} - -// @ts-expect-error - accessing internal property of webapp -WebAppInternals.staticFilesMiddleware = function ( - staticFiles: StaticFiles, - req: http.IncomingMessage & { cookies: Record }, - res: http.ServerResponse & { cookie: (cookie: string, value: string) => void }, - next: NextFunction, -) { - res.setHeader('Access-Control-Allow-Origin', '*'); - const { arch, path, url } = WebApp.categorizeRequest(req); - - if (cachingVersion && req.cookies.cache_version !== cachingVersion) { - res.cookie('cache_version', cachingVersion); - res.setHeader('Clear-Site-Data', '"cache"'); - } - - // Prevent meteor_runtime_config.js to load from a different expected hash possibly causing - // a cache of the file for the wrong hash and start a client loop due to the mismatch - // of the hashes of ui versions which would be checked against a websocket response - if (path === '/meteor_runtime_config.js') { - const hash = getWebAppHash(arch); - - if (!hash || hash !== url.query.hash) { - res.writeHead(404); - return res.end(); - } - - res.setHeader('Cache-Control', 'public, max-age=3600'); - } - - return _staticFilesMiddleware(staticFiles, req, res, next); -}; - -const oldHttpServerListeners = WebApp.httpServer.listeners('request').slice(0); - -WebApp.httpServer.removeAllListeners('request'); - -WebApp.httpServer.addListener('request', (req, res, ...args) => { - const next = () => { - for (const oldListener of oldHttpServerListeners) { - oldListener.apply(WebApp.httpServer, [req, res, ...args]); - } - }; - - if (settings.get('Force_SSL') !== true) { - next(); - return; - } - - const remoteAddress = req.connection.remoteAddress || req.socket.remoteAddress || ''; - const localhostRegexp = /^\s*(127\.0\.0\.1|::1)\s*$/; - const localhostTest = function (x: string) { - return localhostRegexp.test(x); - }; - - const isLocal = - localhostRegexp.test(remoteAddress) && - (!req.headers['x-forwarded-for'] || (req.headers['x-forwarded-for'] as string).split(',').every(localhostTest)); - // @ts-expect-error - `pair` is valid, but doesnt exists on types - const isSsl = req.connection.pair || (req.headers['x-forwarded-proto'] && req.headers['x-forwarded-proto'].indexOf('https') !== -1); - - logger.debug({ msg: 'CORS request info', url: req.url, remoteAddress, isLocal, isSsl, headers: req.headers }); - - if (!isLocal && !isSsl) { - let host = req.headers.host || url.parse(Meteor.absoluteUrl()).hostname || ''; - host = host.replace(/:\d+$/, ''); - res.writeHead(302, { - Location: `https://${host}${req.url}`, - }); - res.end(); - return; - } - - return next(); -}); diff --git a/apps/meteor/app/cors/server/index.ts b/apps/meteor/app/cors/server/index.ts deleted file mode 100644 index e91bda0591a52..0000000000000 --- a/apps/meteor/app/cors/server/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import './cors'; -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings/server'; - -Meteor.startup(() => { - settings.watch('Force_SSL', (value) => { - Meteor.absoluteUrl.defaultOptions.secure = Boolean(value); - }); -}); diff --git a/apps/meteor/app/crowd/server/crowd.ts b/apps/meteor/app/crowd/server/crowd.ts deleted file mode 100644 index 6039aaceb09de..0000000000000 --- a/apps/meteor/app/crowd/server/crowd.ts +++ /dev/null @@ -1,424 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { cronJobs } from '@rocket.chat/cron'; -import { Users } from '@rocket.chat/models'; -import { SHA256 } from '@rocket.chat/sha256'; -import AtlassianCrowd from 'atlassian-crowd-patched'; -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; - -import { logger } from './logger'; -import { crowdIntervalValuesToCronMap } from '../../../server/settings/crowd'; -import { deleteUser } from '../../lib/server/functions/deleteUser'; -import { setRealName } from '../../lib/server/functions/setRealName'; -import { setUserActiveStatus } from '../../lib/server/functions/setUserActiveStatus'; -import { notifyOnUserChange, notifyOnUserChangeById, notifyOnUserChangeAsync } from '../../lib/server/lib/notifyListener'; -import { settings } from '../../settings/server'; - -type CrowdUser = Pick & { crowd: Record; crowd_username: string }; - -function fallbackDefaultAccountSystem(bind: typeof Accounts, username: string | Record, password: string) { - if (typeof username === 'string') { - if (username.indexOf('@') === -1) { - username = { username }; - } else { - username = { email: username }; - } - } - - logger.info({ msg: 'Fallback to default account system', username }); - - const loginRequest = { - user: username, - password: { - digest: SHA256(password), - algorithm: 'sha-256', - }, - }; - - return Accounts._runLoginHandlers(bind, loginRequest); -} - -export class CROWD { - private crowdClient: any; - - private options: { - crowd: { - base: string; - }; - application: { - name: string; - password: string; - }; - rejectUnauthorized: boolean; - }; - - constructor() { - let url = settings.get('CROWD_URL'); - - this.options = { - crowd: { - base: !/\/$/.test(url) ? (url += '/') : url, - }, - application: { - name: settings.get('CROWD_APP_USERNAME'), - password: settings.get('CROWD_APP_PASSWORD'), - }, - rejectUnauthorized: settings.get('CROWD_Reject_Unauthorized'), - }; - - this.crowdClient = new AtlassianCrowd(this.options); - } - - async checkConnection(): Promise { - return new Promise((resolve, reject) => - this.crowdClient.ping((err: any) => { - if (err) { - reject(err); - } - resolve(); - }), - ); - } - - async fetchCrowdUser(crowdUsername: string): Promise> { - return new Promise((resolve, reject) => - this.crowdClient.user.find(crowdUsername, (err: any, userResponse: Record) => { - if (err) { - reject(err); - } - resolve({ - displayname: userResponse['display-name'], - username: userResponse.name, - email: userResponse.email, - active: userResponse.active, - crowd_username: crowdUsername, - }); - }), - ); - } - - async searchForCrowdUserByMail(email?: string): Promise | undefined> { - return new Promise((resolve) => - this.crowdClient.search('user', `email=" ${email} "`, (err: any, response: Record) => { - if (err) { - resolve(undefined); - } - resolve(response); - }), - ); - } - - async authenticate(username: string, password: string): Promise | undefined> { - if (!username || !password) { - logger.error('No username or password'); - return; - } - const projection = { username: 1, crowd_username: 1, crowd: 1 }; - logger.info('Extracting crowd_username'); - let user = null; - let crowdUsername = username; - - if (username.indexOf('@') !== -1) { - const email = username; - - user = await Users.findOne({ 'emails.address': email }, { projection }); - if (user) { - crowdUsername = user.crowd_username; - } else { - logger.debug({ msg: 'Could not find a user by email', username }); - } - } - - if (user == null) { - user = await Users.findOne({ username }, { projection }); - if (user) { - crowdUsername = user.crowd_username; - } else { - logger.debug('Could not find a user by username'); - } - } - - if (user == null) { - user = await Users.findOne({ crowd_username: username }, { projection }); - if (user) { - crowdUsername = user.crowd_username; - } else { - logger.debug({ msg: 'Could not find a user with by crowd_username', username }); - } - } - - if (user && !crowdUsername) { - logger.debug('Local user found, redirecting to fallback login'); - return { - crowd: false, - }; - } - - if (!user && crowdUsername) { - logger.debug('New user. User is not synced yet.'); - } - logger.debug({ msg: 'Going to crowd', crowdUsername }); - - return new Promise((resolve, reject) => - this.crowdClient.user.authenticate(crowdUsername, password, async (err: any, res: Record) => { - if (err) { - reject(err); - } - const user = res; - try { - const crowdUser: Record = await this.fetchCrowdUser(crowdUsername); - if (user && settings.get('CROWD_Allow_Custom_Username') === true) { - crowdUser.username = user.name; - } - - if (user) { - crowdUser._id = user._id; - } - crowdUser.password = password; - - resolve(crowdUser); - } catch (err) { - reject(err); - } - }), - ); - } - - async syncDataToUser(crowdUser: Record, id: string) { - const user = { - username: this.cleanUsername(crowdUser.username), - crowd_username: crowdUser.crowd_username, - emails: [ - { - address: crowdUser.email, - verified: settings.get('Accounts_Verify_Email_For_External_Accounts'), - }, - ], - crowd: true, - }; - - if (crowdUser.password) { - await Accounts.setPasswordAsync(id, crowdUser.password, { - logout: false, - }); - - await Users.unsetRequirePasswordChange(id); - } - - if (crowdUser.displayname) { - await setRealName(id, crowdUser.displayname); - } - - await Users.updateOne( - { _id: id }, - { - $set: user, - }, - ); - - void notifyOnUserChange({ - clientAction: 'updated', - id, - diff: { - ...user, - ...(crowdUser.displayname && { name: crowdUser.displayname }), - }, - }); - - await setUserActiveStatus(id, crowdUser.active); - } - - async sync() { - // if crowd is disabled bail out - if (settings.get('CROWD_Enable') !== true) { - return; - } - - const users = - ((await Users.findCrowdUsers().toArray()) as unknown as (IUser & { crowd: Record; crowd_username: string })[]) || []; - - logger.info('Sync started...'); - - for (const user of users) { - let crowdUsername = user.hasOwnProperty('crowd_username') ? user.crowd_username : user.username; - logger.info({ msg: 'Syncing user', crowdUsername }); - if (!crowdUsername) { - logger.warn({ msg: 'User has no crowd_username', username: user.username }); - continue; - } - - let crowdUser = null; - - try { - crowdUser = await this.fetchCrowdUser(crowdUsername); - } catch (err) { - logger.debug({ msg: 'Error while syncing user from CROWD', err }); - logger.error({ msg: 'Could not sync user with username', crowd_username: crowdUsername }); - - const email = user.emails?.[0].address; - logger.info({ msg: 'Attempting to find user by email', email }); - - const response = await this.searchForCrowdUserByMail(email); - if (!response || response.users.length === 0) { - logger.warn({ msg: 'Could not find user in CROWD with username or email', crowd_username: crowdUsername, email }); - if (settings.get('CROWD_Remove_Orphaned_Users') === true) { - logger.info({ msg: 'Removing user', crowd_username: crowdUsername }); - setImmediate(async () => { - await deleteUser(user._id); - logger.info({ msg: 'User removed', crowd_username: crowdUsername }); - }); - } - return; - } - crowdUsername = response.users[0].name; - logger.info({ msg: 'User found by email. Syncing user', crowd_username: crowdUsername }); - if (!crowdUsername) { - logger.warn({ msg: 'User has no crowd_username', username: user.username }); - continue; - } - - crowdUser = await this.fetchCrowdUser(crowdUsername); - } - - if (settings.get('CROWD_Allow_Custom_Username') === true) { - crowdUser.username = user.username; - } - - await this.syncDataToUser(crowdUser, user._id); - } - } - - cleanUsername(username: string) { - if (settings.get('CROWD_Clean_Usernames') === true) { - return username.split('@')[0]; - } - return username; - } - - async updateUserCollection(crowdUser: Record) { - const username = crowdUser.crowd_username || crowdUser.username; - const mail = crowdUser.email; - - // If id is not provided, user is linked by crowd_username or email address - const userQuery = { - ...(crowdUser._id && { _id: crowdUser._id }), - ...(!crowdUser._id && { - $or: [{ crowd_username: username }, { 'emails.address': mail }], - }), - }; - - // find our existing user if they exist - const user = await Users.findOne(userQuery); - - if (user) { - const stampedToken = Accounts._generateStampedLoginToken(); - - await Users.updateOne( - { _id: user._id }, - { - $push: { - 'services.resume.loginTokens': Accounts._hashStampedToken(stampedToken), - }, - }, - ); - - // TODO this can be optmized so places that care about loginTokens being removed are invoked directly - // instead of having to listen to every watch.users event - void notifyOnUserChangeAsync(async () => { - const userTokens = await Users.findOneById(crowdUser._id, { projection: { 'services.resume.loginTokens': 1 } }); - if (!userTokens) { - return; - } - - return { - clientAction: 'updated', - id: crowdUser._id, - diff: { 'services.resume.loginTokens': userTokens.services?.resume?.loginTokens }, - }; - }); - - await this.syncDataToUser(crowdUser, user._id); - - return { - userId: user._id, - token: stampedToken.token, - }; - } - - // Attempt to create the new user - try { - crowdUser._id = await Accounts.createUserAsync(crowdUser); - - void notifyOnUserChangeById({ clientAction: 'inserted', id: crowdUser._id }); - - // sync the user data - await this.syncDataToUser(crowdUser, crowdUser._id); - - return { - userId: crowdUser._id, - }; - } catch (err) { - logger.error({ msg: 'Error creating new crowd user.', err }); - } - } -} - -Accounts.registerLoginHandler('crowd', async function (this: typeof Accounts, loginRequest) { - if (!loginRequest.crowd) { - return undefined; - } - - logger.info({ msg: 'Init CROWD login', username: loginRequest.username }); - - if (settings.get('CROWD_Enable') !== true) { - return fallbackDefaultAccountSystem(this, loginRequest.username, loginRequest.crowdPassword); - } - - try { - const crowd = new CROWD(); - const user = await crowd.authenticate(loginRequest.username, loginRequest.crowdPassword); - - if (user?.crowd === false) { - logger.debug({ msg: 'User is not a valid crowd user, falling back', username: loginRequest.username }); - return fallbackDefaultAccountSystem(this, loginRequest.username, loginRequest.crowdPassword); - } - - if (!user) { - logger.debug({ msg: 'User is not allowed to access Rocket.Chat', username: loginRequest.username }); - return new Meteor.Error('not-authorized', 'User is not authorized by crowd'); - } - - const result = await crowd.updateUserCollection(user); - - return result; - } catch (err: any) { - logger.error({ msg: 'Crowd user not authenticated due to an error', err }); - - throw new Meteor.Error('user-not-found', err.message); - } -}); - -const jobName = 'CROWD_Sync'; - -Meteor.startup(() => { - settings.watchMultiple(['CROWD_Sync_User_Data', 'CROWD_Sync_Interval'], async function addCronJobDebounced([data, interval]) { - if (data !== true) { - logger.info('Disabling CROWD Background Sync'); - if (await cronJobs.has(jobName)) { - await cronJobs.remove(jobName); - } - return; - } - const crowd = new CROWD(); - if (interval) { - if (await cronJobs.has(jobName)) { - await cronJobs.remove(jobName); - } - - logger.info('Enabling CROWD Background Sync'); - const cronInterval = crowdIntervalValuesToCronMap[String(interval)]; - - await cronJobs.add(jobName, cronInterval, () => crowd.sync()); - } - }); -}); diff --git a/apps/meteor/app/crowd/server/index.ts b/apps/meteor/app/crowd/server/index.ts deleted file mode 100644 index a90aba7f9556c..0000000000000 --- a/apps/meteor/app/crowd/server/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './crowd'; -import './methods'; diff --git a/apps/meteor/app/crowd/server/methods.ts b/apps/meteor/app/crowd/server/methods.ts deleted file mode 100644 index c659afc4fbf36..0000000000000 --- a/apps/meteor/app/crowd/server/methods.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { Meteor } from 'meteor/meteor'; - -import { CROWD } from './crowd'; -import { logger } from './logger'; -import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; -import { settings } from '../../settings/server'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - crowd_test_connection(): { message: TranslationKey; params: string[] }; - crowd_sync_users(): { message: string; params: string[] }; - } -} - -Meteor.methods({ - async crowd_test_connection() { - const user = await Meteor.userAsync(); - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'crowd_test_connection', - }); - } - - if (!(await hasPermissionAsync(user._id, 'test-admin-options'))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'crowd_test_connection', - }); - } - - if (settings.get('CROWD_Enable') !== true) { - throw new Meteor.Error('crowd_disabled'); - } - - try { - const crowd = new CROWD(); - await crowd.checkConnection(); - - return { - message: 'Crowd_Connection_successful' as const, - params: [], - }; - } catch (err) { - logger.error({ - msg: 'Invalid crowd connection details, check the url and application username/password and make sure this server is allowed to speak to crowd', - err, - }); - throw new Meteor.Error('Invalid connection details', '', { method: 'crowd_test_connection' }); - } - }, - async crowd_sync_users() { - const user = await Meteor.userAsync(); - if (settings.get('CROWD_Enable') !== true) { - throw new Meteor.Error('crowd_disabled'); - } - - if (!user || !(await hasPermissionAsync(user._id, 'sync-auth-services-users'))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'crowd_sync_users', - }); - } - - try { - const crowd = new CROWD(); - const startTime = Date.now(); - await crowd.sync(); - const stopTime = Date.now(); - const actual = Math.ceil((stopTime - startTime) / 1000); - - return { - message: `User data synced in ${actual} seconds`, - params: [], - }; - } catch (err) { - logger.error({ msg: 'Error syncing user data. ', err }); - throw new Meteor.Error('Error syncing user data', '', { method: 'crowd_sync_users' }); - } - }, -}); diff --git a/apps/meteor/app/custom-oauth/.gitignore b/apps/meteor/app/custom-oauth/.gitignore deleted file mode 100644 index 677a6fc26373d..0000000000000 --- a/apps/meteor/app/custom-oauth/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.build* diff --git a/apps/meteor/app/custom-sounds/server/index.ts b/apps/meteor/app/custom-sounds/server/index.ts deleted file mode 100644 index 6a14ccff15656..0000000000000 --- a/apps/meteor/app/custom-sounds/server/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import './startup/custom-sounds'; -import './methods/deleteCustomSound'; -import './methods/insertOrUpdateSound'; -import './methods/listCustomSounds'; -import './methods/uploadCustomSound'; diff --git a/apps/meteor/app/custom-sounds/server/lib/insertOrUpdateSound.ts b/apps/meteor/app/custom-sounds/server/lib/insertOrUpdateSound.ts deleted file mode 100644 index 856852106eba6..0000000000000 --- a/apps/meteor/app/custom-sounds/server/lib/insertOrUpdateSound.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { api } from '@rocket.chat/core-services'; -import { CustomSounds } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import type { ICustomSoundData } from '../methods/insertOrUpdateSound'; -import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; - -export const insertOrUpdateSound = async (soundData: ICustomSoundData): Promise => { - // silently strip colon; this allows for uploading :soundname: as soundname - soundData.name = (soundData.name || '').replace(/:/g, ''); - - if (!soundData.name.trim()) { - throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', { - method: 'insertOrUpdateSound', - field: 'Name', - }); - } - - // allow all characters except colon, whitespace, comma, >, <, &, ", ', /, \, (, ) - // more practical than allowing specific sets of characters; also allows foreign languages - const nameValidation = /[\s,:><&"'\/\\\(\)]/; - - if (nameValidation.test(soundData.name)) { - throw new Meteor.Error('error-input-is-not-a-valid-field', `${soundData.name} is not a valid name`, { - method: 'insertOrUpdateSound', - input: soundData.name, - field: 'Name', - }); - } - - const matchingResults = await CustomSounds.findByName(soundData.name, soundData._id).toArray(); - - if (matchingResults.length > 0) { - throw new Meteor.Error('Custom_Sound_Error_Name_Already_In_Use', 'The custom sound name is already in use', { - method: 'insertOrUpdateSound', - }); - } - - if (!soundData._id) { - return ( - await CustomSounds.create({ - name: soundData.name, - extension: soundData.extension, - }) - ).insertedId; - } - - if (soundData.newFile) { - await RocketChatFileCustomSoundsInstance.deleteFile(`${soundData._id}.${soundData.previousExtension}`); - } - - if (soundData.name !== soundData.previousName || soundData.extension !== soundData.previousExtension) { - await CustomSounds.updateById(soundData._id, { name: soundData.name, extension: soundData.extension }); - const updatedSound = await CustomSounds.findOneById(soundData._id); - if (updatedSound) { - void api.broadcast('notify.updateCustomSound', { soundData: updatedSound }); - } - } - - return soundData._id; -}; diff --git a/apps/meteor/app/custom-sounds/server/lib/uploadCustomSound.ts b/apps/meteor/app/custom-sounds/server/lib/uploadCustomSound.ts deleted file mode 100644 index 926b5a2fed540..0000000000000 --- a/apps/meteor/app/custom-sounds/server/lib/uploadCustomSound.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { api } from '@rocket.chat/core-services'; -import type { RequiredField } from '@rocket.chat/core-typings'; -import { CustomSounds } from '@rocket.chat/models'; - -import { RocketChatFile } from '../../../file/server'; -import type { ICustomSoundData } from '../methods/insertOrUpdateSound'; -import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; - -export const uploadCustomSound = async ( - buffer: Buffer, - contentType: string, - soundData: RequiredField, -): Promise => { - const rs = RocketChatFile.bufferToStream(buffer); - - if (soundData.previousExtension) { - await RocketChatFileCustomSoundsInstance.deleteFile(`${soundData._id}.${soundData.previousExtension}`); - } - - return new Promise((resolve, reject) => { - const ws = RocketChatFileCustomSoundsInstance.createWriteStream(`${soundData._id}.${soundData.extension}`, contentType); - - ws.on('error', (err: Error) => { - reject(err); - }); - - rs.on('error', (err: Error) => { - ws.destroy(); - reject(err); - }); - - ws.on('end', () => { - setTimeout(async () => { - const sound = await CustomSounds.findOneById(soundData._id); - if (sound) { - void api.broadcast('notify.updateCustomSound', { soundData: sound }); - } - }, 500); - resolve(); - }); - - rs.pipe(ws); - }); -}; diff --git a/apps/meteor/app/custom-sounds/server/methods/deleteCustomSound.ts b/apps/meteor/app/custom-sounds/server/methods/deleteCustomSound.ts deleted file mode 100644 index 5b122fa5e116f..0000000000000 --- a/apps/meteor/app/custom-sounds/server/methods/deleteCustomSound.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ICustomSound } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { deleteCustomSound } from '../lib/deleteCustomSound'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - deleteCustomSound(_id: ICustomSound['_id']): Promise; - } -} - -Meteor.methods({ - async deleteCustomSound(_id) { - methodDeprecationLogger.method('deleteCustomSound', '9.0.0', '/v1/custom-sounds.delete'); - if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-sounds'))) { - throw new Meteor.Error('not_authorized'); - } - check(_id, String); - await deleteCustomSound(_id); - return true; - }, -}); diff --git a/apps/meteor/app/custom-sounds/server/methods/insertOrUpdateSound.ts b/apps/meteor/app/custom-sounds/server/methods/insertOrUpdateSound.ts deleted file mode 100644 index a2881ad13ef2d..0000000000000 --- a/apps/meteor/app/custom-sounds/server/methods/insertOrUpdateSound.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { insertOrUpdateSound } from '../lib/insertOrUpdateSound'; - -export type ICustomSoundData = { - _id?: string; - name: string; - extension: string; - previousName?: string; - previousSound?: { - extension?: string; - }; - previousExtension?: string; - newFile?: boolean; - random?: number; -}; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - insertOrUpdateSound(soundData: ICustomSoundData): Promise; - } -} - -Meteor.methods({ - async insertOrUpdateSound(soundData) { - methodDeprecationLogger.method('insertOrUpdateSound', '9.0.0', ['/v1/custom-sounds.create', '/v1/custom-sounds.update']); - if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-sounds'))) { - throw new Meteor.Error('not_authorized'); - } - if (soundData._id) { - check(soundData._id, String); - } - return insertOrUpdateSound(soundData); - }, -}); diff --git a/apps/meteor/app/custom-sounds/server/methods/uploadCustomSound.ts b/apps/meteor/app/custom-sounds/server/methods/uploadCustomSound.ts deleted file mode 100644 index e979d66110ba9..0000000000000 --- a/apps/meteor/app/custom-sounds/server/methods/uploadCustomSound.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { RequiredField } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import type { ICustomSoundData } from './insertOrUpdateSound'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { uploadCustomSound } from '../lib/uploadCustomSound'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - uploadCustomSound(binaryContent: string, contentType: string, soundData: RequiredField): void; - } -} - -Meteor.methods({ - async uploadCustomSound(binaryContent, contentType, soundData) { - methodDeprecationLogger.method('uploadCustomSound', '9.0.0', ['/v1/custom-sounds.create', '/v1/custom-sounds.update']); - if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-sounds'))) { - throw new Meteor.Error('not_authorized'); - } - const buffer = Buffer.from(binaryContent, 'binary'); - await uploadCustomSound(buffer, contentType, soundData); - }, -}); diff --git a/apps/meteor/app/discussion/server/index.ts b/apps/meteor/app/discussion/server/index.ts deleted file mode 100644 index 6235f5c5b1a4f..0000000000000 --- a/apps/meteor/app/discussion/server/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import './permissions'; - -import './hooks/propagateDiscussionMetadata'; - -// Methods -import './methods/createDiscussion'; diff --git a/apps/meteor/app/dolphin/server/lib.ts b/apps/meteor/app/dolphin/server/lib.ts deleted file mode 100644 index 826a277b81e4e..0000000000000 --- a/apps/meteor/app/dolphin/server/lib.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; -import { ServiceConfiguration } from 'meteor/service-configuration'; - -import { callbacks } from '../../../server/lib/callbacks'; -import { beforeCreateUserCallback } from '../../../server/lib/callbacks/beforeCreateUserCallback'; -import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; -import { settings } from '../../settings/server'; - -const config = { - serverURL: '', - authorizePath: '/m/oauth2/auth/', - tokenPath: '/m/oauth2/token/', - identityPath: '/m/oauth2/api/me/', - scope: 'basic', - addAutopublishFields: { - forLoggedInUser: ['services.dolphin'], - forOtherUsers: ['services.dolphin.name'], - }, - accessTokenParam: 'access_token', -}; - -const Dolphin = new CustomOAuth('dolphin', config); - -function DolphinOnCreateUser(options: any, user?: IUser) { - // TODO: callbacks Fix this - if (user?.services?.dolphin?.NickName) { - user.username = user.services.dolphin.NickName; - } - return options; -} - -Meteor.startup(async () => { - settings.watch('Accounts_OAuth_Dolphin_URL', (value) => { - config.serverURL = value; - return Dolphin.configure(config); - }); - - if (settings.get('Accounts_OAuth_Dolphin_URL')) { - const data = { - buttonLabelText: settings.get('Accounts_OAuth_Dolphin_button_label_text'), - buttonColor: settings.get('Accounts_OAuth_Dolphin_button_color'), - buttonLabelColor: settings.get('Accounts_OAuth_Dolphin_button_label_color'), - clientId: settings.get('Accounts_OAuth_Dolphin_id'), - secret: settings.get('Accounts_OAuth_Dolphin_secret'), - serverURL: settings.get('Accounts_OAuth_Dolphin_URL'), - loginStyle: settings.get('Accounts_OAuth_Dolphin_login_style'), - }; - - await ServiceConfiguration.configurations.upsertAsync({ service: 'dolphin' }, { $set: data }); - } - - beforeCreateUserCallback.add(DolphinOnCreateUser, callbacks.priority.HIGH, 'dolphin'); -}); diff --git a/apps/meteor/app/drupal/server/index.ts b/apps/meteor/app/drupal/server/index.ts deleted file mode 100644 index cf327e4971bb2..0000000000000 --- a/apps/meteor/app/drupal/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './lib'; diff --git a/apps/meteor/app/drupal/server/lib.ts b/apps/meteor/app/drupal/server/lib.ts deleted file mode 100644 index d137551fb8377..0000000000000 --- a/apps/meteor/app/drupal/server/lib.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { OauthConfig } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; - -import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; -import { settings } from '../../settings/server'; - -// Drupal Server CallBack URL needs to be http(s)://{rocketchat.server}[:port]/_oauth/drupal -// In RocketChat -> Administration the URL needs to be http(s)://{drupal.server}/ - -const config: OauthConfig = { - serverURL: '', - identityPath: '/oauth2/UserInfo', - authorizePath: '/oauth2/authorize', - tokenPath: '/oauth2/token', - scope: 'openid email profile offline_access', - tokenSentVia: 'payload', - usernameField: 'preferred_username', - mergeUsers: true, - addAutopublishFields: { - forLoggedInUser: ['services.drupal'], - forOtherUsers: ['services.drupal.name'], - }, - accessTokenParam: 'access_token', -}; - -const Drupal = new CustomOAuth('drupal', config); - -Meteor.startup(() => { - settings.watch('API_Drupal_URL', (value) => { - config.serverURL = value; - Drupal.configure(config); - }); -}); diff --git a/apps/meteor/app/e2e/server/beforeCreateRoom.ts b/apps/meteor/app/e2e/server/beforeCreateRoom.ts deleted file mode 100644 index cb04d0def049d..0000000000000 --- a/apps/meteor/app/e2e/server/beforeCreateRoom.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { prepareCreateRoomCallback } from '../../../server/lib/callbacks/beforeCreateRoomCallback'; -import { settings } from '../../settings/server'; - -prepareCreateRoomCallback.add(({ type, extraData }) => { - if ( - settings.get('E2E_Enable') && - ((type === 'd' && settings.get('E2E_Enabled_Default_DirectRooms')) || - (type === 'p' && settings.get('E2E_Enabled_Default_PrivateRooms'))) - ) { - extraData.encrypted = extraData.encrypted ?? true; - } -}); diff --git a/apps/meteor/app/e2e/server/index.ts b/apps/meteor/app/e2e/server/index.ts deleted file mode 100644 index 8c8c59488fd79..0000000000000 --- a/apps/meteor/app/e2e/server/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { api } from '@rocket.chat/core-services'; - -import { callbacks } from '../../../server/lib/callbacks'; - -import './beforeCreateRoom'; -import './methods/setUserPublicAndPrivateKeys'; -import './methods/getUsersOfRoomWithoutKey'; -import './methods/updateGroupKey'; -import './methods/setRoomKeyID'; -import './methods/fetchMyKeys'; -import './methods/resetOwnE2EKey'; -import './methods/requestSubscriptionKeys'; - -callbacks.add( - 'afterJoinRoom', - (_user, room) => { - void api.broadcast('notify.e2e.keyRequest', room._id, room.e2eKeyId); - }, - callbacks.priority.MEDIUM, - 'e2e', -); diff --git a/apps/meteor/app/e2e/server/methods/setRoomKeyID.ts b/apps/meteor/app/e2e/server/methods/setRoomKeyID.ts deleted file mode 100644 index a9b4ae5dda49e..0000000000000 --- a/apps/meteor/app/e2e/server/methods/setRoomKeyID.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Rooms } from '@rocket.chat/models'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; -import { notifyOnRoomChangedById } from '../../../lib/server/lib/notifyListener'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'e2e.setRoomKeyID'(rid: IRoom['_id'], keyID: string): void; - } -} - -export const setRoomKeyIDMethod = async (userId: string, rid: IRoom['_id'], keyID: string): Promise => { - if (!(await canAccessRoomIdAsync(rid, userId))) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.setRoomKeyID' }); - } - - const room = await Rooms.findOneById>(rid, { projection: { e2eKeyId: 1 } }); - - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.setRoomKeyID' }); - } - - if (room.e2eKeyId) { - throw new Meteor.Error('error-room-e2e-key-already-exists', 'E2E Key ID already exists', { - method: 'e2e.setRoomKeyID', - }); - } - - await Rooms.setE2eKeyId(room._id, keyID); - - void notifyOnRoomChangedById(room._id); -}; - -Meteor.methods({ - async 'e2e.setRoomKeyID'(rid, keyID) { - check(rid, String); - check(keyID, String); - - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'e2e.setRoomKeyID' }); - } - - if (!rid) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.setRoomKeyID' }); - } - - await setRoomKeyIDMethod(userId, rid, keyID); - }, -}); diff --git a/apps/meteor/app/emoji-custom/server/index.ts b/apps/meteor/app/emoji-custom/server/index.ts deleted file mode 100644 index 3ec2051f8fffb..0000000000000 --- a/apps/meteor/app/emoji-custom/server/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import './startup/emoji-custom'; -import './methods/deleteEmojiCustom'; -import './methods/insertOrUpdateEmoji'; -import './methods/uploadEmojiCustom'; diff --git a/apps/meteor/app/emoji-custom/server/lib/insertOrUpdateEmoji.ts b/apps/meteor/app/emoji-custom/server/lib/insertOrUpdateEmoji.ts deleted file mode 100644 index 0158c1d5fc3f6..0000000000000 --- a/apps/meteor/app/emoji-custom/server/lib/insertOrUpdateEmoji.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { api } from '@rocket.chat/core-services'; -import { EmojiCustom } from '@rocket.chat/models'; -import limax from 'limax'; -import { Meteor } from 'meteor/meteor'; - -import { trim } from '../../../../lib/utils/stringUtils'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom'; - -export type EmojiData = { - _id?: string; - name: string; - aliases?: string; - extension: string; - previousName?: string; - previousExtension?: string; - newFile?: boolean; -}; - -type EmojiDataWithParsedAliases = Omit & { _id: string; aliases: string[] }; - -export async function insertOrUpdateEmoji(userId: string | null, emojiData: EmojiData): Promise { - if (!userId || !(await hasPermissionAsync(userId, 'manage-emoji'))) { - throw new Meteor.Error('not_authorized'); - } - - if (!trim(emojiData.name)) { - throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', { - method: 'insertOrUpdateEmoji', - field: 'Name', - }); - } - - emojiData.name = limax(emojiData.name, { replacement: '_' }); - - // allow all characters except colon, whitespace, comma, >, <, &, ", ', /, \, (, ) - // more practical than allowing specific sets of characters; also allows foreign languages - const nameValidation = /[\s,:><&"'\/\\\(\)]/; - const aliasValidation = /[:><&\|"'\/\\\(\)]/; - - // silently strip colon; this allows for uploading :emojiname: as emojiname - emojiData.name = emojiData.name.replace(/:/g, ''); - emojiData.aliases = emojiData.aliases?.replace(/:/g, ''); - - if (nameValidation.test(emojiData.name)) { - throw new Meteor.Error('error-input-is-not-a-valid-field', `${emojiData.name} is not a valid name`, { - method: 'insertOrUpdateEmoji', - input: emojiData.name, - field: 'Name', - }); - } - - let aliases: string[] = []; - if (emojiData.aliases) { - if (aliasValidation.test(emojiData.aliases)) { - throw new Meteor.Error('error-input-is-not-a-valid-field', `${emojiData.aliases} is not a valid alias set`, { - method: 'insertOrUpdateEmoji', - input: emojiData.aliases, - field: 'Alias_Set', - }); - } - aliases = emojiData.aliases - .split(/\s*,\s*/) - .filter(Boolean) - .map((alias) => limax(alias, { replacement: '_' })) - .filter((alias) => alias !== emojiData.name); - } - - emojiData.extension = emojiData.extension === 'svg+xml' ? 'png' : emojiData.extension; - - let matchingResults = []; - - if (emojiData._id) { - matchingResults = await EmojiCustom.findByNameOrAliasExceptID(emojiData.name, emojiData._id).toArray(); - for (const alias of aliases) { - matchingResults = matchingResults.concat(await EmojiCustom.findByNameOrAliasExceptID(alias, emojiData._id).toArray()); - } - } else { - matchingResults = await EmojiCustom.findByNameOrAlias(emojiData.name).toArray(); - for (const alias of aliases) { - matchingResults = matchingResults.concat(await EmojiCustom.findByNameOrAlias(alias).toArray()); - } - } - - if (matchingResults.length > 0) { - throw new Meteor.Error('Custom_Emoji_Error_Name_Or_Alias_Already_In_Use', 'The custom emoji or one of its aliases is already in use', { - method: 'insertOrUpdateEmoji', - }); - } - - if (typeof emojiData.extension === 'undefined') { - throw new Meteor.Error('error-the-field-is-required', 'The custom emoji file is required', { - method: 'insertOrUpdateEmoji', - }); - } - - if (!emojiData._id) { - // insert emoji - const createEmoji = { - name: emojiData.name, - aliases, - extension: emojiData.extension, - }; - - const _id = (await EmojiCustom.create(createEmoji)).insertedId; - - void api.broadcast('emoji.updateCustom', createEmoji); - - return { ...emojiData, ...createEmoji, _id }; - } - - // update emoji - if (emojiData.newFile) { - await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.extension}`)); - await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.previousExtension}`)); - await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.previousName}.${emojiData.extension}`)); - await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.previousName}.${emojiData.previousExtension}`)); - - await EmojiCustom.setExtension(emojiData._id, emojiData.extension); - } else if (emojiData.name !== emojiData.previousName) { - const rs = await RocketChatFileEmojiCustomInstance.getFileWithReadStream( - encodeURIComponent(`${emojiData.previousName}.${emojiData.previousExtension}`), - ); - - if (rs) { - await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.extension}`)); - const ws = RocketChatFileEmojiCustomInstance.createWriteStream( - encodeURIComponent(`${emojiData.name}.${emojiData.previousExtension}`), - rs.contentType, - ); - ws.on('end', () => - RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.previousName}.${emojiData.previousExtension}`)), - ); - rs.readStream.pipe(ws); - } - } - - if (emojiData.name !== emojiData.previousName) { - await EmojiCustom.setName(emojiData._id, emojiData.name); - } - - if (emojiData.aliases) { - await EmojiCustom.setAliases(emojiData._id, aliases); - } else { - await EmojiCustom.setAliases(emojiData._id, []); - } - - void api.broadcast('emoji.updateCustom', { ...emojiData, aliases }); - - return { ...emojiData, aliases } as EmojiDataWithParsedAliases; -} diff --git a/apps/meteor/app/emoji-custom/server/lib/uploadEmojiCustom.ts b/apps/meteor/app/emoji-custom/server/lib/uploadEmojiCustom.ts deleted file mode 100644 index cfa887c4a4415..0000000000000 --- a/apps/meteor/app/emoji-custom/server/lib/uploadEmojiCustom.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { api, Media } from '@rocket.chat/core-services'; -import { EmojiCustom } from '@rocket.chat/models'; -import { Random } from '@rocket.chat/random'; -import limax from 'limax'; -import { Meteor } from 'meteor/meteor'; -import sharp from 'sharp'; - -import type { EmojiData } from './insertOrUpdateEmoji'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { RocketChatFile } from '../../../file/server'; -import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom'; - -const getFile = async (file: Buffer, extension: string) => { - if (extension !== 'svg+xml') { - return file; - } - - return sharp(file).png().toBuffer(); -}; - -export type EmojiDataWithAliases = Omit & { aliases?: string | string[] }; - -export async function uploadEmojiCustom( - userId: string | null, - binaryContent: string, - contentType: string, - emojiData: EmojiDataWithAliases, -) { - return uploadEmojiCustomWithBuffer(userId, Buffer.from(binaryContent, 'binary'), contentType, emojiData); -} - -export async function uploadEmojiCustomWithBuffer( - userId: string | null, - buffer: Buffer, - contentType: string, - emojiData: EmojiDataWithAliases, -): Promise { - // technically, since this method doesnt have any datatype validations, users can - // upload videos as emojis. The FE won't play them, but they will waste space for sure. - if (!userId || !(await hasPermissionAsync(userId, 'manage-emoji'))) { - throw new Meteor.Error('not_authorized'); - } - - if (!Array.isArray(emojiData.aliases)) { - // delete aliases for notification purposes. here, it is a string or undefined rather than an array - delete emojiData.aliases; - } - - emojiData.name = limax(emojiData.name, { replacement: '_' }); - - const file = await getFile(buffer, emojiData.extension); - emojiData.extension = emojiData.extension === 'svg+xml' ? 'png' : emojiData.extension; - - let fileBuffer; - // sharp doesn't support these formats without imagemagick or libvips installed - // so they will be stored as they are :( - if (['gif', 'x-icon', 'bmp', 'webm'].includes(emojiData.extension)) { - fileBuffer = file; - } else { - // This is to support the idea of having "sticker-like" emojis - const { data: resizedEmojiBuffer } = await Media.resizeFromBuffer(file, 512, 512, true, false, false, 'inside'); - fileBuffer = resizedEmojiBuffer; - } - - const rs = RocketChatFile.bufferToStream(fileBuffer); - await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.extension}`)); - - return new Promise((resolve) => { - const ws = RocketChatFileEmojiCustomInstance.createWriteStream( - encodeURIComponent(`${emojiData.name}.${emojiData.extension}`), - contentType, - ); - ws.on('end', async () => { - const etag = Random.hexString(6); - await EmojiCustom.setETagByName(emojiData.name, etag); - setTimeout(() => api.broadcast('emoji.updateCustom', { ...emojiData, etag }), 500); - resolve(); - }); - - rs.pipe(ws); - }); -} diff --git a/apps/meteor/app/emoji-custom/server/methods/insertOrUpdateEmoji.ts b/apps/meteor/app/emoji-custom/server/methods/insertOrUpdateEmoji.ts deleted file mode 100644 index ed3bc693bac05..0000000000000 --- a/apps/meteor/app/emoji-custom/server/methods/insertOrUpdateEmoji.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { insertOrUpdateEmoji } from '../lib/insertOrUpdateEmoji'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - insertOrUpdateEmoji(emojiData: { - _id?: string; - name: string; - aliases: string; - extension: string; - previousName?: string; - previousExtension?: string; - newFile?: boolean; - }): string | boolean; - } -} - -Meteor.methods({ - async insertOrUpdateEmoji(emojiData) { - methodDeprecationLogger.method('insertOrUpdateEmoji', '9.0.0', ['/v1/emoji-custom.create', '/v1/emoji-custom.update']); - const emoji = await insertOrUpdateEmoji(this.userId, emojiData); - - if (!emojiData._id) { - return emoji._id; - } - - return !!emoji; - }, -}); diff --git a/apps/meteor/app/emoji-custom/server/methods/uploadEmojiCustom.ts b/apps/meteor/app/emoji-custom/server/methods/uploadEmojiCustom.ts deleted file mode 100644 index 91e6ed8270265..0000000000000 --- a/apps/meteor/app/emoji-custom/server/methods/uploadEmojiCustom.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { uploadEmojiCustom } from '../lib/uploadEmojiCustom'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - uploadEmojiCustom( - binaryContent: string, - contentType: string, - emojiData: { - name: string; - aliases?: string; - extension: string; - }, - ): void; - } -} - -Meteor.methods({ - async uploadEmojiCustom(binaryContent, contentType, emojiData) { - methodDeprecationLogger.method('uploadEmojiCustom', '9.0.0', '/v1/emoji-custom.create'); - await uploadEmojiCustom(this.userId, binaryContent, contentType, emojiData); - }, -}); diff --git a/apps/meteor/app/emoji-emojione/.gitignore b/apps/meteor/app/emoji-emojione/.gitignore deleted file mode 100644 index b458e2b7222cd..0000000000000 --- a/apps/meteor/app/emoji-emojione/.gitignore +++ /dev/null @@ -1 +0,0 @@ -emoji.json diff --git a/apps/meteor/app/emoji-emojione/README.md b/apps/meteor/app/emoji-emojione/README.md deleted file mode 100644 index d16237b6fc085..0000000000000 --- a/apps/meteor/app/emoji-emojione/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Updating emojione - -## Generate new category map variable for emojipicker -Run -``` -node --experimental-modules generateEmojiIndex.mjs -``` - -## Generate new percentage sprite -Clone the repository https://github.com/Ranks/emojione/ and replace the file `assets/sprites/emojione.sprites.mustache` with the content -of [emojione.sprites.mustache](emojione.sprites.mustache), then run at `emojione` folder: - -``` -grunt sprite -sass --sourcemap=none assets/sprites/emojione.sprites.scss sprites.css -``` - -And replace the file `sprites.css` at Rocket.Chat's `/packages/rocketchat-emoji-emojione/sprites.css`. diff --git a/apps/meteor/app/emoji-emojione/client/activity-sprites.css b/apps/meteor/app/emoji-emojione/client/activity-sprites.css deleted file mode 100644 index 9af3e23b46d7a..0000000000000 --- a/apps/meteor/app/emoji-emojione/client/activity-sprites.css +++ /dev/null @@ -1,2177 +0,0 @@ -/* stylelint-disable */ - -.emojione-activity { - background-image: url('/packages/emojione/activity-sprites.png'); - - &._26bd { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 0%; - } - - &._1f3c8 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 5.882352941176471%; - } - - &._26be { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 5.882352941176471%; - } - - &._1f94e { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 0%; - } - - &._1f3be { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 5.882352941176471%; - } - - &._1f3d0 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 11.764705882352942%; - } - - &._1f3c9 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 11.764705882352942%; - } - - &._1f3b1 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 11.764705882352942%; - } - - &._1f3d3 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 0%; - } - - &._1f3f8 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 5.882352941176471%; - } - - &._1f945 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 11.764705882352942%; - } - - &._1f3d2 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 17.647058823529413%; - } - - &._1f3d1 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 17.647058823529413%; - } - - &._1f3cf { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 17.647058823529413%; - } - - &._1f94d { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 17.647058823529413%; - } - - &._26f3 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 0%; - } - - &._1f94f { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 5.882352941176471%; - } - - &._1f3f9 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 11.764705882352942%; - } - - &._1f3a3 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 17.647058823529413%; - } - - &._1f94a { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 23.529411764705884%; - } - - &._1f94b { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 23.529411764705884%; - } - - &._1f3bd { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 23.529411764705884%; - } - - &._1f6f9 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 23.529411764705884%; - } - - &._26f8 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 23.529411764705884%; - } - - &._1f94c { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 0%; - } - - &._1f6f7 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 5.882352941176471%; - } - - &._1f3bf { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 11.764705882352942%; - } - - &._26f7 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 17.647058823529413%; - } - - &._1f3c2 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 23.529411764705884%; - } - - &._1f3cb { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 29.41176470588235%; - } - - &._1f3cb-2640 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 29.41176470588235%; - } - - &._1f3cb-2642 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 35.294117647058826%; - } - - &._1f93c { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 23.529411764705884%; - } - - &._1f93c-2640 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 29.41176470588235%; - } - - &._1f93c-2642 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 35.294117647058826%; - } - - &._1f938 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 41.1764705882353%; - } - - &._1f938-2640 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 41.1764705882353%; - } - - &._1f938-2642 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 23.529411764705884%; - } - - &._26f9 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 47.05882352941177%; - } - - &._26f9-2640 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 47.05882352941177%; - } - - &._26f9-2642 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 29.41176470588235%; - } - - &._1f93a { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 52.94117647058823%; - } - - &._1f93e { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 52.94117647058823%; - } - - &._1f93e-2640 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 52.94117647058823%; - } - - &._1f93e-2642 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 29.41176470588235%; - } - - &._1f3cc { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 58.8235294117647%; - } - - &._1f3cc-2640 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 58.8235294117647%; - } - - &._1f3cc-2642 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 11.764705882352942%; - } - - &._1f3c7 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 47.05882352941177%; - } - - &._1f9d8 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 64.70588235294117%; - } - - &._1f9d8-2640 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 64.70588235294117%; - } - - &._1f9d8-2642 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 17.647058823529413%; - } - - &._1f3c4 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 52.94117647058823%; - } - - &._1f3c4-2640 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 70.58823529411765%; - } - - &._1f3c0 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 70.58823529411765%; - } - - &._1f3c4-2642 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 70.58823529411765%; - } - - &._1f3ca { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 11.764705882352942%; - } - - &._1f3ca-2640 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 47.05882352941177%; - } - - &._1f3ca-2642 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 76.47058823529412%; - } - - &._1f93d { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 76.47058823529412%; - } - - &._1f93d-2640 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 76.47058823529412%; - } - - &._1f93d-2642 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 29.41176470588235%; - } - - &._1f6a3 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 64.70588235294117%; - } - - &._1f6a3-2640 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 82.3529411764706%; - } - - &._1f6a3-2642 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 82.3529411764706%; - } - - &._1f9d7 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 0%; - } - - &._1f9d7-2640 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 35.294117647058826%; - } - - &._1f9d7-2642 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 70.58823529411765%; - } - - &._1f6b5 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 88.23529411764706%; - } - - &._1f6b5-2640 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 88.23529411764706%; - } - - &._1f6b5-2642 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 88.23529411764706%; - } - - &._1f6b4 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 29.41176470588235%; - } - - &._1f6b4-2640 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 64.70588235294117%; - } - - &._1f6b4-2642 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 94.11764705882354%; - } - - &._1f3c6 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 94.11764705882354%; - } - - &._1f947 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 94.11764705882354%; - } - - &._1f948 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 94.11764705882354%; - } - - &._1f949 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 94.11764705882354%; - } - - &._1f3c5 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 94.11764705882354%; - } - - &._1f396 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 94.11764705882354%; - } - - &._1f3f5 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 94.11764705882354%; - } - - &._1f397 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 94.11764705882354%; - } - - &._1f3ab { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 94.11764705882354%; - } - - &._1f39f { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 94.11764705882354%; - } - - &._1f3aa { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 0%; - } - - &._1f939 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 5.882352941176471%; - } - - &._1f939-2640 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 41.1764705882353%; - } - - &._1f939-2642 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 76.47058823529412%; - } - - &._1f3ad { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 100%; - } - - &._1f3a8 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 100%; - } - - &._1f3ac { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 100%; - } - - &._1f3a4 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 100%; - } - - &._1f3a7 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 100%; - } - - &._1f3bc { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 100%; - } - - &._1f3b9 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 100%; - } - - &._1f941 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 100%; - } - - &._1f3b7 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 100%; - } - - &._1f3ba { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 100%; - } - - &._1f3b8 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 100%; - } - - &._1f3bb { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 100%; - } - - &._1f3b2 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 100%; - } - - &._1f3af { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 100%; - } - - &._1f3b3 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 100%; - } - - &._1f3ae { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 100%; - } - - &._1f3b0 { - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 100% 0%; - } - -} - - -.emojione-diversity._1f3c4-1f3fd-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 0%; -} - -.emojione-diversity._1f3c2-1f3fb { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 29.41176470588235%; -} - -.emojione-diversity._1f3c2-1f3fc { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 29.41176470588235%; -} - -.emojione-diversity._1f3c2-1f3fd { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 29.41176470588235%; -} - -.emojione-diversity._1f3c2-1f3fe { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 29.41176470588235%; -} - -.emojione-diversity._1f3c2-1f3ff { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 29.41176470588235%; -} - -.emojione-diversity._1f3cb-1f3fb { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 0%; -} - -.emojione-diversity._1f3cb-1f3fc { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 5.882352941176471%; -} - -.emojione-diversity._1f3cb-1f3fd { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 11.764705882352942%; -} - -.emojione-diversity._1f3cb-1f3fe { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 17.647058823529413%; -} - -.emojione-diversity._1f3cb-1f3ff { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 23.529411764705884%; -} - -.emojione-diversity._1f3cb-1f3fb-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 35.294117647058826%; -} - -.emojione-diversity._1f3cb-1f3fc-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 35.294117647058826%; -} - -.emojione-diversity._1f3cb-1f3fd-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 35.294117647058826%; -} - -.emojione-diversity._1f3cb-1f3fe-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 35.294117647058826%; -} - -.emojione-diversity._1f3cb-1f3ff-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 35.294117647058826%; -} - -.emojione-diversity._1f3cb-1f3fb-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 35.294117647058826%; -} - -.emojione-diversity._1f3cb-1f3fc-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 0%; -} - -.emojione-diversity._1f3cb-1f3fd-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 5.882352941176471%; -} - -.emojione-diversity._1f3cb-1f3fe-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 11.764705882352942%; -} - -.emojione-diversity._1f3cb-1f3ff-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 17.647058823529413%; -} - -.emojione-diversity._1f938-1f3fb { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 41.1764705882353%; -} - -.emojione-diversity._1f938-1f3fc { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 41.1764705882353%; -} - -.emojione-diversity._1f938-1f3fd { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 41.1764705882353%; -} - -.emojione-diversity._1f938-1f3fe { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 41.1764705882353%; -} - -.emojione-diversity._1f938-1f3ff { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 41.1764705882353%; -} - -.emojione-diversity._1f938-1f3fb-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 41.1764705882353%; -} - -.emojione-diversity._1f938-1f3fc-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 0%; -} - -.emojione-diversity._1f938-1f3fd-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 5.882352941176471%; -} - -.emojione-diversity._1f938-1f3fe-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 11.764705882352942%; -} - -.emojione-diversity._1f938-1f3ff-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 17.647058823529413%; -} - -.emojione-diversity._1f938-1f3fb-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 29.41176470588235%; -} - -.emojione-diversity._1f938-1f3fc-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 35.294117647058826%; -} - -.emojione-diversity._1f938-1f3fd-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 41.1764705882353%; -} - -.emojione-diversity._1f938-1f3fe-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 47.05882352941177%; -} - -.emojione-diversity._1f938-1f3ff-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 47.05882352941177%; -} - -.emojione-diversity._26f9-1f3fb { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 47.05882352941177%; -} - -.emojione-diversity._26f9-1f3fc { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 47.05882352941177%; -} - -.emojione-diversity._26f9-1f3fd { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 47.05882352941177%; -} - -.emojione-diversity._26f9-1f3fe { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 47.05882352941177%; -} - -.emojione-diversity._26f9-1f3ff { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 47.05882352941177%; -} - -.emojione-diversity._26f9-1f3fb-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 0%; -} - -.emojione-diversity._26f9-1f3fc-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 5.882352941176471%; -} - -.emojione-diversity._26f9-1f3fd-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 11.764705882352942%; -} - -.emojione-diversity._26f9-1f3fe-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 17.647058823529413%; -} - -.emojione-diversity._26f9-1f3ff-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 23.529411764705884%; -} - -.emojione-diversity._26f9-1f3fb-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 35.294117647058826%; -} - -.emojione-diversity._26f9-1f3fc-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 41.1764705882353%; -} - -.emojione-diversity._26f9-1f3fd-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 47.05882352941177%; -} - -.emojione-diversity._26f9-1f3fe-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 52.94117647058823%; -} - -.emojione-diversity._26f9-1f3ff-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 52.94117647058823%; -} - -.emojione-diversity._1f93e-1f3fb { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 52.94117647058823%; -} - -.emojione-diversity._1f93e-1f3fc { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 52.94117647058823%; -} - -.emojione-diversity._1f93e-1f3fd { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 52.94117647058823%; -} - -.emojione-diversity._1f93e-1f3fe { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 52.94117647058823%; -} - -.emojione-diversity._1f93e-1f3ff { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 52.94117647058823%; -} - -.emojione-diversity._1f93e-1f3fb-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 0%; -} - -.emojione-diversity._1f93e-1f3fc-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 5.882352941176471%; -} - -.emojione-diversity._1f93e-1f3fd-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 11.764705882352942%; -} - -.emojione-diversity._1f93e-1f3fe-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 17.647058823529413%; -} - -.emojione-diversity._1f93e-1f3ff-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 23.529411764705884%; -} - -.emojione-diversity._1f93e-1f3fb-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 35.294117647058826%; -} - -.emojione-diversity._1f93e-1f3fc-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 41.1764705882353%; -} - -.emojione-diversity._1f93e-1f3fd-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 47.05882352941177%; -} - -.emojione-diversity._1f93e-1f3fe-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 52.94117647058823%; -} - -.emojione-diversity._1f93e-1f3ff-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 58.8235294117647%; -} - -.emojione-diversity._1f3cc-1f3fb { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 58.8235294117647%; -} - -.emojione-diversity._1f3cc-1f3fc { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 58.8235294117647%; -} - -.emojione-diversity._1f3cc-1f3fd { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 58.8235294117647%; -} - -.emojione-diversity._1f3cc-1f3fe { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 58.8235294117647%; -} - -.emojione-diversity._1f3cc-1f3ff { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 58.8235294117647%; -} - -.emojione-diversity._1f3cc-1f3fb-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 58.8235294117647%; -} - -.emojione-diversity._1f3cc-1f3fc-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 58.8235294117647%; -} - -.emojione-diversity._1f3cc-1f3fd-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 58.8235294117647%; -} - -.emojione-diversity._1f3cc-1f3fe-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 0%; -} - -.emojione-diversity._1f3cc-1f3ff-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 5.882352941176471%; -} - -.emojione-diversity._1f3cc-1f3fb-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 17.647058823529413%; -} - -.emojione-diversity._1f3cc-1f3fc-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 23.529411764705884%; -} - -.emojione-diversity._1f3cc-1f3fd-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 29.41176470588235%; -} - -.emojione-diversity._1f3cc-1f3fe-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 35.294117647058826%; -} - -.emojione-diversity._1f3cc-1f3ff-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 41.1764705882353%; -} - -.emojione-diversity._1f3c7-1f3fb { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 52.94117647058823%; -} - -.emojione-diversity._1f3c7-1f3fc { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 58.8235294117647%; -} - -.emojione-diversity._1f3c7-1f3fd { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 64.70588235294117%; -} - -.emojione-diversity._1f3c7-1f3fe { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 64.70588235294117%; -} - -.emojione-diversity._1f3c7-1f3ff { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 64.70588235294117%; -} - -.emojione-diversity._1f9d8-1f3fb { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 64.70588235294117%; -} - -.emojione-diversity._1f9d8-1f3fc { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 64.70588235294117%; -} - -.emojione-diversity._1f9d8-1f3fd { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 64.70588235294117%; -} - -.emojione-diversity._1f9d8-1f3fe { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 64.70588235294117%; -} - -.emojione-diversity._1f9d8-1f3ff { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 64.70588235294117%; -} - -.emojione-diversity._1f9d8-1f3fb-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 64.70588235294117%; -} - -.emojione-diversity._1f9d8-1f3fc-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 64.70588235294117%; -} - -.emojione-diversity._1f9d8-1f3fd-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 0%; -} - -.emojione-diversity._1f9d8-1f3fe-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 5.882352941176471%; -} - -.emojione-diversity._1f9d8-1f3ff-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 11.764705882352942%; -} - -.emojione-diversity._1f9d8-1f3fb-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 23.529411764705884%; -} - -.emojione-diversity._1f9d8-1f3fc-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 29.41176470588235%; -} - -.emojione-diversity._1f9d8-1f3fd-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 35.294117647058826%; -} - -.emojione-diversity._1f9d8-1f3fe-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 41.1764705882353%; -} - -.emojione-diversity._1f9d8-1f3ff-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 47.05882352941177%; -} - -.emojione-diversity._1f3c4-1f3fb { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 58.8235294117647%; -} - -.emojione-diversity._1f3c4-1f3fc { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 64.70588235294117%; -} - -.emojione-diversity._1f3c4-1f3fd { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 70.58823529411765%; -} - -.emojione-diversity._1f3c4-1f3fe { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 70.58823529411765%; -} - -.emojione-diversity._1f3c4-1f3ff { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 70.58823529411765%; -} - -.emojione-diversity._1f3c4-1f3fb-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 70.58823529411765%; -} - -.emojione-diversity._1f3c4-1f3fc-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 70.58823529411765%; -} - -.emojione-diversity._1f3c4-1f3fe-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 70.58823529411765%; -} - -.emojione-diversity._1f3c4-1f3ff-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 70.58823529411765%; -} - -.emojione-diversity._1f3c4-1f3fb-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 70.58823529411765%; -} - -.emojione-diversity._1f3c4-1f3fc-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 70.58823529411765%; -} - -.emojione-diversity._1f3c4-1f3fd-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 70.58823529411765%; -} - -.emojione-diversity._1f3c4-1f3fe-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 0%; -} - -.emojione-diversity._1f3c4-1f3ff-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 5.882352941176471%; -} - -.emojione-diversity._1f3ca-1f3fb { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 17.647058823529413%; -} - -.emojione-diversity._1f3ca-1f3fc { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 23.529411764705884%; -} - -.emojione-diversity._1f3ca-1f3fd { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 29.41176470588235%; -} - -.emojione-diversity._1f3ca-1f3fe { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 35.294117647058826%; -} - -.emojione-diversity._1f3ca-1f3ff { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 41.1764705882353%; -} - -.emojione-diversity._1f3ca-1f3fb-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 52.94117647058823%; -} - -.emojione-diversity._1f3ca-1f3fc-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 58.8235294117647%; -} - -.emojione-diversity._1f3ca-1f3fd-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 64.70588235294117%; -} - -.emojione-diversity._1f3ca-1f3fe-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 70.58823529411765%; -} - -.emojione-diversity._1f3ca-1f3ff-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 76.47058823529412%; -} - -.emojione-diversity._1f3ca-1f3fb-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 76.47058823529412%; -} - -.emojione-diversity._1f3ca-1f3fc-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 76.47058823529412%; -} - -.emojione-diversity._1f3ca-1f3fd-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 76.47058823529412%; -} - -.emojione-diversity._1f3ca-1f3fe-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 76.47058823529412%; -} - -.emojione-diversity._1f3ca-1f3ff-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 76.47058823529412%; -} - -.emojione-diversity._1f93d-1f3fb { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 76.47058823529412%; -} - -.emojione-diversity._1f93d-1f3fc { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 50% 76.47058823529412%; -} - -.emojione-diversity._1f93d-1f3fd { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 76.47058823529412%; -} - -.emojione-diversity._1f93d-1f3fe { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 76.47058823529412%; -} - -.emojione-diversity._1f93d-1f3ff { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 76.47058823529412%; -} - -.emojione-diversity._1f93d-1f3fb-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 0%; -} - -.emojione-diversity._1f93d-1f3fc-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 5.882352941176471%; -} - -.emojione-diversity._1f93d-1f3fd-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 11.764705882352942%; -} - -.emojione-diversity._1f93d-1f3fe-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 17.647058823529413%; -} - -.emojione-diversity._1f93d-1f3ff-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 23.529411764705884%; -} - -.emojione-diversity._1f93d-1f3fb-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 35.294117647058826%; -} - -.emojione-diversity._1f93d-1f3fc-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 41.1764705882353%; -} - -.emojione-diversity._1f93d-1f3fd-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 47.05882352941177%; -} - -.emojione-diversity._1f93d-1f3fe-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 52.94117647058823%; -} - -.emojione-diversity._1f93d-1f3ff-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 58.8235294117647%; -} - -.emojione-diversity._1f6a3-1f3fb { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 70.58823529411765%; -} - -.emojione-diversity._1f6a3-1f3fc { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 76.47058823529412%; -} - -.emojione-diversity._1f6a3-1f3fd { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 82.3529411764706%; -} - -.emojione-diversity._1f6a3-1f3fe { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 82.3529411764706%; -} - -.emojione-diversity._1f6a3-1f3ff { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 82.3529411764706%; -} - -.emojione-diversity._1f6a3-1f3fb-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 82.3529411764706%; -} - -.emojione-diversity._1f6a3-1f3fc-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 82.3529411764706%; -} - -.emojione-diversity._1f6a3-1f3fd-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 82.3529411764706%; -} - -.emojione-diversity._1f6a3-1f3fe-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 82.3529411764706%; -} - -.emojione-diversity._1f6a3-1f3ff-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 82.3529411764706%; -} - -.emojione-diversity._1f6a3-1f3fb-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 82.3529411764706%; -} - -.emojione-diversity._1f6a3-1f3fc-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 82.3529411764706%; -} - -.emojione-diversity._1f6a3-1f3fd-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 82.3529411764706%; -} - -.emojione-diversity._1f6a3-1f3fe-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 82.3529411764706%; -} - -.emojione-diversity._1f6a3-1f3ff-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 82.3529411764706%; -} - -.emojione-diversity._1f9d7-1f3fb { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 5.882352941176471%; -} - -.emojione-diversity._1f9d7-1f3fc { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 11.764705882352942%; -} - -.emojione-diversity._1f9d7-1f3fd { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 17.647058823529413%; -} - -.emojione-diversity._1f9d7-1f3fe { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 23.529411764705884%; -} - -.emojione-diversity._1f9d7-1f3ff { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 29.41176470588235%; -} - -.emojione-diversity._1f9d7-1f3fb-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 41.1764705882353%; -} - -.emojione-diversity._1f9d7-1f3fc-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 47.05882352941177%; -} - -.emojione-diversity._1f9d7-1f3fd-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 52.94117647058823%; -} - -.emojione-diversity._1f9d7-1f3fe-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 58.8235294117647%; -} - -.emojione-diversity._1f9d7-1f3ff-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 64.70588235294117%; -} - -.emojione-diversity._1f9d7-1f3fb-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 76.47058823529412%; -} - -.emojione-diversity._1f9d7-1f3fc-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 83.33333333333333% 82.3529411764706%; -} - -.emojione-diversity._1f9d7-1f3fd-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 88.23529411764706%; -} - -.emojione-diversity._1f9d7-1f3fe-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 88.23529411764706%; -} - -.emojione-diversity._1f9d7-1f3ff-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 88.23529411764706%; -} - -.emojione-diversity._1f6b5-1f3fb { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 88.23529411764706%; -} - -.emojione-diversity._1f6b5-1f3fc { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 88.23529411764706%; -} - -.emojione-diversity._1f6b5-1f3fd { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 88.23529411764706%; -} - -.emojione-diversity._1f6b5-1f3fe { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 38.888888888888886% 88.23529411764706%; -} - -.emojione-diversity._1f6b5-1f3ff { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 44.44444444444444% 88.23529411764706%; -} - -.emojione-diversity._1f6b5-1f3fb-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 55.55555555555556% 88.23529411764706%; -} - -.emojione-diversity._1f6b5-1f3fc-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 61.111111111111114% 88.23529411764706%; -} - -.emojione-diversity._1f6b5-1f3fd-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 66.66666666666667% 88.23529411764706%; -} - -.emojione-diversity._1f6b5-1f3fe-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 72.22222222222223% 88.23529411764706%; -} - -.emojione-diversity._1f6b5-1f3ff-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 77.77777777777777% 88.23529411764706%; -} - -.emojione-diversity._1f6b5-1f3fb-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 0%; -} - -.emojione-diversity._1f6b5-1f3fc-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 5.882352941176471%; -} - -.emojione-diversity._1f6b5-1f3fd-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 11.764705882352942%; -} - -.emojione-diversity._1f6b5-1f3fe-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 17.647058823529413%; -} - -.emojione-diversity._1f6b5-1f3ff-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 23.529411764705884%; -} - -.emojione-diversity._1f6b4-1f3fb { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 35.294117647058826%; -} - -.emojione-diversity._1f6b4-1f3fc { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 41.1764705882353%; -} - -.emojione-diversity._1f6b4-1f3fd { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 47.05882352941177%; -} - -.emojione-diversity._1f6b4-1f3fe { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 52.94117647058823%; -} - -.emojione-diversity._1f6b4-1f3ff { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 58.8235294117647%; -} - -.emojione-diversity._1f6b4-1f3fb-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 70.58823529411765%; -} - -.emojione-diversity._1f6b4-1f3fc-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 76.47058823529412%; -} - -.emojione-diversity._1f6b4-1f3fd-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 82.3529411764706%; -} - -.emojione-diversity._1f6b4-1f3fe-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 88.88888888888889% 88.23529411764706%; -} - -.emojione-diversity._1f6b4-1f3ff-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 94.11764705882354%; -} - -.emojione-diversity._1f6b4-1f3fb-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 11.11111111111111% 94.11764705882354%; -} - -.emojione-diversity._1f6b4-1f3fc-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 16.666666666666668% 94.11764705882354%; -} - -.emojione-diversity._1f6b4-1f3fd-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 22.22222222222222% 94.11764705882354%; -} - -.emojione-diversity._1f6b4-1f3fe-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 27.77777777777778% 94.11764705882354%; -} - -.emojione-diversity._1f6b4-1f3ff-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 33.333333333333336% 94.11764705882354%; -} - -.emojione-diversity._1f939-1f3fb { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 11.764705882352942%; -} - -.emojione-diversity._1f939-1f3fc { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 17.647058823529413%; -} - -.emojione-diversity._1f939-1f3fd { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 23.529411764705884%; -} - -.emojione-diversity._1f939-1f3fe { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 29.41176470588235%; -} - -.emojione-diversity._1f939-1f3ff { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 35.294117647058826%; -} - -.emojione-diversity._1f939-1f3fb-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 47.05882352941177%; -} - -.emojione-diversity._1f939-1f3fc-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 52.94117647058823%; -} - -.emojione-diversity._1f939-1f3fd-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 58.8235294117647%; -} - -.emojione-diversity._1f939-1f3fe-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 64.70588235294117%; -} - -.emojione-diversity._1f939-1f3ff-2640 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 70.58823529411765%; -} - -.emojione-diversity._1f939-1f3fb-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 82.3529411764706%; -} - -.emojione-diversity._1f939-1f3fc-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 88.23529411764706%; -} - -.emojione-diversity._1f939-1f3fd-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 94.44444444444444% 94.11764705882354%; -} - -.emojione-diversity._1f939-1f3fe-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 0% 100%; -} - -.emojione-diversity._1f939-1f3ff-2642 { - background-image: url('/packages/emojione/activity-sprites.png'); - background-repeat: no-repeat; - background-size: 1900% 1800%; - background-position: 5.555555555555555% 100%; -} diff --git a/apps/meteor/app/emoji-emojione/client/emojione-sprites.css b/apps/meteor/app/emoji-emojione/client/emojione-sprites.css deleted file mode 100644 index eb6d29ef768c9..0000000000000 --- a/apps/meteor/app/emoji-emojione/client/emojione-sprites.css +++ /dev/null @@ -1,30 +0,0 @@ -@import './symbols-sprites.css'; -@import './activity-sprites.css'; -@import './objects-sprites.css'; -@import './nature-sprites.css'; -@import './food-sprites.css'; -@import './people-sprites.css'; -@import './regional-sprites.css'; -@import './travel-sprites.css'; -@import './flags-sprites.css'; -@import './modifier-sprites.css'; - -.emojione { - position: relative; - - display: inline-block; - overflow: hidden; - - width: 1.375rem; - height: 1.375rem; - margin: 0 0.15em; - - vertical-align: middle; - white-space: nowrap; - text-indent: 100%; - - font-size: inherit; - line-height: normal; - image-rendering: -webkit-optimize-contrast; - image-rendering: optimizeQuality; -} diff --git a/apps/meteor/app/emoji-emojione/client/flags-sprites.css b/apps/meteor/app/emoji-emojione/client/flags-sprites.css deleted file mode 100644 index 63a716591b2dc..0000000000000 --- a/apps/meteor/app/emoji-emojione/client/flags-sprites.css +++ /dev/null @@ -1,1615 +0,0 @@ -/* stylelint-disable */ - -.emojione-flags { - background-image: url('/packages/emojione/flags-sprites.png'); - - &._1f1f1-1f1f9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 0% 0%; - } - - &._1f3f3 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 6.25% 0%; - } - - &._1f3c1 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 0% 6.666666666666667%; - } - - &._1f6a9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 6.25% 6.666666666666667%; - } - - &._1f3f3-1f308 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 12.5% 0%; - } - - &._1f3f4-2620 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 12.5% 6.666666666666667%; - } - - &._1f1e6-1f1eb { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 0% 13.333333333333334%; - } - - &._1f1e6-1f1fd { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 6.25% 13.333333333333334%; - } - - &._1f1e6-1f1f1 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 12.5% 13.333333333333334%; - } - - &._1f1e9-1f1ff { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 18.75% 0%; - } - - &._1f1e6-1f1f8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 18.75% 6.666666666666667%; - } - - &._1f1e6-1f1e9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 18.75% 13.333333333333334%; - } - - &._1f1e6-1f1f4 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 0% 20%; - } - - &._1f1e6-1f1ee { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 6.25% 20%; - } - - &._1f1e6-1f1f6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 12.5% 20%; - } - - &._1f1e6-1f1ec { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 18.75% 20%; - } - - &._1f1e6-1f1f7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 25% 0%; - } - - &._1f1e6-1f1f2 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 25% 6.666666666666667%; - } - - &._1f1e6-1f1fc { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 25% 13.333333333333334%; - } - - &._1f1e6-1f1fa { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 25% 20%; - } - - &._1f1e6-1f1f9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 0% 26.666666666666668%; - } - - &._1f1e6-1f1ff { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 6.25% 26.666666666666668%; - } - - &._1f1e7-1f1f8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 12.5% 26.666666666666668%; - } - - &._1f1e7-1f1ed { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 18.75% 26.666666666666668%; - } - - &._1f1e7-1f1e9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 25% 26.666666666666668%; - } - - &._1f1e7-1f1e7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 31.25% 0%; - } - - &._1f1e7-1f1fe { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 31.25% 6.666666666666667%; - } - - &._1f1e7-1f1ea { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 31.25% 13.333333333333334%; - } - - &._1f1e7-1f1ff { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 31.25% 20%; - } - - &._1f1e7-1f1ef { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 31.25% 26.666666666666668%; - } - - &._1f1e7-1f1f2 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 0% 33.333333333333336%; - } - - &._1f1e7-1f1f9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 6.25% 33.333333333333336%; - } - - &._1f1e7-1f1f4 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 12.5% 33.333333333333336%; - } - - &._1f1e7-1f1e6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 18.75% 33.333333333333336%; - } - - &._1f1e7-1f1fc { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 25% 33.333333333333336%; - } - - &._1f1e7-1f1f7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 31.25% 33.333333333333336%; - } - - &._1f1ee-1f1f4 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 37.5% 0%; - } - - &._1f1fb-1f1ec { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 37.5% 6.666666666666667%; - } - - &._1f1e7-1f1f3 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 37.5% 13.333333333333334%; - } - - &._1f1e7-1f1ec { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 37.5% 20%; - } - - &._1f1e7-1f1eb { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 37.5% 26.666666666666668%; - } - - &._1f1e7-1f1ee { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 37.5% 33.333333333333336%; - } - - &._1f1f0-1f1ed { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 0% 40%; - } - - &._1f1e8-1f1f2 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 6.25% 40%; - } - - &._1f1e8-1f1e6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 12.5% 40%; - } - - &._1f1ee-1f1e8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 18.75% 40%; - } - - &._1f1e8-1f1fb { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 25% 40%; - } - - &._1f1e7-1f1f6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 31.25% 40%; - } - - &._1f1f0-1f1fe { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 37.5% 40%; - } - - &._1f1e8-1f1eb { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 43.75% 0%; - } - - &._1f1f9-1f1e9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 43.75% 6.666666666666667%; - } - - &._1f1e8-1f1f1 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 43.75% 13.333333333333334%; - } - - &._1f1e8-1f1f3 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 43.75% 20%; - } - - &._1f1e8-1f1fd { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 43.75% 26.666666666666668%; - } - - &._1f1e8-1f1e8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 43.75% 33.333333333333336%; - } - - &._1f1e8-1f1f4 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 43.75% 40%; - } - - &._1f1f0-1f1f2 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 0% 46.666666666666664%; - } - - &._1f1e8-1f1ec { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 6.25% 46.666666666666664%; - } - - &._1f1e8-1f1e9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 12.5% 46.666666666666664%; - } - - &._1f1e8-1f1f0 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 18.75% 46.666666666666664%; - } - - &._1f1e8-1f1f7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 25% 46.666666666666664%; - } - - &._1f1e8-1f1ee { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 31.25% 46.666666666666664%; - } - - &._1f1ed-1f1f7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 37.5% 46.666666666666664%; - } - - &._1f1e8-1f1fa { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 43.75% 46.666666666666664%; - } - - &._1f1e8-1f1fc { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 50% 0%; - } - - &._1f1e8-1f1fe { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 50% 6.666666666666667%; - } - - &._1f1e8-1f1ff { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 50% 13.333333333333334%; - } - - &._1f1e9-1f1f0 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 50% 20%; - } - - &._1f1e9-1f1ef { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 50% 26.666666666666668%; - } - - &._1f1e9-1f1f2 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 50% 33.333333333333336%; - } - - &._1f1e9-1f1f4 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 50% 40%; - } - - &._1f1ea-1f1e8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 50% 46.666666666666664%; - } - - &._1f1ea-1f1ec { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 0% 53.333333333333336%; - } - - &._1f1f8-1f1fb { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 6.25% 53.333333333333336%; - } - - &._1f1ec-1f1f6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 12.5% 53.333333333333336%; - } - - &._1f1ea-1f1f7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 18.75% 53.333333333333336%; - } - - &._1f1ea-1f1ea { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 25% 53.333333333333336%; - } - - &._1f1ea-1f1f9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 31.25% 53.333333333333336%; - } - - &._1f1ea-1f1fa { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 37.5% 53.333333333333336%; - } - - &._1f1eb-1f1f0 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 43.75% 53.333333333333336%; - } - - &._1f1eb-1f1f4 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 50% 53.333333333333336%; - } - - &._1f1eb-1f1ef { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 56.25% 0%; - } - - &._1f1eb-1f1ee { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 56.25% 6.666666666666667%; - } - - &._1f1eb-1f1f7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 56.25% 13.333333333333334%; - } - - &._1f1ec-1f1eb { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 56.25% 20%; - } - - &._1f1f5-1f1eb { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 56.25% 26.666666666666668%; - } - - &._1f1f9-1f1eb { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 56.25% 33.333333333333336%; - } - - &._1f1ec-1f1e6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 56.25% 40%; - } - - &._1f1ec-1f1f2 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 56.25% 46.666666666666664%; - } - - &._1f1ec-1f1ea { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 56.25% 53.333333333333336%; - } - - &._1f1e9-1f1ea { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 0% 60%; - } - - &._1f1ec-1f1ed { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 6.25% 60%; - } - - &._1f1ec-1f1ee { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 12.5% 60%; - } - - &._1f1ec-1f1f7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 18.75% 60%; - } - - &._1f1ec-1f1f1 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 25% 60%; - } - - &._1f1ec-1f1e9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 31.25% 60%; - } - - &._1f1ec-1f1f5 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 37.5% 60%; - } - - &._1f1ec-1f1fa { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 43.75% 60%; - } - - &._1f1ec-1f1f9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 50% 60%; - } - - &._1f1ec-1f1ec { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 56.25% 60%; - } - - &._1f1ec-1f1f3 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 62.5% 0%; - } - - &._1f1ec-1f1fc { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 62.5% 6.666666666666667%; - } - - &._1f1ec-1f1fe { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 62.5% 13.333333333333334%; - } - - &._1f1ed-1f1f9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 62.5% 20%; - } - - &._1f1ed-1f1f3 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 62.5% 26.666666666666668%; - } - - &._1f1ed-1f1f0 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 62.5% 33.333333333333336%; - } - - &._1f1ed-1f1fa { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 62.5% 40%; - } - - &._1f1ee-1f1f8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 62.5% 46.666666666666664%; - } - - &._1f1ee-1f1f3 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 62.5% 53.333333333333336%; - } - - &._1f1ee-1f1e9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 62.5% 60%; - } - - &._1f1ee-1f1f7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 0% 66.66666666666667%; - } - - &._1f1ee-1f1f6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 6.25% 66.66666666666667%; - } - - &._1f1ee-1f1ea { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 12.5% 66.66666666666667%; - } - - &._1f1ee-1f1f2 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 18.75% 66.66666666666667%; - } - - &._1f1ee-1f1f1 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 25% 66.66666666666667%; - } - - &._1f1ee-1f1f9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 31.25% 66.66666666666667%; - } - - &._1f1ef-1f1f2 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 37.5% 66.66666666666667%; - } - - &._1f1ef-1f1f5 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 43.75% 66.66666666666667%; - } - - &._1f38c { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 50% 66.66666666666667%; - } - - &._1f1ef-1f1ea { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 56.25% 66.66666666666667%; - } - - &._1f1ef-1f1f4 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 62.5% 66.66666666666667%; - } - - &._1f1f0-1f1ff { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 68.75% 0%; - } - - &._1f1f0-1f1ea { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 68.75% 6.666666666666667%; - } - - &._1f1f0-1f1ee { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 68.75% 13.333333333333334%; - } - - &._1f1fd-1f1f0 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 68.75% 20%; - } - - &._1f1f0-1f1fc { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 68.75% 26.666666666666668%; - } - - &._1f1f0-1f1ec { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 68.75% 33.333333333333336%; - } - - &._1f1f1-1f1e6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 68.75% 40%; - } - - &._1f1f1-1f1fb { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 68.75% 46.666666666666664%; - } - - &._1f1f1-1f1e7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 68.75% 53.333333333333336%; - } - - &._1f1f1-1f1f8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 68.75% 60%; - } - - &._1f1f1-1f1f7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 68.75% 66.66666666666667%; - } - - &._1f1f1-1f1fe { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 0% 73.33333333333333%; - } - - &._1f1f1-1f1ee { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 6.25% 73.33333333333333%; - } - - &._1f3f4 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 12.5% 73.33333333333333%; - } - - &._1f1f1-1f1fa { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 18.75% 73.33333333333333%; - } - - &._1f1f2-1f1f4 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 25% 73.33333333333333%; - } - - &._1f1f2-1f1f0 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 31.25% 73.33333333333333%; - } - - &._1f1f2-1f1ec { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 37.5% 73.33333333333333%; - } - - &._1f1f2-1f1fc { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 43.75% 73.33333333333333%; - } - - &._1f1f2-1f1fe { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 50% 73.33333333333333%; - } - - &._1f1f2-1f1fb { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 56.25% 73.33333333333333%; - } - - &._1f1f2-1f1f1 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 62.5% 73.33333333333333%; - } - - &._1f1f2-1f1f9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 68.75% 73.33333333333333%; - } - - &._1f1f2-1f1ed { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 75% 0%; - } - - &._1f1f2-1f1f6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 75% 6.666666666666667%; - } - - &._1f1f2-1f1f7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 75% 13.333333333333334%; - } - - &._1f1f2-1f1fa { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 75% 20%; - } - - &._1f1fe-1f1f9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 75% 26.666666666666668%; - } - - &._1f1f2-1f1fd { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 75% 33.333333333333336%; - } - - &._1f1eb-1f1f2 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 75% 40%; - } - - &._1f1f2-1f1e9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 75% 46.666666666666664%; - } - - &._1f1f2-1f1e8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 75% 53.333333333333336%; - } - - &._1f1f2-1f1f3 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 75% 60%; - } - - &._1f1f2-1f1ea { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 75% 66.66666666666667%; - } - - &._1f1f2-1f1f8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 75% 73.33333333333333%; - } - - &._1f1f2-1f1e6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 0% 80%; - } - - &._1f1f2-1f1ff { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 6.25% 80%; - } - - &._1f1f2-1f1f2 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 12.5% 80%; - } - - &._1f1f3-1f1e6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 18.75% 80%; - } - - &._1f1f3-1f1f7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 25% 80%; - } - - &._1f1f3-1f1f5 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 31.25% 80%; - } - - &._1f1f3-1f1f1 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 37.5% 80%; - } - - &._1f1f3-1f1e8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 43.75% 80%; - } - - &._1f1f3-1f1ff { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 50% 80%; - } - - &._1f1f3-1f1ee { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 56.25% 80%; - } - - &._1f1f3-1f1ea { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 62.5% 80%; - } - - &._1f1f3-1f1ec { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 68.75% 80%; - } - - &._1f1f3-1f1fa { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 75% 80%; - } - - &._1f1f3-1f1eb { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 81.25% 0%; - } - - &._1f1f0-1f1f5 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 81.25% 6.666666666666667%; - } - - &._1f1f2-1f1f5 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 81.25% 13.333333333333334%; - } - - &._1f1f3-1f1f4 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 81.25% 20%; - } - - &._1f1f4-1f1f2 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 81.25% 26.666666666666668%; - } - - &._1f1f5-1f1f0 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 81.25% 33.333333333333336%; - } - - &._1f1f5-1f1fc { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 81.25% 40%; - } - - &._1f1f5-1f1f8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 81.25% 46.666666666666664%; - } - - &._1f1f5-1f1e6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 81.25% 53.333333333333336%; - } - - &._1f1f5-1f1ec { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 81.25% 60%; - } - - &._1f1f5-1f1fe { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 81.25% 66.66666666666667%; - } - - &._1f1f5-1f1ea { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 81.25% 73.33333333333333%; - } - - &._1f1f5-1f1ed { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 81.25% 80%; - } - - &._1f1f5-1f1f3 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 0% 86.66666666666667%; - } - - &._1f1f5-1f1f1 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 6.25% 86.66666666666667%; - } - - &._1f1f5-1f1f9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 12.5% 86.66666666666667%; - } - - &._1f1f5-1f1f7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 18.75% 86.66666666666667%; - } - - &._1f1f6-1f1e6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 25% 86.66666666666667%; - } - - &._1f1f7-1f1ea { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 31.25% 86.66666666666667%; - } - - &._1f1f7-1f1f4 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 37.5% 86.66666666666667%; - } - - &._1f1f7-1f1fa { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 43.75% 86.66666666666667%; - } - - &._1f1f7-1f1fc { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 50% 86.66666666666667%; - } - - &._1f1fc-1f1f8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 56.25% 86.66666666666667%; - } - - &._1f1f8-1f1f2 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 62.5% 86.66666666666667%; - } - - &._1f1f8-1f1f9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 68.75% 86.66666666666667%; - } - - &._1f1f8-1f1e6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 75% 86.66666666666667%; - } - - &._1f1f8-1f1f3 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 81.25% 86.66666666666667%; - } - - &._1f1f7-1f1f8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 87.5% 0%; - } - - &._1f1f8-1f1e8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 87.5% 6.666666666666667%; - } - - &._1f1f8-1f1f1 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 87.5% 13.333333333333334%; - } - - &._1f1f8-1f1ec { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 87.5% 20%; - } - - &._1f1f8-1f1fd { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 87.5% 26.666666666666668%; - } - - &._1f1f8-1f1f0 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 87.5% 33.333333333333336%; - } - - &._1f1f8-1f1ee { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 87.5% 40%; - } - - &._1f1ec-1f1f8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 87.5% 46.666666666666664%; - } - - &._1f1f8-1f1e7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 87.5% 53.333333333333336%; - } - - &._1f1f8-1f1f4 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 87.5% 60%; - } - - &._1f1ff-1f1e6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 87.5% 66.66666666666667%; - } - - &._1f1f0-1f1f7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 87.5% 73.33333333333333%; - } - - &._1f1f8-1f1f8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 87.5% 80%; - } - - &._1f1ea-1f1f8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 87.5% 86.66666666666667%; - } - - &._1f1f1-1f1f0 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 0% 93.33333333333333%; - } - - &._1f1e7-1f1f1 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 6.25% 93.33333333333333%; - } - - &._1f1f8-1f1ed { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 12.5% 93.33333333333333%; - } - - &._1f1f0-1f1f3 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 18.75% 93.33333333333333%; - } - - &._1f1f1-1f1e8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 25% 93.33333333333333%; - } - - &._1f1f5-1f1f2 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 31.25% 93.33333333333333%; - } - - &._1f1fb-1f1e8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 37.5% 93.33333333333333%; - } - - &._1f1f8-1f1e9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 43.75% 93.33333333333333%; - } - - &._1f1f8-1f1f7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 50% 93.33333333333333%; - } - - &._1f1f8-1f1ff { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 56.25% 93.33333333333333%; - } - - &._1f1f8-1f1ea { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 62.5% 93.33333333333333%; - } - - &._1f1e8-1f1ed { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 68.75% 93.33333333333333%; - } - - &._1f1f8-1f1fe { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 75% 93.33333333333333%; - } - - &._1f1f9-1f1fc { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 81.25% 93.33333333333333%; - } - - &._1f1f9-1f1ef { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 87.5% 93.33333333333333%; - } - - &._1f1f9-1f1ff { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 93.75% 0%; - } - - &._1f1f9-1f1ed { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 93.75% 6.666666666666667%; - } - - &._1f1f9-1f1f1 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 93.75% 13.333333333333334%; - } - - &._1f1f9-1f1ec { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 93.75% 20%; - } - - &._1f1f9-1f1f0 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 93.75% 26.666666666666668%; - } - - &._1f1f9-1f1f4 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 93.75% 33.333333333333336%; - } - - &._1f1f9-1f1f9 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 93.75% 40%; - } - - &._1f1f9-1f1f3 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 93.75% 46.666666666666664%; - } - - &._1f1f9-1f1f7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 93.75% 53.333333333333336%; - } - - &._1f1f9-1f1f2 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 93.75% 60%; - } - - &._1f1f9-1f1e8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 93.75% 66.66666666666667%; - } - - &._1f1fb-1f1ee { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 93.75% 73.33333333333333%; - } - - &._1f1f9-1f1fb { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 93.75% 80%; - } - - &._1f1fa-1f1ec { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 93.75% 86.66666666666667%; - } - - &._1f1fa-1f1e6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 93.75% 93.33333333333333%; - } - - &._1f1e6-1f1ea { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 0% 100%; - } - - &._1f1ec-1f1e7 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 6.25% 100%; - } - - &._1f3f4-e0067-e0062-e0065-e006e-e0067-e007f { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 12.5% 100%; - } - - &._1f3f4-e0067-e0062-e0073-e0063-e0074-e007f { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 18.75% 100%; - } - - &._1f3f4-e0067-e0062-e0077-e006c-e0073-e007f { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 25% 100%; - } - - &._1f1fa-1f1f8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 31.25% 100%; - } - - &._1f1fa-1f1fe { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 37.5% 100%; - } - - &._1f1fa-1f1ff { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 43.75% 100%; - } - - &._1f1fb-1f1fa { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 50% 100%; - } - - &._1f1fb-1f1e6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 56.25% 100%; - } - - &._1f1fb-1f1ea { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 62.5% 100%; - } - - &._1f1fb-1f1f3 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 68.75% 100%; - } - - &._1f1fc-1f1eb { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 75% 100%; - } - - &._1f1ea-1f1ed { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 81.25% 100%; - } - - &._1f1fe-1f1ea { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 87.5% 100%; - } - - &._1f1ff-1f1f2 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 93.75% 100%; - } - - &._1f1ff-1f1fc { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 100% 0%; - } - - &._1f1e6-1f1e8 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 100% 6.666666666666667%; - } - - &._1f1e7-1f1fb { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 100% 13.333333333333334%; - } - - &._1f1e8-1f1f5 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 100% 20%; - } - - &._1f1ea-1f1e6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 100% 26.666666666666668%; - } - - &._1f1e9-1f1ec { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 100% 33.333333333333336%; - } - - &._1f1ed-1f1f2 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 100% 40%; - } - - &._1f1f2-1f1eb { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 100% 46.666666666666664%; - } - - &._1f1f8-1f1ef { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 100% 53.333333333333336%; - } - - &._1f1f9-1f1e6 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 100% 60%; - } - - &._1f1fa-1f1f2 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 100% 66.66666666666667%; - } - - &._1f1fa-1f1f3 { - background-repeat: no-repeat; - background-size: 1700% 1600%; - background-position: 100% 73.33333333333333%; - } - -} - diff --git a/apps/meteor/app/emoji-emojione/client/food-sprites.css b/apps/meteor/app/emoji-emojione/client/food-sprites.css deleted file mode 100644 index 078e4d9cdb20f..0000000000000 --- a/apps/meteor/app/emoji-emojione/client/food-sprites.css +++ /dev/null @@ -1,637 +0,0 @@ -/* stylelint-disable */ - -.emojione-food { - background-image: url('/packages/emojione/food-sprites.png'); - - &._1f35d { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 0% 0%; - } - - &._2615 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 10% 0%; - } - - &._1f34e { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 0% 11.11111111111111%; - } - - &._1f350 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 10% 11.11111111111111%; - } - - &._1f34a { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 20% 0%; - } - - &._1f34b { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 20% 11.11111111111111%; - } - - &._1f34c { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 0% 22.22222222222222%; - } - - &._1f349 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 10% 22.22222222222222%; - } - - &._1f347 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 20% 22.22222222222222%; - } - - &._1f353 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 30% 0%; - } - - &._1f348 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 30% 11.11111111111111%; - } - - &._1f352 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 30% 22.22222222222222%; - } - - &._1f351 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 0% 33.333333333333336%; - } - - &._1f96d { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 10% 33.333333333333336%; - } - - &._1f34d { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 20% 33.333333333333336%; - } - - &._1f965 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 30% 33.333333333333336%; - } - - &._1f95d { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 40% 0%; - } - - &._1f345 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 40% 11.11111111111111%; - } - - &._1f346 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 40% 22.22222222222222%; - } - - &._1f951 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 40% 33.333333333333336%; - } - - &._1f966 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 0% 44.44444444444444%; - } - - &._1f96c { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 10% 44.44444444444444%; - } - - &._1f952 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 20% 44.44444444444444%; - } - - &._1f336 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 30% 44.44444444444444%; - } - - &._1f33d { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 40% 44.44444444444444%; - } - - &._1f955 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 50% 0%; - } - - &._1f954 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 50% 11.11111111111111%; - } - - &._1f360 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 50% 22.22222222222222%; - } - - &._1f950 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 50% 33.333333333333336%; - } - - &._1f35e { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 50% 44.44444444444444%; - } - - &._1f956 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 0% 55.55555555555556%; - } - - &._1f968 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 10% 55.55555555555556%; - } - - &._1f96f { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 20% 55.55555555555556%; - } - - &._1f9c0 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 30% 55.55555555555556%; - } - - &._1f95a { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 40% 55.55555555555556%; - } - - &._1f373 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 50% 55.55555555555556%; - } - - &._1f95e { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 60% 0%; - } - - &._1f953 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 60% 11.11111111111111%; - } - - &._1f969 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 60% 22.22222222222222%; - } - - &._1f357 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 60% 33.333333333333336%; - } - - &._1f356 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 60% 44.44444444444444%; - } - - &._1f32d { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 60% 55.55555555555556%; - } - - &._1f354 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 0% 66.66666666666667%; - } - - &._1f35f { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 10% 66.66666666666667%; - } - - &._1f355 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 20% 66.66666666666667%; - } - - &._1f96a { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 30% 66.66666666666667%; - } - - &._1f959 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 40% 66.66666666666667%; - } - - &._1f32e { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 50% 66.66666666666667%; - } - - &._1f32f { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 60% 66.66666666666667%; - } - - &._1f957 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 70% 0%; - } - - &._1f958 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 70% 11.11111111111111%; - } - - &._1f96b { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 70% 22.22222222222222%; - } - - &._1f34f { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 70% 33.333333333333336%; - } - - &._1f35c { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 70% 44.44444444444444%; - } - - &._1f372 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 70% 55.55555555555556%; - } - - &._1f35b { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 70% 66.66666666666667%; - } - - &._1f363 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 0% 77.77777777777777%; - } - - &._1f371 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 10% 77.77777777777777%; - } - - &._1f364 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 20% 77.77777777777777%; - } - - &._1f359 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 30% 77.77777777777777%; - } - - &._1f35a { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 40% 77.77777777777777%; - } - - &._1f358 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 50% 77.77777777777777%; - } - - &._1f365 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 60% 77.77777777777777%; - } - - &._1f960 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 70% 77.77777777777777%; - } - - &._1f362 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 80% 0%; - } - - &._1f361 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 80% 11.11111111111111%; - } - - &._1f367 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 80% 22.22222222222222%; - } - - &._1f368 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 80% 33.333333333333336%; - } - - &._1f366 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 80% 44.44444444444444%; - } - - &._1f967 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 80% 55.55555555555556%; - } - - &._1f370 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 80% 66.66666666666667%; - } - - &._1f382 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 80% 77.77777777777777%; - } - - &._1f96e { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 0% 88.88888888888889%; - } - - &._1f9c1 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 10% 88.88888888888889%; - } - - &._1f36e { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 20% 88.88888888888889%; - } - - &._1f36d { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 30% 88.88888888888889%; - } - - &._1f36c { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 40% 88.88888888888889%; - } - - &._1f36b { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 50% 88.88888888888889%; - } - - &._1f37f { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 60% 88.88888888888889%; - } - - &._1f9c2 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 70% 88.88888888888889%; - } - - &._1f369 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 80% 88.88888888888889%; - } - - &._1f95f { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 90% 0%; - } - - &._1f36a { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 90% 11.11111111111111%; - } - - &._1f330 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 90% 22.22222222222222%; - } - - &._1f95c { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 90% 33.333333333333336%; - } - - &._1f36f { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 90% 44.44444444444444%; - } - - &._1f95b { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 90% 55.55555555555556%; - } - - &._1f37c { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 90% 66.66666666666667%; - } - - &._1f375 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 90% 77.77777777777777%; - } - - &._1f964 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 90% 88.88888888888889%; - } - - &._1f376 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 0% 100%; - } - - &._1f37a { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 10% 100%; - } - - &._1f37b { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 20% 100%; - } - - &._1f942 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 30% 100%; - } - - &._1f377 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 40% 100%; - } - - &._1f943 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 50% 100%; - } - - &._1f378 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 60% 100%; - } - - &._1f379 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 70% 100%; - } - - &._1f37e { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 80% 100%; - } - - &._1f944 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 90% 100%; - } - - &._1f374 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 100% 0%; - } - - &._1f37d { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 100% 11.11111111111111%; - } - - &._1f963 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 100% 22.22222222222222%; - } - - &._1f961 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 100% 33.333333333333336%; - } - - &._1f962 { - background-repeat: no-repeat; - background-size: 1100% 1000%; - background-position: 100% 44.44444444444444%; - } - -} - diff --git a/apps/meteor/app/emoji-emojione/client/index.ts b/apps/meteor/app/emoji-emojione/client/index.ts deleted file mode 100644 index 2e84171259c48..0000000000000 --- a/apps/meteor/app/emoji-emojione/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './emojione-sprites.css'; diff --git a/apps/meteor/app/emoji-emojione/client/modifier-sprites.css b/apps/meteor/app/emoji-emojione/client/modifier-sprites.css deleted file mode 100644 index 087b9c2bcfa7b..0000000000000 --- a/apps/meteor/app/emoji-emojione/client/modifier-sprites.css +++ /dev/null @@ -1,42 +0,0 @@ -/* stylelint-disable */ - -.emojione-modifier { - background-image: url('/packages/emojione/modifier-sprites.png'); - -} - - -.emojione-diversity._1f3fb { - background-image: url('/packages/emojione/modifier-sprites.png'); - background-repeat: no-repeat; - background-size: 300% 200%; - background-position: 0% 0%; -} - -.emojione-diversity._1f3fc { - background-image: url('/packages/emojione/modifier-sprites.png'); - background-repeat: no-repeat; - background-size: 300% 200%; - background-position: 50% 0%; -} - -.emojione-diversity._1f3fd { - background-image: url('/packages/emojione/modifier-sprites.png'); - background-repeat: no-repeat; - background-size: 300% 200%; - background-position: 0% 100%; -} - -.emojione-diversity._1f3fe { - background-image: url('/packages/emojione/modifier-sprites.png'); - background-repeat: no-repeat; - background-size: 300% 200%; - background-position: 50% 100%; -} - -.emojione-diversity._1f3ff { - background-image: url('/packages/emojione/modifier-sprites.png'); - background-repeat: no-repeat; - background-size: 300% 200%; - background-position: 100% 0%; -} diff --git a/apps/meteor/app/emoji-emojione/client/nature-sprites.css b/apps/meteor/app/emoji-emojione/client/nature-sprites.css deleted file mode 100644 index e9ee1c97290af..0000000000000 --- a/apps/meteor/app/emoji-emojione/client/nature-sprites.css +++ /dev/null @@ -1,1069 +0,0 @@ -/* stylelint-disable */ - -.emojione-nature { - background-image: url('/packages/emojione/nature-sprites.png'); - - &._1f42b { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 0% 0%; - } - - &._2600 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 7.6923076923076925% 0%; - } - - &._2602 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 0% 8.333333333333334%; - } - - &._2603 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 7.6923076923076925% 8.333333333333334%; - } - - &._2604 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 15.384615384615385% 0%; - } - - &._2614 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 15.384615384615385% 8.333333333333334%; - } - - &._2618 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 0% 16.666666666666668%; - } - - &._2728 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 7.6923076923076925% 16.666666666666668%; - } - - &._2744 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 15.384615384615385% 16.666666666666668%; - } - - &._1f436 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 23.076923076923077% 0%; - } - - &._1f431 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 23.076923076923077% 8.333333333333334%; - } - - &._1f42d { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 23.076923076923077% 16.666666666666668%; - } - - &._1f439 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 0% 25%; - } - - &._1f430 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 7.6923076923076925% 25%; - } - - &._1f98a { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 15.384615384615385% 25%; - } - - &._1f99d { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 23.076923076923077% 25%; - } - - &._1f43b { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 30.76923076923077% 0%; - } - - &._1f43c { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 30.76923076923077% 8.333333333333334%; - } - - &._1f998 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 30.76923076923077% 16.666666666666668%; - } - - &._1f9a1 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 30.76923076923077% 25%; - } - - &._1f428 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 0% 33.333333333333336%; - } - - &._1f42f { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 7.6923076923076925% 33.333333333333336%; - } - - &._1f981 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 15.384615384615385% 33.333333333333336%; - } - - &._1f42e { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 23.076923076923077% 33.333333333333336%; - } - - &._1f437 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 30.76923076923077% 33.333333333333336%; - } - - &._1f43d { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 38.46153846153846% 0%; - } - - &._1f438 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 38.46153846153846% 8.333333333333334%; - } - - &._1f435 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 38.46153846153846% 16.666666666666668%; - } - - &._1f648 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 38.46153846153846% 25%; - } - - &._1f649 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 38.46153846153846% 33.333333333333336%; - } - - &._1f64a { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 0% 41.666666666666664%; - } - - &._1f412 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 7.6923076923076925% 41.666666666666664%; - } - - &._1f414 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 15.384615384615385% 41.666666666666664%; - } - - &._1f427 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 23.076923076923077% 41.666666666666664%; - } - - &._1f426 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 30.76923076923077% 41.666666666666664%; - } - - &._1f424 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 38.46153846153846% 41.666666666666664%; - } - - &._1f423 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 46.15384615384615% 0%; - } - - &._1f425 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 46.15384615384615% 8.333333333333334%; - } - - &._1f986 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 46.15384615384615% 16.666666666666668%; - } - - &._1f9a2 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 46.15384615384615% 25%; - } - - &._1f985 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 46.15384615384615% 33.333333333333336%; - } - - &._1f989 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 46.15384615384615% 41.666666666666664%; - } - - &._1f99c { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 0% 50%; - } - - &._1f99a { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 7.6923076923076925% 50%; - } - - &._1f987 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 15.384615384615385% 50%; - } - - &._1f43a { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 23.076923076923077% 50%; - } - - &._1f417 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 30.76923076923077% 50%; - } - - &._1f434 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 38.46153846153846% 50%; - } - - &._1f984 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 46.15384615384615% 50%; - } - - &._1f41d { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 53.84615384615385% 0%; - } - - &._1f41b { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 53.84615384615385% 8.333333333333334%; - } - - &._1f98b { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 53.84615384615385% 16.666666666666668%; - } - - &._1f40c { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 53.84615384615385% 25%; - } - - &._1f41a { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 53.84615384615385% 33.333333333333336%; - } - - &._1f41e { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 53.84615384615385% 41.666666666666664%; - } - - &._1f41c { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 53.84615384615385% 50%; - } - - &._1f997 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 0% 58.333333333333336%; - } - - &._1f577 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 7.6923076923076925% 58.333333333333336%; - } - - &._1f578 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 15.384615384615385% 58.333333333333336%; - } - - &._1f982 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 23.076923076923077% 58.333333333333336%; - } - - &._1f99f { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 30.76923076923077% 58.333333333333336%; - } - - &._1f9a0 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 38.46153846153846% 58.333333333333336%; - } - - &._1f422 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 46.15384615384615% 58.333333333333336%; - } - - &._1f40d { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 53.84615384615385% 58.333333333333336%; - } - - &._1f98e { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 61.53846153846154% 0%; - } - - &._1f996 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 61.53846153846154% 8.333333333333334%; - } - - &._1f995 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 61.53846153846154% 16.666666666666668%; - } - - &._1f419 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 61.53846153846154% 25%; - } - - &._1f991 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 61.53846153846154% 33.333333333333336%; - } - - &._1f990 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 61.53846153846154% 41.666666666666664%; - } - - &._1f980 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 61.53846153846154% 50%; - } - - &._1f99e { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 61.53846153846154% 58.333333333333336%; - } - - &._1f421 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 0% 66.66666666666667%; - } - - &._1f420 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 7.6923076923076925% 66.66666666666667%; - } - - &._1f41f { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 15.384615384615385% 66.66666666666667%; - } - - &._1f42c { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 23.076923076923077% 66.66666666666667%; - } - - &._1f433 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 30.76923076923077% 66.66666666666667%; - } - - &._1f40b { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 38.46153846153846% 66.66666666666667%; - } - - &._1f988 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 46.15384615384615% 66.66666666666667%; - } - - &._1f40a { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 53.84615384615385% 66.66666666666667%; - } - - &._1f405 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 61.53846153846154% 66.66666666666667%; - } - - &._1f406 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 69.23076923076923% 0%; - } - - &._1f993 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 69.23076923076923% 8.333333333333334%; - } - - &._1f98d { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 69.23076923076923% 16.666666666666668%; - } - - &._1f418 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 69.23076923076923% 25%; - } - - &._1f98f { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 69.23076923076923% 33.333333333333336%; - } - - &._1f99b { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 69.23076923076923% 41.666666666666664%; - } - - &._1f42a { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 69.23076923076923% 50%; - } - - &._2601 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 69.23076923076923% 58.333333333333336%; - } - - &._1f992 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 69.23076923076923% 66.66666666666667%; - } - - &._1f999 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 0% 75%; - } - - &._1f403 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 7.6923076923076925% 75%; - } - - &._1f402 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 15.384615384615385% 75%; - } - - &._1f404 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 23.076923076923077% 75%; - } - - &._1f40e { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 30.76923076923077% 75%; - } - - &._1f416 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 38.46153846153846% 75%; - } - - &._1f40f { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 46.15384615384615% 75%; - } - - &._1f411 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 53.84615384615385% 75%; - } - - &._1f410 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 61.53846153846154% 75%; - } - - &._1f98c { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 69.23076923076923% 75%; - } - - &._1f415 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 76.92307692307692% 0%; - } - - &._1f429 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 76.92307692307692% 8.333333333333334%; - } - - &._1f408 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 76.92307692307692% 16.666666666666668%; - } - - &._1f413 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 76.92307692307692% 25%; - } - - &._1f983 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 76.92307692307692% 33.333333333333336%; - } - - &._1f54a { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 76.92307692307692% 41.666666666666664%; - } - - &._1f407 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 76.92307692307692% 50%; - } - - &._1f401 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 76.92307692307692% 58.333333333333336%; - } - - &._1f400 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 76.92307692307692% 66.66666666666667%; - } - - &._1f43f { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 76.92307692307692% 75%; - } - - &._1f994 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 0% 83.33333333333333%; - } - - &._1f43e { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 7.6923076923076925% 83.33333333333333%; - } - - &._1f409 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 15.384615384615385% 83.33333333333333%; - } - - &._1f432 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 23.076923076923077% 83.33333333333333%; - } - - &._1f335 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 30.76923076923077% 83.33333333333333%; - } - - &._1f384 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 38.46153846153846% 83.33333333333333%; - } - - &._1f332 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 46.15384615384615% 83.33333333333333%; - } - - &._1f333 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 53.84615384615385% 83.33333333333333%; - } - - &._1f334 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 61.53846153846154% 83.33333333333333%; - } - - &._1f331 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 69.23076923076923% 83.33333333333333%; - } - - &._1f33f { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 76.92307692307692% 83.33333333333333%; - } - - &._1f340 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 84.61538461538461% 0%; - } - - &._1f38d { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 84.61538461538461% 8.333333333333334%; - } - - &._1f38b { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 84.61538461538461% 16.666666666666668%; - } - - &._1f343 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 84.61538461538461% 25%; - } - - &._1f342 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 84.61538461538461% 33.333333333333336%; - } - - &._1f341 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 84.61538461538461% 41.666666666666664%; - } - - &._1f344 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 84.61538461538461% 50%; - } - - &._1f33e { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 84.61538461538461% 58.333333333333336%; - } - - &._1f490 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 84.61538461538461% 66.66666666666667%; - } - - &._1f337 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 84.61538461538461% 75%; - } - - &._1f339 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 84.61538461538461% 83.33333333333333%; - } - - &._1f940 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 0% 91.66666666666667%; - } - - &._1f33a { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 7.6923076923076925% 91.66666666666667%; - } - - &._1f338 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 15.384615384615385% 91.66666666666667%; - } - - &._1f33c { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 23.076923076923077% 91.66666666666667%; - } - - &._1f33b { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 30.76923076923077% 91.66666666666667%; - } - - &._1f31e { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 38.46153846153846% 91.66666666666667%; - } - - &._1f31d { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 46.15384615384615% 91.66666666666667%; - } - - &._1f31b { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 53.84615384615385% 91.66666666666667%; - } - - &._1f31c { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 61.53846153846154% 91.66666666666667%; - } - - &._1f31a { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 69.23076923076923% 91.66666666666667%; - } - - &._1f315 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 76.92307692307692% 91.66666666666667%; - } - - &._1f316 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 84.61538461538461% 91.66666666666667%; - } - - &._1f317 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 92.3076923076923% 0%; - } - - &._1f318 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 92.3076923076923% 8.333333333333334%; - } - - &._1f311 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 92.3076923076923% 16.666666666666668%; - } - - &._1f312 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 92.3076923076923% 25%; - } - - &._1f313 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 92.3076923076923% 33.333333333333336%; - } - - &._1f314 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 92.3076923076923% 41.666666666666664%; - } - - &._1f319 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 92.3076923076923% 50%; - } - - &._1f30e { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 92.3076923076923% 58.333333333333336%; - } - - &._1f30d { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 92.3076923076923% 66.66666666666667%; - } - - &._1f30f { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 92.3076923076923% 75%; - } - - &._1f4ab { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 92.3076923076923% 83.33333333333333%; - } - - &._2b50 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 92.3076923076923% 91.66666666666667%; - } - - &._1f31f { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 0% 100%; - } - - &._26a1 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 7.6923076923076925% 100%; - } - - &._1f4a5 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 15.384615384615385% 100%; - } - - &._1f525 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 23.076923076923077% 100%; - } - - &._1f32a { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 30.76923076923077% 100%; - } - - &._1f308 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 38.46153846153846% 100%; - } - - &._1f324 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 46.15384615384615% 100%; - } - - &._26c5 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 53.84615384615385% 100%; - } - - &._1f325 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 61.53846153846154% 100%; - } - - &._1f326 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 69.23076923076923% 100%; - } - - &._1f327 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 76.92307692307692% 100%; - } - - &._26c8 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 84.61538461538461% 100%; - } - - &._1f329 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 92.3076923076923% 100%; - } - - &._1f328 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 100% 0%; - } - - &._26c4 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 100% 8.333333333333334%; - } - - &._1f32c { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 100% 16.666666666666668%; - } - - &._1f4a8 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 100% 25%; - } - - &._1f4a7 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 100% 33.333333333333336%; - } - - &._1f4a6 { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 100% 41.666666666666664%; - } - - &._1f30a { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 100% 50%; - } - - &._1f32b { - background-repeat: no-repeat; - background-size: 1400% 1300%; - background-position: 100% 58.333333333333336%; - } - -} - diff --git a/apps/meteor/app/emoji-emojione/client/objects-sprites.css b/apps/meteor/app/emoji-emojione/client/objects-sprites.css deleted file mode 100644 index 1f9551c860017..0000000000000 --- a/apps/meteor/app/emoji-emojione/client/objects-sprites.css +++ /dev/null @@ -1,1259 +0,0 @@ -/* stylelint-disable */ - -.emojione-objects { - background-image: url('/packages/emojione/objects-sprites.png'); - - &._2328 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 7.142857142857143% 0%; - } - - &._2694 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 0% 7.6923076923076925%; - } - - &._2696 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 7.142857142857143% 7.6923076923076925%; - } - - &._2697 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 14.285714285714286% 0%; - } - - &._2699 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 14.285714285714286% 7.6923076923076925%; - } - - &._2702 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 0% 15.384615384615385%; - } - - &._2709 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 7.142857142857143% 15.384615384615385%; - } - - &._2712 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 14.285714285714286% 15.384615384615385%; - } - - &._231a { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 21.428571428571427% 0%; - } - - &._1f4f1 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 21.428571428571427% 7.6923076923076925%; - } - - &._1f4f2 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 21.428571428571427% 15.384615384615385%; - } - - &._1f4bb { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 0% 23.076923076923077%; - } - - &._1f5a5 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 7.142857142857143% 23.076923076923077%; - } - - &._1f5a8 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 14.285714285714286% 23.076923076923077%; - } - - &._1f5b1 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 21.428571428571427% 23.076923076923077%; - } - - &._1f5b2 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 28.571428571428573% 0%; - } - - &._1f579 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 28.571428571428573% 7.6923076923076925%; - } - - &._265f { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 28.571428571428573% 15.384615384615385%; - } - - &._1f9e9 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 28.571428571428573% 23.076923076923077%; - } - - &._1f5dc { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 0% 30.76923076923077%; - } - - &._1f4bd { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 7.142857142857143% 30.76923076923077%; - } - - &._1f4be { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 14.285714285714286% 30.76923076923077%; - } - - &._1f4bf { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 21.428571428571427% 30.76923076923077%; - } - - &._1f4c0 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 28.571428571428573% 30.76923076923077%; - } - - &._1f4fc { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 35.714285714285715% 0%; - } - - &._1f4f7 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 35.714285714285715% 7.6923076923076925%; - } - - &._1f4f8 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 35.714285714285715% 15.384615384615385%; - } - - &._1f4f9 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 35.714285714285715% 23.076923076923077%; - } - - &._1f3a5 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 35.714285714285715% 30.76923076923077%; - } - - &._1f4fd { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 0% 38.46153846153846%; - } - - &._1f39e { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 7.142857142857143% 38.46153846153846%; - } - - &._1f4de { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 14.285714285714286% 38.46153846153846%; - } - - &._260e { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 21.428571428571427% 38.46153846153846%; - } - - &._1f4df { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 28.571428571428573% 38.46153846153846%; - } - - &._1f4e0 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 35.714285714285715% 38.46153846153846%; - } - - &._1f4fa { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 42.857142857142854% 0%; - } - - &._1f4fb { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 42.857142857142854% 7.6923076923076925%; - } - - &._1f399 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 42.857142857142854% 15.384615384615385%; - } - - &._1f39a { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 42.857142857142854% 23.076923076923077%; - } - - &._1f39b { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 42.857142857142854% 30.76923076923077%; - } - - &._23f1 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 42.857142857142854% 38.46153846153846%; - } - - &._23f2 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 0% 46.15384615384615%; - } - - &._23f0 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 7.142857142857143% 46.15384615384615%; - } - - &._1f570 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 14.285714285714286% 46.15384615384615%; - } - - &._231b { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 21.428571428571427% 46.15384615384615%; - } - - &._23f3 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 28.571428571428573% 46.15384615384615%; - } - - &._1f4e1 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 35.714285714285715% 46.15384615384615%; - } - - &._1f9ed { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 42.857142857142854% 46.15384615384615%; - } - - &._1f50b { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 50% 0%; - } - - &._1f50c { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 50% 7.6923076923076925%; - } - - &._1f9f2 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 50% 15.384615384615385%; - } - - &._1f4a1 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 50% 23.076923076923077%; - } - - &._1f526 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 50% 30.76923076923077%; - } - - &._1f56f { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 50% 38.46153846153846%; - } - - &._1f9ef { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 50% 46.15384615384615%; - } - - &._1f5d1 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 0% 53.84615384615385%; - } - - &._1f6e2 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 7.142857142857143% 53.84615384615385%; - } - - &._1f4b8 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 14.285714285714286% 53.84615384615385%; - } - - &._1f4b5 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 21.428571428571427% 53.84615384615385%; - } - - &._1f4b4 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 28.571428571428573% 53.84615384615385%; - } - - &._1f4b6 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 35.714285714285715% 53.84615384615385%; - } - - &._1f4b7 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 42.857142857142854% 53.84615384615385%; - } - - &._1f4b0 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 50% 53.84615384615385%; - } - - &._1f4b3 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 57.142857142857146% 0%; - } - - &._1f48e { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 57.142857142857146% 7.6923076923076925%; - } - - &._1f9ff { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 57.142857142857146% 15.384615384615385%; - } - - &._1f9f1 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 57.142857142857146% 23.076923076923077%; - } - - &._1f9f0 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 57.142857142857146% 30.76923076923077%; - } - - &._1f527 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 57.142857142857146% 38.46153846153846%; - } - - &._1f528 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 57.142857142857146% 46.15384615384615%; - } - - &._1f6e0 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 57.142857142857146% 53.84615384615385%; - } - - &._26cf { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 0% 61.53846153846154%; - } - - &._1f529 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 7.142857142857143% 61.53846153846154%; - } - - &._26d3 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 14.285714285714286% 61.53846153846154%; - } - - &._1f52b { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 21.428571428571427% 61.53846153846154%; - } - - &._1f4a3 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 28.571428571428573% 61.53846153846154%; - } - - &._1f52a { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 35.714285714285715% 61.53846153846154%; - } - - &._1f5e1 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 42.857142857142854% 61.53846153846154%; - } - - &._1f6e1 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 50% 61.53846153846154%; - } - - &._1f6ac { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 57.142857142857146% 61.53846153846154%; - } - - &._26b0 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 64.28571428571429% 0%; - } - - &._26b1 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 64.28571428571429% 7.6923076923076925%; - } - - &._1f3fa { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 64.28571428571429% 15.384615384615385%; - } - - &._1f52e { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 64.28571428571429% 23.076923076923077%; - } - - &._1f4ff { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 64.28571428571429% 30.76923076923077%; - } - - &._1f488 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 64.28571428571429% 38.46153846153846%; - } - - &._1f9ea { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 64.28571428571429% 46.15384615384615%; - } - - &._1f9eb { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 64.28571428571429% 53.84615384615385%; - } - - &._1f9ec { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 64.28571428571429% 61.53846153846154%; - } - - &._1f9ee { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 0% 69.23076923076923%; - } - - &._1f52d { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 7.142857142857143% 69.23076923076923%; - } - - &._1f52c { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 14.285714285714286% 69.23076923076923%; - } - - &._1f573 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 21.428571428571427% 69.23076923076923%; - } - - &._1f48a { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 28.571428571428573% 69.23076923076923%; - } - - &._1f489 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 35.714285714285715% 69.23076923076923%; - } - - &._1f321 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 42.857142857142854% 69.23076923076923%; - } - - &._1f6bd { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 50% 69.23076923076923%; - } - - &._1f6b0 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 57.142857142857146% 69.23076923076923%; - } - - &._1f6bf { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 64.28571428571429% 69.23076923076923%; - } - - &._1f6c1 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 71.42857142857143% 0%; - } - - &._1f6c0 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 71.42857142857143% 7.6923076923076925%; - } - - &._2692 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 71.42857142857143% 23.076923076923077%; - } - - &._1f9f9 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 71.42857142857143% 53.84615384615385%; - } - - &._1f9fa { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 71.42857142857143% 61.53846153846154%; - } - - &._1f9fb { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 71.42857142857143% 69.23076923076923%; - } - - &._1f9fc { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 0% 76.92307692307692%; - } - - &._1f9fd { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 7.142857142857143% 76.92307692307692%; - } - - &._1f9f4 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 14.285714285714286% 76.92307692307692%; - } - - &._1f9f5 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 21.428571428571427% 76.92307692307692%; - } - - &._1f9f6 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 28.571428571428573% 76.92307692307692%; - } - - &._1f6ce { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 35.714285714285715% 76.92307692307692%; - } - - &._1f511 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 42.857142857142854% 76.92307692307692%; - } - - &._1f5dd { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 50% 76.92307692307692%; - } - - &._1f6aa { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 57.142857142857146% 76.92307692307692%; - } - - &._1f6cb { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 64.28571428571429% 76.92307692307692%; - } - - &._1f6cf { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 71.42857142857143% 76.92307692307692%; - } - - &._1f6cc { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 78.57142857142857% 0%; - } - - &._1f9f8 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 78.57142857142857% 46.15384615384615%; - } - - &._1f5bc { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 78.57142857142857% 53.84615384615385%; - } - - &._1f6cd { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 78.57142857142857% 61.53846153846154%; - } - - &._1f6d2 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 78.57142857142857% 69.23076923076923%; - } - - &._1f381 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 78.57142857142857% 76.92307692307692%; - } - - &._1f388 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 0% 84.61538461538461%; - } - - &._1f38f { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 7.142857142857143% 84.61538461538461%; - } - - &._1f380 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 14.285714285714286% 84.61538461538461%; - } - - &._1f38a { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 21.428571428571427% 84.61538461538461%; - } - - &._1f389 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 28.571428571428573% 84.61538461538461%; - } - - &._1f38e { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 35.714285714285715% 84.61538461538461%; - } - - &._1f3ee { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 42.857142857142854% 84.61538461538461%; - } - - &._1f390 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 50% 84.61538461538461%; - } - - &._1f9e7 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 57.142857142857146% 84.61538461538461%; - } - - &._1f4e9 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 64.28571428571429% 84.61538461538461%; - } - - &._1f4e8 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 71.42857142857143% 84.61538461538461%; - } - - &._1f4e7 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 78.57142857142857% 84.61538461538461%; - } - - &._1f48c { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 85.71428571428571% 0%; - } - - &._1f4e5 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 85.71428571428571% 7.6923076923076925%; - } - - &._1f4e4 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 85.71428571428571% 15.384615384615385%; - } - - &._1f4e6 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 85.71428571428571% 23.076923076923077%; - } - - &._1f3f7 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 85.71428571428571% 30.76923076923077%; - } - - &._1f4ea { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 85.71428571428571% 38.46153846153846%; - } - - &._1f4eb { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 85.71428571428571% 46.15384615384615%; - } - - &._1f4ec { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 85.71428571428571% 53.84615384615385%; - } - - &._1f4ed { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 85.71428571428571% 61.53846153846154%; - } - - &._1f4ee { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 85.71428571428571% 69.23076923076923%; - } - - &._1f4ef { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 85.71428571428571% 76.92307692307692%; - } - - &._1f4dc { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 85.71428571428571% 84.61538461538461%; - } - - &._1f4c3 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 0% 92.3076923076923%; - } - - &._1f4c4 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 7.142857142857143% 92.3076923076923%; - } - - &._1f9fe { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 14.285714285714286% 92.3076923076923%; - } - - &._1f4d1 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 21.428571428571427% 92.3076923076923%; - } - - &._1f4ca { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 28.571428571428573% 92.3076923076923%; - } - - &._1f4c8 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 35.714285714285715% 92.3076923076923%; - } - - &._1f4c9 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 42.857142857142854% 92.3076923076923%; - } - - &._1f5d2 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 50% 92.3076923076923%; - } - - &._1f5d3 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 57.142857142857146% 92.3076923076923%; - } - - &._1f4c6 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 64.28571428571429% 92.3076923076923%; - } - - &._1f4c5 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 71.42857142857143% 92.3076923076923%; - } - - &._1f4c7 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 78.57142857142857% 92.3076923076923%; - } - - &._1f5c3 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 85.71428571428571% 92.3076923076923%; - } - - &._1f5f3 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 92.85714285714286% 0%; - } - - &._1f5c4 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 92.85714285714286% 7.6923076923076925%; - } - - &._1f4cb { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 92.85714285714286% 15.384615384615385%; - } - - &._1f4c1 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 92.85714285714286% 23.076923076923077%; - } - - &._1f4c2 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 92.85714285714286% 30.76923076923077%; - } - - &._1f5c2 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 92.85714285714286% 38.46153846153846%; - } - - &._1f5de { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 92.85714285714286% 46.15384615384615%; - } - - &._1f4f0 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 92.85714285714286% 53.84615384615385%; - } - - &._1f4d3 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 92.85714285714286% 61.53846153846154%; - } - - &._1f4d4 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 92.85714285714286% 69.23076923076923%; - } - - &._1f4d2 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 92.85714285714286% 76.92307692307692%; - } - - &._1f4d5 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 92.85714285714286% 84.61538461538461%; - } - - &._1f4d7 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 92.85714285714286% 92.3076923076923%; - } - - &._1f4d8 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 0% 100%; - } - - &._1f4d9 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 7.142857142857143% 100%; - } - - &._1f4da { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 14.285714285714286% 100%; - } - - &._1f4d6 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 21.428571428571427% 100%; - } - - &._1f516 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 28.571428571428573% 100%; - } - - &._1f517 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 35.714285714285715% 100%; - } - - &._1f4ce { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 42.857142857142854% 100%; - } - - &._1f587 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 50% 100%; - } - - &._1f4d0 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 57.142857142857146% 100%; - } - - &._1f4cf { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 64.28571428571429% 100%; - } - - &._1f9f7 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 71.42857142857143% 100%; - } - - &._1f4cc { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 78.57142857142857% 100%; - } - - &._1f4cd { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 85.71428571428571% 100%; - } - - &._1f58a { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 92.85714285714286% 100%; - } - - &._1f58b { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 100% 0%; - } - - &._1f58c { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 100% 7.6923076923076925%; - } - - &._1f58d { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 100% 15.384615384615385%; - } - - &._1f4dd { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 100% 23.076923076923077%; - } - - &._270f { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 100% 30.76923076923077%; - } - - &._1f50d { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 100% 38.46153846153846%; - } - - &._1f50e { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 100% 46.15384615384615%; - } - - &._1f50f { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 100% 53.84615384615385%; - } - - &._1f510 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 100% 61.53846153846154%; - } - - &._1f512 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 100% 69.23076923076923%; - } - - &._1f513 { - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 100% 76.92307692307692%; - } - -} - - -.emojione-diversity._1f6c0-1f3fc { - background-image: url('/packages/emojione/objects-sprites.png'); - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 0% 0%; -} - -.emojione-diversity._1f6c0-1f3fb { - background-image: url('/packages/emojione/objects-sprites.png'); - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 71.42857142857143% 15.384615384615385%; -} - -.emojione-diversity._1f6c0-1f3fd { - background-image: url('/packages/emojione/objects-sprites.png'); - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 71.42857142857143% 30.76923076923077%; -} - -.emojione-diversity._1f6c0-1f3fe { - background-image: url('/packages/emojione/objects-sprites.png'); - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 71.42857142857143% 38.46153846153846%; -} - -.emojione-diversity._1f6c0-1f3ff { - background-image: url('/packages/emojione/objects-sprites.png'); - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 71.42857142857143% 46.15384615384615%; -} - -.emojione-diversity._1f6cc-1f3fb { - background-image: url('/packages/emojione/objects-sprites.png'); - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 78.57142857142857% 7.6923076923076925%; -} - -.emojione-diversity._1f6cc-1f3fc { - background-image: url('/packages/emojione/objects-sprites.png'); - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 78.57142857142857% 15.384615384615385%; -} - -.emojione-diversity._1f6cc-1f3fd { - background-image: url('/packages/emojione/objects-sprites.png'); - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 78.57142857142857% 23.076923076923077%; -} - -.emojione-diversity._1f6cc-1f3fe { - background-image: url('/packages/emojione/objects-sprites.png'); - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 78.57142857142857% 30.76923076923077%; -} - -.emojione-diversity._1f6cc-1f3ff { - background-image: url('/packages/emojione/objects-sprites.png'); - background-repeat: no-repeat; - background-size: 1500% 1400%; - background-position: 78.57142857142857% 38.46153846153846%; -} diff --git a/apps/meteor/app/emoji-emojione/client/people-sprites.css b/apps/meteor/app/emoji-emojione/client/people-sprites.css deleted file mode 100644 index 9e48a9516369a..0000000000000 --- a/apps/meteor/app/emoji-emojione/client/people-sprites.css +++ /dev/null @@ -1,8729 +0,0 @@ -/* stylelint-disable */ - -.emojione-people { - background-image: url('/packages/emojione/people-sprites.png'); - - &._2620 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 0%; - } - - &._1f600 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 2.857142857142857%; - } - - &._1f603 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 2.857142857142857%; - } - - &._1f604 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 0%; - } - - &._1f601 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 2.857142857142857%; - } - - &._1f606 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 5.714285714285714%; - } - - &._1f605 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 5.714285714285714%; - } - - &._1f602 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 5.714285714285714%; - } - - &._1f923 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 0%; - } - - &._263a { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 2.857142857142857%; - } - - &._1f60a { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 5.714285714285714%; - } - - &._1f607 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 8.571428571428571%; - } - - &._1f642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 8.571428571428571%; - } - - &._1f643 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 8.571428571428571%; - } - - &._1f609 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 8.571428571428571%; - } - - &._1f60c { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 0%; - } - - &._1f60d { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 2.857142857142857%; - } - - &._1f618 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 5.714285714285714%; - } - - &._1f970 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 8.571428571428571%; - } - - &._1f617 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 11.428571428571429%; - } - - &._1f619 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 11.428571428571429%; - } - - &._1f61a { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 11.428571428571429%; - } - - &._1f60b { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 11.428571428571429%; - } - - &._1f61b { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 11.428571428571429%; - } - - &._1f61d { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 0%; - } - - &._1f61c { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 2.857142857142857%; - } - - &._1f92a { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 5.714285714285714%; - } - - &._1f928 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 8.571428571428571%; - } - - &._1f9d0 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 11.428571428571429%; - } - - &._1f913 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 14.285714285714286%; - } - - &._1f60e { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 14.285714285714286%; - } - - &._1f929 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 14.285714285714286%; - } - - &._1f973 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 14.285714285714286%; - } - - &._1f60f { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 14.285714285714286%; - } - - &._1f612 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 14.285714285714286%; - } - - &._1f61e { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 0%; - } - - &._1f614 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 2.857142857142857%; - } - - &._1f61f { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 5.714285714285714%; - } - - &._1f615 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 8.571428571428571%; - } - - &._1f641 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 11.428571428571429%; - } - - &._1f623 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 14.285714285714286%; - } - - &._1f616 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 17.142857142857142%; - } - - &._1f62b { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 17.142857142857142%; - } - - &._1f629 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 17.142857142857142%; - } - - &._1f622 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 17.142857142857142%; - } - - &._1f62d { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 17.142857142857142%; - } - - &._1f624 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 17.142857142857142%; - } - - &._1f620 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 17.142857142857142%; - } - - &._1f621 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 0%; - } - - &._1f92c { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 2.857142857142857%; - } - - &._1f92f { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 5.714285714285714%; - } - - &._1f633 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 8.571428571428571%; - } - - &._1f631 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 11.428571428571429%; - } - - &._1f628 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 14.285714285714286%; - } - - &._1f630 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 17.142857142857142%; - } - - &._1f975 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 20%; - } - - &._1f976 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 20%; - } - - &._1f97a { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 20%; - } - - &._1f625 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 20%; - } - - &._1f613 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 20%; - } - - &._1f917 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 20%; - } - - &._1f914 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 20%; - } - - &._1f92d { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 20%; - } - - &._1f92b { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 0%; - } - - &._1f925 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 2.857142857142857%; - } - - &._1f636 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 5.714285714285714%; - } - - &._1f610 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 8.571428571428571%; - } - - &._1f611 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 11.428571428571429%; - } - - &._1f62c { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 14.285714285714286%; - } - - &._1f644 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 17.142857142857142%; - } - - &._1f62f { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 20%; - } - - &._1f626 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 22.857142857142858%; - } - - &._1f627 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 22.857142857142858%; - } - - &._1f62e { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 22.857142857142858%; - } - - &._1f632 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 22.857142857142858%; - } - - &._1f634 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 22.857142857142858%; - } - - &._1f924 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 22.857142857142858%; - } - - &._1f62a { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 22.857142857142858%; - } - - &._1f635 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 22.857142857142858%; - } - - &._1f910 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 22.857142857142858%; - } - - &._1f974 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 0%; - } - - &._1f922 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 2.857142857142857%; - } - - &._1f92e { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 5.714285714285714%; - } - - &._1f927 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 8.571428571428571%; - } - - &._1f637 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 11.428571428571429%; - } - - &._1f912 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 14.285714285714286%; - } - - &._1f915 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 17.142857142857142%; - } - - &._1f911 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 20%; - } - - &._1f920 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 22.857142857142858%; - } - - &._1f608 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 25.714285714285715%; - } - - &._1f47f { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 25.714285714285715%; - } - - &._1f479 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 25.714285714285715%; - } - - &._1f47a { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 25.714285714285715%; - } - - &._1f921 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 25.714285714285715%; - } - - &._1f4a9 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 25.714285714285715%; - } - - &._1f47b { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 25.714285714285715%; - } - - &._1f480 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 25.714285714285715%; - } - - &._1f47d { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 25.714285714285715%; - } - - &._1f47e { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 25.714285714285715%; - } - - &._1f916 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 0%; - } - - &._1f383 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 2.857142857142857%; - } - - &._1f63a { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 5.714285714285714%; - } - - &._1f638 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 8.571428571428571%; - } - - &._1f639 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 11.428571428571429%; - } - - &._1f63b { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 14.285714285714286%; - } - - &._1f63c { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 17.142857142857142%; - } - - &._1f63d { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 20%; - } - - &._1f640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 22.857142857142858%; - } - - &._1f63f { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 25.714285714285715%; - } - - &._1f63e { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 28.571428571428573%; - } - - &._1f932 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 28.571428571428573%; - } - - &._1f450 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 28.571428571428573%; - } - - &._1f64c { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 5.714285714285714%; - } - - &._1f44f { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 22.857142857142858%; - } - - &._1f91d { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 31.428571428571427%; - } - - &._1f44d { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 31.428571428571427%; - } - - &._1f44e { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 31.428571428571427%; - } - - &._1f44a { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 11.428571428571429%; - } - - &._270a { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 28.571428571428573%; - } - - &._1f91b { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 34.285714285714285%; - } - - &._1f91c { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 34.285714285714285%; - } - - &._1f91e { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 8.571428571428571%; - } - - &._270c { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 25.714285714285715%; - } - - &._1f91f { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 37.142857142857146%; - } - - &._1f918 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 37.142857142857146%; - } - - &._1f44c { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 0%; - } - - &._1f448 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 17.142857142857142%; - } - - &._1f449 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 34.285714285714285%; - } - - &._1f446 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 40%; - } - - &._1f447 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 40%; - } - - &._261d { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 2.857142857142857%; - } - - &._270b { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 20%; - } - - &._1f91a { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 37.142857142857146%; - } - - &._1f590 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 42.857142857142854%; - } - - &._1f596 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 42.857142857142854%; - } - - &._1f44b { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 0%; - } - - &._1f919 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 17.142857142857142%; - } - - &._1f4aa { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 34.285714285714285%; - } - - &._1f9b5 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 45.714285714285715%; - } - - &._1f9b6 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 45.714285714285715%; - } - - &._1f595 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 45.714285714285715%; - } - - &._270d { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 8.571428571428571%; - } - - &._1f64f { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 25.714285714285715%; - } - - &._1f48d { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 42.857142857142854%; - } - - &._1f484 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 45.714285714285715%; - } - - &._1f48b { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 48.57142857142857%; - } - - &._1f444 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 48.57142857142857%; - } - - &._1f445 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 48.57142857142857%; - } - - &._1f442 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 48.57142857142857%; - } - - &._1f443 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 48.57142857142857%; - } - - &._1f463 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 48.57142857142857%; - } - - &._1f441 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 48.57142857142857%; - } - - &._1f440 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 48.57142857142857%; - } - - &._1f9e0 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 0%; - } - - &._1f9b4 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 2.857142857142857%; - } - - &._1f9b7 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 5.714285714285714%; - } - - &._1f5e3 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 8.571428571428571%; - } - - &._1f464 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 11.428571428571429%; - } - - &._1f465 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 14.285714285714286%; - } - - &._1f476 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 17.142857142857142%; - } - - &._1f467 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 34.285714285714285%; - } - - &._1f9d2 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 51.42857142857143%; - } - - &._1f466 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 51.42857142857143%; - } - - &._1f469 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 51.42857142857143%; - } - - &._1f9d1 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 51.42857142857143%; - } - - &._1f468 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 14.285714285714286%; - } - - &._1f471 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 31.428571428571427%; - } - - &._1f471-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 48.57142857142857%; - } - - &._1f471-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 54.285714285714285%; - } - - &._1f469-1f9b0 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 54.285714285714285%; - } - - &._1f468-1f9b0 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 54.285714285714285%; - } - - &._1f469-1f9b1 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 5.714285714285714%; - } - - &._1f468-1f9b1 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 22.857142857142858%; - } - - &._1f469-1f9b3 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 40%; - } - - &._1f468-1f9b3 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 57.142857142857146%; - } - - &._1f469-1f9b2 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 57.142857142857146%; - } - - &._1f468-1f9b2 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 57.142857142857146%; - } - - &._1f9d4 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 57.142857142857146%; - } - - &._1f475 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 8.571428571428571%; - } - - &._1f9d3 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 25.714285714285715%; - } - - &._1f474 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 42.857142857142854%; - } - - &._1f472 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 60%; - } - - &._1f473 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 60%; - } - - &._1f473-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 60%; - } - - &._1f473-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 60%; - } - - &._1f9d5 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 5.714285714285714%; - } - - &._1f46e { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 22.857142857142858%; - } - - &._1f46e-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 40%; - } - - &._1f46e-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 57.142857142857146%; - } - - &._1f477 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 62.857142857142854%; - } - - &._1f477-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 62.857142857142854%; - } - - &._1f477-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 62.857142857142854%; - } - - &._1f482 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 62.857142857142854%; - } - - &._1f482-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 14.285714285714286%; - } - - &._1f482-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 31.428571428571427%; - } - - &._1f575 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 48.57142857142857%; - } - - &._1f575-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 65.71428571428571%; - } - - &._1f575-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 65.71428571428571%; - } - - &._1f469-2695 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 65.71428571428571%; - } - - &._1f468-2695 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 65.71428571428571%; - } - - &._1f469-1f33e { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 0%; - } - - &._1f468-1f33e { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 17.142857142857142%; - } - - &._1f469-1f373 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 34.285714285714285%; - } - - &._1f468-1f373 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 51.42857142857143%; - } - - &._1f469-1f393 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 68.57142857142857%; - } - - &._1f468-1f393 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 68.57142857142857%; - } - - &._1f469-1f3a4 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 68.57142857142857%; - } - - &._1f468-1f3a4 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 68.57142857142857%; - } - - &._2639 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 68.57142857142857%; - } - - &._1f469-1f3eb { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 68.57142857142857%; - } - - &._1f468-1f3eb { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 14.285714285714286%; - } - - &._1f469-1f3ed { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 31.428571428571427%; - } - - &._1f468-1f3ed { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 48.57142857142857%; - } - - &._1f469-1f4bb { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 65.71428571428571%; - } - - &._1f468-1f4bb { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 71.42857142857143%; - } - - &._1f469-1f4bc { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 71.42857142857143%; - } - - &._1f468-1f4bc { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 71.42857142857143%; - } - - &._1f469-1f527 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 71.42857142857143%; - } - - &._1f468-1f527 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 5.714285714285714%; - } - - &._1f469-1f52c { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 22.857142857142858%; - } - - &._1f468-1f52c { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 40%; - } - - &._1f469-1f3a8 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 57.142857142857146%; - } - - &._1f468-1f3a8 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 74.28571428571429%; - } - - &._1f469-1f692 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 74.28571428571429%; - } - - &._1f468-1f692 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 74.28571428571429%; - } - - &._1f469-2708 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 74.28571428571429%; - } - - &._1f468-2708 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 74.28571428571429%; - } - - &._1f469-1f680 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 8.571428571428571%; - } - - &._1f468-1f680 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 25.714285714285715%; - } - - &._1f469-2696 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 42.857142857142854%; - } - - &._1f468-2696 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 60%; - } - - &._1f470 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 77.14285714285714%; - } - - &._1f935 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 77.14285714285714%; - } - - &._1f478 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 77.14285714285714%; - } - - &._1f934 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 77.14285714285714%; - } - - &._1f936 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 77.14285714285714%; - } - - &._1f385 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 5.714285714285714%; - } - - &._1f9b8 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 22.857142857142858%; - } - - &._1f9b8-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 40%; - } - - &._1f9b8-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 57.142857142857146%; - } - - &._1f9b9 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 74.28571428571429%; - } - - &._1f9b9-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 80%; - } - - &._1f9b9-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 80%; - } - - &._1f9d9 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 80%; - } - - &._1f9d9-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 80%; - } - - &._1f9d9-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 80%; - } - - &._1f9dd { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 14.285714285714286%; - } - - &._1f9dd-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 31.428571428571427%; - } - - &._1f9dd-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 48.57142857142857%; - } - - &._1f9db { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 65.71428571428571%; - } - - &._1f9db-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 82.85714285714286%; - } - - &._1f9db-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 82.85714285714286%; - } - - &._1f9df { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 82.85714285714286%; - } - - &._1f9df-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 82.85714285714286%; - } - - &._1f9df-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 82.85714285714286%; - } - - &._1f9de { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 82.85714285714286%; - } - - &._1f9de-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 82.85714285714286%; - } - - &._1f9de-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 82.85714285714286%; - } - - &._1f9dc { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 82.85714285714286%; - } - - &._1f9dc-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 82.85714285714286%; - } - - &._1f9dc-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 0%; - } - - &._1f9da { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 17.142857142857142%; - } - - &._1f9da-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 34.285714285714285%; - } - - &._1f9da-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 51.42857142857143%; - } - - &._1f47c { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 68.57142857142857%; - } - - &._1f930 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 85.71428571428571%; - } - - &._1f931 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 85.71428571428571%; - } - - &._1f647 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 85.71428571428571%; - } - - &._1f647-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 85.71428571428571%; - } - - &._1f647-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 85.71428571428571%; - } - - &._1f481 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 85.71428571428571%; - } - - &._1f481-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 14.285714285714286%; - } - - &._1f481-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 31.428571428571427%; - } - - &._1f645 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 48.57142857142857%; - } - - &._1f645-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 65.71428571428571%; - } - - &._1f645-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 82.85714285714286%; - } - - &._1f646 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 88.57142857142857%; - } - - &._1f646-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 88.57142857142857%; - } - - &._1f646-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 88.57142857142857%; - } - - &._1f64b { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 88.57142857142857%; - } - - &._1f64b-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 88.57142857142857%; - } - - &._1f64b-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 5.714285714285714%; - } - - &._1f926 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 22.857142857142858%; - } - - &._1f926-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 40%; - } - - &._1f926-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 57.142857142857146%; - } - - &._1f937 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 74.28571428571429%; - } - - &._1f937-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 91.42857142857143%; - } - - &._1f937-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 91.42857142857143%; - } - - &._1f64e { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 91.42857142857143%; - } - - &._1f64e-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 91.42857142857143%; - } - - &._1f64e-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 91.42857142857143%; - } - - &._1f64d { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 91.42857142857143%; - } - - &._1f64d-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 8.571428571428571%; - } - - &._1f64d-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 25.714285714285715%; - } - - &._1f487 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 42.857142857142854%; - } - - &._1f487-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 60%; - } - - &._1f487-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 77.14285714285714%; - } - - &._1f486 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 94.28571428571429%; - } - - &._1f486-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 94.28571428571429%; - } - - &._1f486-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 94.28571428571429%; - } - - &._1f9d6 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 94.28571428571429%; - } - - &._1f9d6-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 94.28571428571429%; - } - - &._1f9d6-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 94.28571428571429%; - } - - &._1f485 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 5.714285714285714%; - } - - &._1f933 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 22.857142857142858%; - } - - &._1f483 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 40%; - } - - &._1f57a { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 57.142857142857146%; - } - - &._1f46f { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 74.28571428571429%; - } - - &._1f46f-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 77.14285714285714%; - } - - &._1f46f-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 80%; - } - - &._1f574 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 82.85714285714286%; - } - - &._1f6b6 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 97.14285714285714%; - } - - &._1f6b6-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 97.14285714285714%; - } - - &._1f6b6-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 97.14285714285714%; - } - - &._1f3c3 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 97.14285714285714%; - } - - &._1f3c3-2640 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 97.14285714285714%; - } - - &._1f3c3-2642 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 97.14285714285714%; - } - - &._1f46b { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 5.714285714285714%; - } - - &._1f46d { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 8.571428571428571%; - } - - &._1f46c { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 11.428571428571429%; - } - - &._1f491 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 14.285714285714286%; - } - - &._1f469-2764-1f468 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 17.142857142857142%; - } - - &._1f469-2764-1f469 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 20%; - } - - &._1f468-2764-1f468 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 22.857142857142858%; - } - - &._1f48f { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 25.714285714285715%; - } - - &._1f469-2764-1f48b-1f468 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 28.571428571428573%; - } - - &._1f469-2764-1f48b-1f469 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 31.428571428571427%; - } - - &._1f468-2764-1f48b-1f468 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 34.285714285714285%; - } - - &._1f46a { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 37.142857142857146%; - } - - &._1f468-1f469-1f466 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 40%; - } - - &._1f468-1f469-1f467 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 42.857142857142854%; - } - - &._1f468-1f469-1f467-1f466 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 45.714285714285715%; - } - - &._1f468-1f469-1f466-1f466 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 48.57142857142857%; - } - - &._1f468-1f469-1f467-1f467 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 51.42857142857143%; - } - - &._1f469-1f469-1f466 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 54.285714285714285%; - } - - &._1f469-1f469-1f467 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 57.142857142857146%; - } - - &._1f469-1f469-1f467-1f466 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 60%; - } - - &._1f469-1f469-1f466-1f466 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 62.857142857142854%; - } - - &._1f469-1f469-1f467-1f467 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 65.71428571428571%; - } - - &._1f468-1f468-1f466 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 68.57142857142857%; - } - - &._1f468-1f468-1f467 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 71.42857142857143%; - } - - &._1f468-1f468-1f467-1f466 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 74.28571428571429%; - } - - &._1f468-1f468-1f466-1f466 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 77.14285714285714%; - } - - &._1f468-1f468-1f467-1f467 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 80%; - } - - &._1f469-1f466 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 82.85714285714286%; - } - - &._1f469-1f467 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 85.71428571428571%; - } - - &._1f469-1f467-1f466 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 88.57142857142857%; - } - - &._1f469-1f466-1f466 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 91.42857142857143%; - } - - &._1f469-1f467-1f467 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 94.28571428571429%; - } - - &._1f468-1f466 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 97.14285714285714%; - } - - &._1f468-1f467 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 100%; - } - - &._1f468-1f467-1f466 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 100%; - } - - &._1f468-1f466-1f466 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 100%; - } - - &._1f468-1f467-1f467 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 100%; - } - - &._1f9e5 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 100%; - } - - &._1f45a { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 100%; - } - - &._1f455 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 100%; - } - - &._1f456 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 100%; - } - - &._1f454 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 100%; - } - - &._1f457 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 100%; - } - - &._1f459 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 100%; - } - - &._1f458 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 100%; - } - - &._1f97c { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 100%; - } - - &._1f460 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 100%; - } - - &._1f461 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 100%; - } - - &._1f462 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 100%; - } - - &._1f45e { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 100%; - } - - &._1f45f { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 100%; - } - - &._1f97e { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 100%; - } - - &._1f97f { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 100%; - } - - &._1f9e6 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 100%; - } - - &._1f9e4 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 100%; - } - - &._1f9e3 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 100%; - } - - &._1f3a9 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 100%; - } - - &._1f9e2 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 100%; - } - - &._1f452 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 100%; - } - - &._1f393 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 100%; - } - - &._26d1 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 100%; - } - - &._1f451 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 100%; - } - - &._1f45d { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 100%; - } - - &._1f45b { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 100%; - } - - &._1f45c { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 100%; - } - - &._1f4bc { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 100%; - } - - &._1f392 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 100%; - } - - &._1f453 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 100%; - } - - &._1f576 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 100%; - } - - &._1f97d { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 100% 0%; - } - - &._1f302 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 100% 2.857142857142857%; - } - - &._1f9b0 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 100% 5.714285714285714%; - } - - &._1f9b1 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 100% 8.571428571428571%; - } - - &._1f9b3 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 100% 11.428571428571429%; - } - - &._1f9b2 { - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 100% 14.285714285714286%; - } - -} - - -.emojione-diversity._1f468-1f3fb-1f3a4 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 0%; -} - -.emojione-diversity._1f932-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 28.571428571428573%; -} - -.emojione-diversity._1f932-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 28.571428571428573%; -} - -.emojione-diversity._1f932-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 28.571428571428573%; -} - -.emojione-diversity._1f932-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 28.571428571428573%; -} - -.emojione-diversity._1f932-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 28.571428571428573%; -} - -.emojione-diversity._1f450-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 28.571428571428573%; -} - -.emojione-diversity._1f450-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 28.571428571428573%; -} - -.emojione-diversity._1f450-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 28.571428571428573%; -} - -.emojione-diversity._1f450-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 0%; -} - -.emojione-diversity._1f450-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 2.857142857142857%; -} - -.emojione-diversity._1f64c-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 8.571428571428571%; -} - -.emojione-diversity._1f64c-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 11.428571428571429%; -} - -.emojione-diversity._1f64c-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 14.285714285714286%; -} - -.emojione-diversity._1f64c-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 17.142857142857142%; -} - -.emojione-diversity._1f64c-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 20%; -} - -.emojione-diversity._1f44f-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 25.714285714285715%; -} - -.emojione-diversity._1f44f-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 28.571428571428573%; -} - -.emojione-diversity._1f44f-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 31.428571428571427%; -} - -.emojione-diversity._1f44f-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 31.428571428571427%; -} - -.emojione-diversity._1f44f-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 31.428571428571427%; -} - -.emojione-diversity._1f44d-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 31.428571428571427%; -} - -.emojione-diversity._1f44d-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 31.428571428571427%; -} - -.emojione-diversity._1f44d-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 31.428571428571427%; -} - -.emojione-diversity._1f44d-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 31.428571428571427%; -} - -.emojione-diversity._1f44d-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 31.428571428571427%; -} - -.emojione-diversity._1f44e-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 31.428571428571427%; -} - -.emojione-diversity._1f44e-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 0%; -} - -.emojione-diversity._1f44e-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 2.857142857142857%; -} - -.emojione-diversity._1f44e-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 5.714285714285714%; -} - -.emojione-diversity._1f44e-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 8.571428571428571%; -} - -.emojione-diversity._1f44a-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 14.285714285714286%; -} - -.emojione-diversity._1f44a-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 17.142857142857142%; -} - -.emojione-diversity._1f44a-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 20%; -} - -.emojione-diversity._1f44a-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 22.857142857142858%; -} - -.emojione-diversity._1f44a-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 25.714285714285715%; -} - -.emojione-diversity._270a-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 31.428571428571427%; -} - -.emojione-diversity._270a-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 34.285714285714285%; -} - -.emojione-diversity._270a-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 34.285714285714285%; -} - -.emojione-diversity._270a-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 34.285714285714285%; -} - -.emojione-diversity._270a-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 34.285714285714285%; -} - -.emojione-diversity._1f91b-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 34.285714285714285%; -} - -.emojione-diversity._1f91b-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 34.285714285714285%; -} - -.emojione-diversity._1f91b-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 34.285714285714285%; -} - -.emojione-diversity._1f91b-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 34.285714285714285%; -} - -.emojione-diversity._1f91b-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 34.285714285714285%; -} - -.emojione-diversity._1f91c-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 34.285714285714285%; -} - -.emojione-diversity._1f91c-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 34.285714285714285%; -} - -.emojione-diversity._1f91c-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 0%; -} - -.emojione-diversity._1f91c-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 2.857142857142857%; -} - -.emojione-diversity._1f91c-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 5.714285714285714%; -} - -.emojione-diversity._1f91e-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 11.428571428571429%; -} - -.emojione-diversity._1f91e-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 14.285714285714286%; -} - -.emojione-diversity._1f91e-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 17.142857142857142%; -} - -.emojione-diversity._1f91e-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 20%; -} - -.emojione-diversity._1f91e-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 22.857142857142858%; -} - -.emojione-diversity._270c-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 28.571428571428573%; -} - -.emojione-diversity._270c-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 31.428571428571427%; -} - -.emojione-diversity._270c-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 34.285714285714285%; -} - -.emojione-diversity._270c-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 37.142857142857146%; -} - -.emojione-diversity._270c-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 37.142857142857146%; -} - -.emojione-diversity._1f91f-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 37.142857142857146%; -} - -.emojione-diversity._1f91f-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 37.142857142857146%; -} - -.emojione-diversity._1f91f-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 37.142857142857146%; -} - -.emojione-diversity._1f91f-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 37.142857142857146%; -} - -.emojione-diversity._1f91f-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 37.142857142857146%; -} - -.emojione-diversity._1f918-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 37.142857142857146%; -} - -.emojione-diversity._1f918-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 37.142857142857146%; -} - -.emojione-diversity._1f918-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 37.142857142857146%; -} - -.emojione-diversity._1f918-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 37.142857142857146%; -} - -.emojione-diversity._1f918-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 37.142857142857146%; -} - -.emojione-diversity._1f44c-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 2.857142857142857%; -} - -.emojione-diversity._1f44c-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 5.714285714285714%; -} - -.emojione-diversity._1f44c-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 8.571428571428571%; -} - -.emojione-diversity._1f44c-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 11.428571428571429%; -} - -.emojione-diversity._1f44c-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 14.285714285714286%; -} - -.emojione-diversity._1f448-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 20%; -} - -.emojione-diversity._1f448-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 22.857142857142858%; -} - -.emojione-diversity._1f448-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 25.714285714285715%; -} - -.emojione-diversity._1f448-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 28.571428571428573%; -} - -.emojione-diversity._1f448-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 31.428571428571427%; -} - -.emojione-diversity._1f449-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 37.142857142857146%; -} - -.emojione-diversity._1f449-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 40%; -} - -.emojione-diversity._1f449-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 40%; -} - -.emojione-diversity._1f449-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 40%; -} - -.emojione-diversity._1f449-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 40%; -} - -.emojione-diversity._1f446-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 40%; -} - -.emojione-diversity._1f446-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 40%; -} - -.emojione-diversity._1f446-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 40%; -} - -.emojione-diversity._1f446-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 40%; -} - -.emojione-diversity._1f446-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 40%; -} - -.emojione-diversity._1f447-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 40%; -} - -.emojione-diversity._1f447-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 40%; -} - -.emojione-diversity._1f447-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 40%; -} - -.emojione-diversity._1f447-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 40%; -} - -.emojione-diversity._1f447-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 0%; -} - -.emojione-diversity._261d-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 5.714285714285714%; -} - -.emojione-diversity._261d-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 8.571428571428571%; -} - -.emojione-diversity._261d-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 11.428571428571429%; -} - -.emojione-diversity._261d-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 14.285714285714286%; -} - -.emojione-diversity._261d-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 17.142857142857142%; -} - -.emojione-diversity._270b-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 22.857142857142858%; -} - -.emojione-diversity._270b-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 25.714285714285715%; -} - -.emojione-diversity._270b-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 28.571428571428573%; -} - -.emojione-diversity._270b-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 31.428571428571427%; -} - -.emojione-diversity._270b-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 34.285714285714285%; -} - -.emojione-diversity._1f91a-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 40%; -} - -.emojione-diversity._1f91a-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 42.857142857142854%; -} - -.emojione-diversity._1f91a-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 42.857142857142854%; -} - -.emojione-diversity._1f91a-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 42.857142857142854%; -} - -.emojione-diversity._1f91a-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 42.857142857142854%; -} - -.emojione-diversity._1f590-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 42.857142857142854%; -} - -.emojione-diversity._1f590-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 42.857142857142854%; -} - -.emojione-diversity._1f590-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 42.857142857142854%; -} - -.emojione-diversity._1f590-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 42.857142857142854%; -} - -.emojione-diversity._1f590-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 42.857142857142854%; -} - -.emojione-diversity._1f596-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 42.857142857142854%; -} - -.emojione-diversity._1f596-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 42.857142857142854%; -} - -.emojione-diversity._1f596-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 42.857142857142854%; -} - -.emojione-diversity._1f596-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 42.857142857142854%; -} - -.emojione-diversity._1f596-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 42.857142857142854%; -} - -.emojione-diversity._1f44b-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 2.857142857142857%; -} - -.emojione-diversity._1f44b-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 5.714285714285714%; -} - -.emojione-diversity._1f44b-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 8.571428571428571%; -} - -.emojione-diversity._1f44b-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 11.428571428571429%; -} - -.emojione-diversity._1f44b-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 14.285714285714286%; -} - -.emojione-diversity._1f919-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 20%; -} - -.emojione-diversity._1f919-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 22.857142857142858%; -} - -.emojione-diversity._1f919-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 25.714285714285715%; -} - -.emojione-diversity._1f919-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 28.571428571428573%; -} - -.emojione-diversity._1f919-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 31.428571428571427%; -} - -.emojione-diversity._1f4aa-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 37.142857142857146%; -} - -.emojione-diversity._1f4aa-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 40%; -} - -.emojione-diversity._1f4aa-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 42.857142857142854%; -} - -.emojione-diversity._1f4aa-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 45.714285714285715%; -} - -.emojione-diversity._1f4aa-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 45.714285714285715%; -} - -.emojione-diversity._1f9b5-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 45.714285714285715%; -} - -.emojione-diversity._1f9b5-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 45.714285714285715%; -} - -.emojione-diversity._1f9b5-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 45.714285714285715%; -} - -.emojione-diversity._1f9b5-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 45.714285714285715%; -} - -.emojione-diversity._1f9b5-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 45.714285714285715%; -} - -.emojione-diversity._1f9b6-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 45.714285714285715%; -} - -.emojione-diversity._1f9b6-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 45.714285714285715%; -} - -.emojione-diversity._1f9b6-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 45.714285714285715%; -} - -.emojione-diversity._1f9b6-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 45.714285714285715%; -} - -.emojione-diversity._1f9b6-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 45.714285714285715%; -} - -.emojione-diversity._1f595-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 45.714285714285715%; -} - -.emojione-diversity._1f595-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 45.714285714285715%; -} - -.emojione-diversity._1f595-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 0%; -} - -.emojione-diversity._1f595-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 2.857142857142857%; -} - -.emojione-diversity._1f595-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 5.714285714285714%; -} - -.emojione-diversity._270d-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 11.428571428571429%; -} - -.emojione-diversity._270d-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 14.285714285714286%; -} - -.emojione-diversity._270d-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 17.142857142857142%; -} - -.emojione-diversity._270d-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 20%; -} - -.emojione-diversity._270d-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 22.857142857142858%; -} - -.emojione-diversity._1f64f-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 28.571428571428573%; -} - -.emojione-diversity._1f64f-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 31.428571428571427%; -} - -.emojione-diversity._1f64f-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 34.285714285714285%; -} - -.emojione-diversity._1f64f-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 37.142857142857146%; -} - -.emojione-diversity._1f64f-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 40%; -} - -.emojione-diversity._1f442-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 48.57142857142857%; -} - -.emojione-diversity._1f442-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 48.57142857142857%; -} - -.emojione-diversity._1f442-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 48.57142857142857%; -} - -.emojione-diversity._1f442-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 48.57142857142857%; -} - -.emojione-diversity._1f442-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 48.57142857142857%; -} - -.emojione-diversity._1f443-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 48.57142857142857%; -} - -.emojione-diversity._1f443-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 48.57142857142857%; -} - -.emojione-diversity._1f443-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 48.57142857142857%; -} - -.emojione-diversity._1f443-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 48.57142857142857%; -} - -.emojione-diversity._1f443-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 48.57142857142857%; -} - -.emojione-diversity._1f476-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 20%; -} - -.emojione-diversity._1f476-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 22.857142857142858%; -} - -.emojione-diversity._1f476-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 25.714285714285715%; -} - -.emojione-diversity._1f476-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 28.571428571428573%; -} - -.emojione-diversity._1f476-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 31.428571428571427%; -} - -.emojione-diversity._1f467-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 37.142857142857146%; -} - -.emojione-diversity._1f467-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 40%; -} - -.emojione-diversity._1f467-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 42.857142857142854%; -} - -.emojione-diversity._1f467-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 45.714285714285715%; -} - -.emojione-diversity._1f467-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 48.57142857142857%; -} - -.emojione-diversity._1f9d2-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 51.42857142857143%; -} - -.emojione-diversity._1f9d2-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 51.42857142857143%; -} - -.emojione-diversity._1f9d2-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 51.42857142857143%; -} - -.emojione-diversity._1f9d2-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 51.42857142857143%; -} - -.emojione-diversity._1f9d2-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 51.42857142857143%; -} - -.emojione-diversity._1f466-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 51.42857142857143%; -} - -.emojione-diversity._1f466-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 51.42857142857143%; -} - -.emojione-diversity._1f466-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 51.42857142857143%; -} - -.emojione-diversity._1f466-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 51.42857142857143%; -} - -.emojione-diversity._1f466-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 51.42857142857143%; -} - -.emojione-diversity._1f469-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 51.42857142857143%; -} - -.emojione-diversity._1f469-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 51.42857142857143%; -} - -.emojione-diversity._1f469-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 51.42857142857143%; -} - -.emojione-diversity._1f469-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 51.42857142857143%; -} - -.emojione-diversity._1f469-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 51.42857142857143%; -} - -.emojione-diversity._1f9d1-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 0%; -} - -.emojione-diversity._1f9d1-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 2.857142857142857%; -} - -.emojione-diversity._1f9d1-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 5.714285714285714%; -} - -.emojione-diversity._1f9d1-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 8.571428571428571%; -} - -.emojione-diversity._1f9d1-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 11.428571428571429%; -} - -.emojione-diversity._1f468-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 17.142857142857142%; -} - -.emojione-diversity._1f468-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 20%; -} - -.emojione-diversity._1f468-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 22.857142857142858%; -} - -.emojione-diversity._1f468-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 25.714285714285715%; -} - -.emojione-diversity._1f468-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 28.571428571428573%; -} - -.emojione-diversity._1f471-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 34.285714285714285%; -} - -.emojione-diversity._1f471-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 37.142857142857146%; -} - -.emojione-diversity._1f471-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 40%; -} - -.emojione-diversity._1f471-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 42.857142857142854%; -} - -.emojione-diversity._1f471-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 45.714285714285715%; -} - -.emojione-diversity._1f471-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 51.42857142857143%; -} - -.emojione-diversity._1f471-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 54.285714285714285%; -} - -.emojione-diversity._1f471-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 54.285714285714285%; -} - -.emojione-diversity._1f471-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 54.285714285714285%; -} - -.emojione-diversity._1f471-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 54.285714285714285%; -} - -.emojione-diversity._1f471-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 54.285714285714285%; -} - -.emojione-diversity._1f471-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 54.285714285714285%; -} - -.emojione-diversity._1f471-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 54.285714285714285%; -} - -.emojione-diversity._1f471-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 54.285714285714285%; -} - -.emojione-diversity._1f471-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 54.285714285714285%; -} - -.emojione-diversity._1f469-1f3fb-1f9b0 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 54.285714285714285%; -} - -.emojione-diversity._1f469-1f3fc-1f9b0 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 54.285714285714285%; -} - -.emojione-diversity._1f469-1f3fd-1f9b0 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 54.285714285714285%; -} - -.emojione-diversity._1f469-1f3fe-1f9b0 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 54.285714285714285%; -} - -.emojione-diversity._1f469-1f3ff-1f9b0 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 54.285714285714285%; -} - -.emojione-diversity._1f468-1f3fb-1f9b0 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 54.285714285714285%; -} - -.emojione-diversity._1f468-1f3fc-1f9b0 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 54.285714285714285%; -} - -.emojione-diversity._1f468-1f3fd-1f9b0 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 54.285714285714285%; -} - -.emojione-diversity._1f468-1f3fe-1f9b0 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 0%; -} - -.emojione-diversity._1f468-1f3ff-1f9b0 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 2.857142857142857%; -} - -.emojione-diversity._1f469-1f3fb-1f9b1 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 8.571428571428571%; -} - -.emojione-diversity._1f469-1f3fc-1f9b1 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 11.428571428571429%; -} - -.emojione-diversity._1f469-1f3fd-1f9b1 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 14.285714285714286%; -} - -.emojione-diversity._1f469-1f3fe-1f9b1 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 17.142857142857142%; -} - -.emojione-diversity._1f469-1f3ff-1f9b1 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 20%; -} - -.emojione-diversity._1f468-1f3fb-1f9b1 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 25.714285714285715%; -} - -.emojione-diversity._1f468-1f3fc-1f9b1 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 28.571428571428573%; -} - -.emojione-diversity._1f468-1f3fd-1f9b1 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 31.428571428571427%; -} - -.emojione-diversity._1f468-1f3fe-1f9b1 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 34.285714285714285%; -} - -.emojione-diversity._1f468-1f3ff-1f9b1 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 37.142857142857146%; -} - -.emojione-diversity._1f469-1f3fb-1f9b3 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 42.857142857142854%; -} - -.emojione-diversity._1f469-1f3fc-1f9b3 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 45.714285714285715%; -} - -.emojione-diversity._1f469-1f3fd-1f9b3 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 48.57142857142857%; -} - -.emojione-diversity._1f469-1f3fe-1f9b3 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 51.42857142857143%; -} - -.emojione-diversity._1f469-1f3ff-1f9b3 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 54.285714285714285%; -} - -.emojione-diversity._1f468-1f3fb-1f9b3 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 57.142857142857146%; -} - -.emojione-diversity._1f468-1f3fc-1f9b3 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 57.142857142857146%; -} - -.emojione-diversity._1f468-1f3fd-1f9b3 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 57.142857142857146%; -} - -.emojione-diversity._1f468-1f3fe-1f9b3 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 57.142857142857146%; -} - -.emojione-diversity._1f468-1f3ff-1f9b3 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 57.142857142857146%; -} - -.emojione-diversity._1f469-1f3fb-1f9b2 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 57.142857142857146%; -} - -.emojione-diversity._1f469-1f3fc-1f9b2 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 57.142857142857146%; -} - -.emojione-diversity._1f469-1f3fd-1f9b2 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 57.142857142857146%; -} - -.emojione-diversity._1f469-1f3fe-1f9b2 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 57.142857142857146%; -} - -.emojione-diversity._1f469-1f3ff-1f9b2 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 57.142857142857146%; -} - -.emojione-diversity._1f468-1f3fb-1f9b2 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 57.142857142857146%; -} - -.emojione-diversity._1f468-1f3fc-1f9b2 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 57.142857142857146%; -} - -.emojione-diversity._1f468-1f3fd-1f9b2 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 57.142857142857146%; -} - -.emojione-diversity._1f468-1f3fe-1f9b2 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 57.142857142857146%; -} - -.emojione-diversity._1f468-1f3ff-1f9b2 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 57.142857142857146%; -} - -.emojione-diversity._1f9d4-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 57.142857142857146%; -} - -.emojione-diversity._1f9d4-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 57.142857142857146%; -} - -.emojione-diversity._1f9d4-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 0%; -} - -.emojione-diversity._1f9d4-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 2.857142857142857%; -} - -.emojione-diversity._1f9d4-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 5.714285714285714%; -} - -.emojione-diversity._1f475-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 11.428571428571429%; -} - -.emojione-diversity._1f475-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 14.285714285714286%; -} - -.emojione-diversity._1f475-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 17.142857142857142%; -} - -.emojione-diversity._1f475-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 20%; -} - -.emojione-diversity._1f475-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 22.857142857142858%; -} - -.emojione-diversity._1f9d3-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 28.571428571428573%; -} - -.emojione-diversity._1f9d3-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 31.428571428571427%; -} - -.emojione-diversity._1f9d3-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 34.285714285714285%; -} - -.emojione-diversity._1f9d3-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 37.142857142857146%; -} - -.emojione-diversity._1f9d3-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 40%; -} - -.emojione-diversity._1f474-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 45.714285714285715%; -} - -.emojione-diversity._1f474-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 48.57142857142857%; -} - -.emojione-diversity._1f474-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 51.42857142857143%; -} - -.emojione-diversity._1f474-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 54.285714285714285%; -} - -.emojione-diversity._1f474-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 57.142857142857146%; -} - -.emojione-diversity._1f472-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 60%; -} - -.emojione-diversity._1f472-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 60%; -} - -.emojione-diversity._1f472-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 60%; -} - -.emojione-diversity._1f472-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 60%; -} - -.emojione-diversity._1f472-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 60%; -} - -.emojione-diversity._1f473-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 60%; -} - -.emojione-diversity._1f473-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 60%; -} - -.emojione-diversity._1f473-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 60%; -} - -.emojione-diversity._1f473-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 60%; -} - -.emojione-diversity._1f473-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 60%; -} - -.emojione-diversity._1f473-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 60%; -} - -.emojione-diversity._1f473-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 60%; -} - -.emojione-diversity._1f473-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 60%; -} - -.emojione-diversity._1f473-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 60%; -} - -.emojione-diversity._1f473-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 60%; -} - -.emojione-diversity._1f473-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 60%; -} - -.emojione-diversity._1f473-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 60%; -} - -.emojione-diversity._1f473-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 60%; -} - -.emojione-diversity._1f473-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 0%; -} - -.emojione-diversity._1f473-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 2.857142857142857%; -} - -.emojione-diversity._1f9d5-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 8.571428571428571%; -} - -.emojione-diversity._1f9d5-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 11.428571428571429%; -} - -.emojione-diversity._1f9d5-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 14.285714285714286%; -} - -.emojione-diversity._1f9d5-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 17.142857142857142%; -} - -.emojione-diversity._1f9d5-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 20%; -} - -.emojione-diversity._1f46e-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 25.714285714285715%; -} - -.emojione-diversity._1f46e-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 28.571428571428573%; -} - -.emojione-diversity._1f46e-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 31.428571428571427%; -} - -.emojione-diversity._1f46e-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 34.285714285714285%; -} - -.emojione-diversity._1f46e-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 37.142857142857146%; -} - -.emojione-diversity._1f46e-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 42.857142857142854%; -} - -.emojione-diversity._1f46e-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 45.714285714285715%; -} - -.emojione-diversity._1f46e-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 48.57142857142857%; -} - -.emojione-diversity._1f46e-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 51.42857142857143%; -} - -.emojione-diversity._1f46e-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 54.285714285714285%; -} - -.emojione-diversity._1f46e-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 60%; -} - -.emojione-diversity._1f46e-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 62.857142857142854%; -} - -.emojione-diversity._1f46e-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 62.857142857142854%; -} - -.emojione-diversity._1f46e-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 62.857142857142854%; -} - -.emojione-diversity._1f46e-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 62.857142857142854%; -} - -.emojione-diversity._1f477-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 62.857142857142854%; -} - -.emojione-diversity._1f477-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 62.857142857142854%; -} - -.emojione-diversity._1f477-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 62.857142857142854%; -} - -.emojione-diversity._1f477-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 62.857142857142854%; -} - -.emojione-diversity._1f477-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 62.857142857142854%; -} - -.emojione-diversity._1f477-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 62.857142857142854%; -} - -.emojione-diversity._1f477-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 62.857142857142854%; -} - -.emojione-diversity._1f477-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 62.857142857142854%; -} - -.emojione-diversity._1f477-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 62.857142857142854%; -} - -.emojione-diversity._1f477-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 62.857142857142854%; -} - -.emojione-diversity._1f477-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 62.857142857142854%; -} - -.emojione-diversity._1f477-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 62.857142857142854%; -} - -.emojione-diversity._1f477-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 62.857142857142854%; -} - -.emojione-diversity._1f477-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 62.857142857142854%; -} - -.emojione-diversity._1f477-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 62.857142857142854%; -} - -.emojione-diversity._1f482-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 0%; -} - -.emojione-diversity._1f482-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 2.857142857142857%; -} - -.emojione-diversity._1f482-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 5.714285714285714%; -} - -.emojione-diversity._1f482-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 8.571428571428571%; -} - -.emojione-diversity._1f482-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 11.428571428571429%; -} - -.emojione-diversity._1f482-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 17.142857142857142%; -} - -.emojione-diversity._1f482-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 20%; -} - -.emojione-diversity._1f482-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 22.857142857142858%; -} - -.emojione-diversity._1f482-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 25.714285714285715%; -} - -.emojione-diversity._1f482-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 28.571428571428573%; -} - -.emojione-diversity._1f482-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 34.285714285714285%; -} - -.emojione-diversity._1f482-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 37.142857142857146%; -} - -.emojione-diversity._1f482-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 40%; -} - -.emojione-diversity._1f482-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 42.857142857142854%; -} - -.emojione-diversity._1f482-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 45.714285714285715%; -} - -.emojione-diversity._1f575-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 51.42857142857143%; -} - -.emojione-diversity._1f575-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 54.285714285714285%; -} - -.emojione-diversity._1f575-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 57.142857142857146%; -} - -.emojione-diversity._1f575-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 60%; -} - -.emojione-diversity._1f575-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 62.857142857142854%; -} - -.emojione-diversity._1f575-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 65.71428571428571%; -} - -.emojione-diversity._1f575-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 65.71428571428571%; -} - -.emojione-diversity._1f575-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 65.71428571428571%; -} - -.emojione-diversity._1f575-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 65.71428571428571%; -} - -.emojione-diversity._1f575-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 65.71428571428571%; -} - -.emojione-diversity._1f575-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 65.71428571428571%; -} - -.emojione-diversity._1f575-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 65.71428571428571%; -} - -.emojione-diversity._1f575-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 65.71428571428571%; -} - -.emojione-diversity._1f575-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 65.71428571428571%; -} - -.emojione-diversity._1f575-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 65.71428571428571%; -} - -.emojione-diversity._1f469-1f3fb-2695 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 65.71428571428571%; -} - -.emojione-diversity._1f469-1f3fc-2695 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 65.71428571428571%; -} - -.emojione-diversity._1f469-1f3fd-2695 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 65.71428571428571%; -} - -.emojione-diversity._1f469-1f3fe-2695 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 65.71428571428571%; -} - -.emojione-diversity._1f469-1f3ff-2695 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 65.71428571428571%; -} - -.emojione-diversity._1f468-1f3fb-2695 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 65.71428571428571%; -} - -.emojione-diversity._1f468-1f3fc-2695 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 65.71428571428571%; -} - -.emojione-diversity._1f468-1f3fd-2695 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 65.71428571428571%; -} - -.emojione-diversity._1f468-1f3fe-2695 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 65.71428571428571%; -} - -.emojione-diversity._1f468-1f3ff-2695 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 65.71428571428571%; -} - -.emojione-diversity._1f469-1f3fb-1f33e { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 2.857142857142857%; -} - -.emojione-diversity._1f469-1f3fc-1f33e { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 5.714285714285714%; -} - -.emojione-diversity._1f469-1f3fd-1f33e { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 8.571428571428571%; -} - -.emojione-diversity._1f469-1f3fe-1f33e { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 11.428571428571429%; -} - -.emojione-diversity._1f469-1f3ff-1f33e { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 14.285714285714286%; -} - -.emojione-diversity._1f468-1f3fb-1f33e { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 20%; -} - -.emojione-diversity._1f468-1f3fc-1f33e { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 22.857142857142858%; -} - -.emojione-diversity._1f468-1f3fd-1f33e { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 25.714285714285715%; -} - -.emojione-diversity._1f468-1f3fe-1f33e { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 28.571428571428573%; -} - -.emojione-diversity._1f468-1f3ff-1f33e { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 31.428571428571427%; -} - -.emojione-diversity._1f469-1f3fb-1f373 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 37.142857142857146%; -} - -.emojione-diversity._1f469-1f3fc-1f373 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 40%; -} - -.emojione-diversity._1f469-1f3fd-1f373 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 42.857142857142854%; -} - -.emojione-diversity._1f469-1f3fe-1f373 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 45.714285714285715%; -} - -.emojione-diversity._1f469-1f3ff-1f373 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 48.57142857142857%; -} - -.emojione-diversity._1f468-1f3fb-1f373 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 54.285714285714285%; -} - -.emojione-diversity._1f468-1f3fc-1f373 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 57.142857142857146%; -} - -.emojione-diversity._1f468-1f3fd-1f373 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 60%; -} - -.emojione-diversity._1f468-1f3fe-1f373 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 62.857142857142854%; -} - -.emojione-diversity._1f468-1f3ff-1f373 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 65.71428571428571%; -} - -.emojione-diversity._1f469-1f3fb-1f393 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 68.57142857142857%; -} - -.emojione-diversity._1f469-1f3fc-1f393 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 68.57142857142857%; -} - -.emojione-diversity._1f469-1f3fd-1f393 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 68.57142857142857%; -} - -.emojione-diversity._1f469-1f3fe-1f393 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 68.57142857142857%; -} - -.emojione-diversity._1f469-1f3ff-1f393 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 68.57142857142857%; -} - -.emojione-diversity._1f468-1f3fb-1f393 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 68.57142857142857%; -} - -.emojione-diversity._1f468-1f3fc-1f393 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 68.57142857142857%; -} - -.emojione-diversity._1f468-1f3fd-1f393 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 68.57142857142857%; -} - -.emojione-diversity._1f468-1f3fe-1f393 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 68.57142857142857%; -} - -.emojione-diversity._1f468-1f3ff-1f393 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 68.57142857142857%; -} - -.emojione-diversity._1f469-1f3fb-1f3a4 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 68.57142857142857%; -} - -.emojione-diversity._1f469-1f3fc-1f3a4 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 68.57142857142857%; -} - -.emojione-diversity._1f469-1f3fd-1f3a4 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 68.57142857142857%; -} - -.emojione-diversity._1f469-1f3fe-1f3a4 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 68.57142857142857%; -} - -.emojione-diversity._1f469-1f3ff-1f3a4 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 68.57142857142857%; -} - -.emojione-diversity._1f468-1f3fc-1f3a4 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 68.57142857142857%; -} - -.emojione-diversity._1f468-1f3fd-1f3a4 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 68.57142857142857%; -} - -.emojione-diversity._1f468-1f3fe-1f3a4 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 68.57142857142857%; -} - -.emojione-diversity._1f468-1f3ff-1f3a4 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 68.57142857142857%; -} - -.emojione-diversity._1f469-1f3fb-1f3eb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 0%; -} - -.emojione-diversity._1f469-1f3fc-1f3eb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 2.857142857142857%; -} - -.emojione-diversity._1f469-1f3fd-1f3eb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 5.714285714285714%; -} - -.emojione-diversity._1f469-1f3fe-1f3eb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 8.571428571428571%; -} - -.emojione-diversity._1f469-1f3ff-1f3eb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 11.428571428571429%; -} - -.emojione-diversity._1f468-1f3fb-1f3eb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 17.142857142857142%; -} - -.emojione-diversity._1f468-1f3fc-1f3eb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 20%; -} - -.emojione-diversity._1f468-1f3fd-1f3eb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 22.857142857142858%; -} - -.emojione-diversity._1f468-1f3fe-1f3eb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 25.714285714285715%; -} - -.emojione-diversity._1f468-1f3ff-1f3eb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 28.571428571428573%; -} - -.emojione-diversity._1f469-1f3fb-1f3ed { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 34.285714285714285%; -} - -.emojione-diversity._1f469-1f3fc-1f3ed { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 37.142857142857146%; -} - -.emojione-diversity._1f469-1f3fd-1f3ed { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 40%; -} - -.emojione-diversity._1f469-1f3fe-1f3ed { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 42.857142857142854%; -} - -.emojione-diversity._1f469-1f3ff-1f3ed { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 45.714285714285715%; -} - -.emojione-diversity._1f468-1f3fb-1f3ed { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 51.42857142857143%; -} - -.emojione-diversity._1f468-1f3fc-1f3ed { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 54.285714285714285%; -} - -.emojione-diversity._1f468-1f3fd-1f3ed { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 57.142857142857146%; -} - -.emojione-diversity._1f468-1f3fe-1f3ed { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 60%; -} - -.emojione-diversity._1f468-1f3ff-1f3ed { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 62.857142857142854%; -} - -.emojione-diversity._1f469-1f3fb-1f4bb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 68.57142857142857%; -} - -.emojione-diversity._1f469-1f3fc-1f4bb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 71.42857142857143%; -} - -.emojione-diversity._1f469-1f3fd-1f4bb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 71.42857142857143%; -} - -.emojione-diversity._1f469-1f3fe-1f4bb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 71.42857142857143%; -} - -.emojione-diversity._1f469-1f3ff-1f4bb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 71.42857142857143%; -} - -.emojione-diversity._1f468-1f3fb-1f4bb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 71.42857142857143%; -} - -.emojione-diversity._1f468-1f3fc-1f4bb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 71.42857142857143%; -} - -.emojione-diversity._1f468-1f3fd-1f4bb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 71.42857142857143%; -} - -.emojione-diversity._1f468-1f3fe-1f4bb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 71.42857142857143%; -} - -.emojione-diversity._1f468-1f3ff-1f4bb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 71.42857142857143%; -} - -.emojione-diversity._1f469-1f3fb-1f4bc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 71.42857142857143%; -} - -.emojione-diversity._1f469-1f3fc-1f4bc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 71.42857142857143%; -} - -.emojione-diversity._1f469-1f3fd-1f4bc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 71.42857142857143%; -} - -.emojione-diversity._1f469-1f3fe-1f4bc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 71.42857142857143%; -} - -.emojione-diversity._1f469-1f3ff-1f4bc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 71.42857142857143%; -} - -.emojione-diversity._1f468-1f3fb-1f4bc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 71.42857142857143%; -} - -.emojione-diversity._1f468-1f3fc-1f4bc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 71.42857142857143%; -} - -.emojione-diversity._1f468-1f3fd-1f4bc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 71.42857142857143%; -} - -.emojione-diversity._1f468-1f3fe-1f4bc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 71.42857142857143%; -} - -.emojione-diversity._1f468-1f3ff-1f4bc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 71.42857142857143%; -} - -.emojione-diversity._1f469-1f3fb-1f527 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 71.42857142857143%; -} - -.emojione-diversity._1f469-1f3fc-1f527 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 71.42857142857143%; -} - -.emojione-diversity._1f469-1f3fd-1f527 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 71.42857142857143%; -} - -.emojione-diversity._1f469-1f3fe-1f527 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 0%; -} - -.emojione-diversity._1f469-1f3ff-1f527 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 2.857142857142857%; -} - -.emojione-diversity._1f468-1f3fb-1f527 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 8.571428571428571%; -} - -.emojione-diversity._1f468-1f3fc-1f527 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 11.428571428571429%; -} - -.emojione-diversity._1f468-1f3fd-1f527 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 14.285714285714286%; -} - -.emojione-diversity._1f468-1f3fe-1f527 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 17.142857142857142%; -} - -.emojione-diversity._1f468-1f3ff-1f527 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 20%; -} - -.emojione-diversity._1f469-1f3fb-1f52c { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 25.714285714285715%; -} - -.emojione-diversity._1f469-1f3fc-1f52c { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 28.571428571428573%; -} - -.emojione-diversity._1f469-1f3fd-1f52c { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 31.428571428571427%; -} - -.emojione-diversity._1f469-1f3fe-1f52c { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 34.285714285714285%; -} - -.emojione-diversity._1f469-1f3ff-1f52c { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 37.142857142857146%; -} - -.emojione-diversity._1f468-1f3fb-1f52c { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 42.857142857142854%; -} - -.emojione-diversity._1f468-1f3fc-1f52c { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 45.714285714285715%; -} - -.emojione-diversity._1f468-1f3fd-1f52c { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 48.57142857142857%; -} - -.emojione-diversity._1f468-1f3fe-1f52c { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 51.42857142857143%; -} - -.emojione-diversity._1f468-1f3ff-1f52c { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 54.285714285714285%; -} - -.emojione-diversity._1f469-1f3fb-1f3a8 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 60%; -} - -.emojione-diversity._1f469-1f3fc-1f3a8 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 62.857142857142854%; -} - -.emojione-diversity._1f469-1f3fd-1f3a8 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 65.71428571428571%; -} - -.emojione-diversity._1f469-1f3fe-1f3a8 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 68.57142857142857%; -} - -.emojione-diversity._1f469-1f3ff-1f3a8 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 71.42857142857143%; -} - -.emojione-diversity._1f468-1f3fb-1f3a8 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 74.28571428571429%; -} - -.emojione-diversity._1f468-1f3fc-1f3a8 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 74.28571428571429%; -} - -.emojione-diversity._1f468-1f3fd-1f3a8 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 74.28571428571429%; -} - -.emojione-diversity._1f468-1f3fe-1f3a8 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 74.28571428571429%; -} - -.emojione-diversity._1f468-1f3ff-1f3a8 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 74.28571428571429%; -} - -.emojione-diversity._1f469-1f3fb-1f692 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 74.28571428571429%; -} - -.emojione-diversity._1f469-1f3fc-1f692 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 74.28571428571429%; -} - -.emojione-diversity._1f469-1f3fd-1f692 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 74.28571428571429%; -} - -.emojione-diversity._1f469-1f3fe-1f692 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 74.28571428571429%; -} - -.emojione-diversity._1f469-1f3ff-1f692 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 74.28571428571429%; -} - -.emojione-diversity._1f468-1f3fb-1f692 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 74.28571428571429%; -} - -.emojione-diversity._1f468-1f3fc-1f692 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 74.28571428571429%; -} - -.emojione-diversity._1f468-1f3fd-1f692 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 74.28571428571429%; -} - -.emojione-diversity._1f468-1f3fe-1f692 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 74.28571428571429%; -} - -.emojione-diversity._1f468-1f3ff-1f692 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 74.28571428571429%; -} - -.emojione-diversity._1f469-1f3fb-2708 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 74.28571428571429%; -} - -.emojione-diversity._1f469-1f3fc-2708 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 74.28571428571429%; -} - -.emojione-diversity._1f469-1f3fd-2708 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 74.28571428571429%; -} - -.emojione-diversity._1f469-1f3fe-2708 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 74.28571428571429%; -} - -.emojione-diversity._1f469-1f3ff-2708 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 74.28571428571429%; -} - -.emojione-diversity._1f468-1f3fb-2708 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 74.28571428571429%; -} - -.emojione-diversity._1f468-1f3fc-2708 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 74.28571428571429%; -} - -.emojione-diversity._1f468-1f3fd-2708 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 0%; -} - -.emojione-diversity._1f468-1f3fe-2708 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 2.857142857142857%; -} - -.emojione-diversity._1f468-1f3ff-2708 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 5.714285714285714%; -} - -.emojione-diversity._1f469-1f3fb-1f680 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 11.428571428571429%; -} - -.emojione-diversity._1f469-1f3fc-1f680 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 14.285714285714286%; -} - -.emojione-diversity._1f469-1f3fd-1f680 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 17.142857142857142%; -} - -.emojione-diversity._1f469-1f3fe-1f680 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 20%; -} - -.emojione-diversity._1f469-1f3ff-1f680 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 22.857142857142858%; -} - -.emojione-diversity._1f468-1f3fb-1f680 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 28.571428571428573%; -} - -.emojione-diversity._1f468-1f3fc-1f680 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 31.428571428571427%; -} - -.emojione-diversity._1f468-1f3fd-1f680 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 34.285714285714285%; -} - -.emojione-diversity._1f468-1f3fe-1f680 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 37.142857142857146%; -} - -.emojione-diversity._1f468-1f3ff-1f680 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 40%; -} - -.emojione-diversity._1f469-1f3fb-2696 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 45.714285714285715%; -} - -.emojione-diversity._1f469-1f3fc-2696 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 48.57142857142857%; -} - -.emojione-diversity._1f469-1f3fd-2696 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 51.42857142857143%; -} - -.emojione-diversity._1f469-1f3fe-2696 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 54.285714285714285%; -} - -.emojione-diversity._1f469-1f3ff-2696 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 57.142857142857146%; -} - -.emojione-diversity._1f468-1f3fb-2696 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 62.857142857142854%; -} - -.emojione-diversity._1f468-1f3fc-2696 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 65.71428571428571%; -} - -.emojione-diversity._1f468-1f3fd-2696 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 68.57142857142857%; -} - -.emojione-diversity._1f468-1f3fe-2696 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 71.42857142857143%; -} - -.emojione-diversity._1f468-1f3ff-2696 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 74.28571428571429%; -} - -.emojione-diversity._1f470-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 77.14285714285714%; -} - -.emojione-diversity._1f470-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 77.14285714285714%; -} - -.emojione-diversity._1f470-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 77.14285714285714%; -} - -.emojione-diversity._1f470-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 77.14285714285714%; -} - -.emojione-diversity._1f470-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 77.14285714285714%; -} - -.emojione-diversity._1f935-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 77.14285714285714%; -} - -.emojione-diversity._1f935-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 77.14285714285714%; -} - -.emojione-diversity._1f935-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 77.14285714285714%; -} - -.emojione-diversity._1f935-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 77.14285714285714%; -} - -.emojione-diversity._1f935-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 77.14285714285714%; -} - -.emojione-diversity._1f478-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 77.14285714285714%; -} - -.emojione-diversity._1f478-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 77.14285714285714%; -} - -.emojione-diversity._1f478-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 77.14285714285714%; -} - -.emojione-diversity._1f478-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 77.14285714285714%; -} - -.emojione-diversity._1f478-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 77.14285714285714%; -} - -.emojione-diversity._1f934-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 77.14285714285714%; -} - -.emojione-diversity._1f934-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 77.14285714285714%; -} - -.emojione-diversity._1f934-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 77.14285714285714%; -} - -.emojione-diversity._1f934-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 77.14285714285714%; -} - -.emojione-diversity._1f934-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 77.14285714285714%; -} - -.emojione-diversity._1f936-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 77.14285714285714%; -} - -.emojione-diversity._1f936-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 77.14285714285714%; -} - -.emojione-diversity._1f936-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 77.14285714285714%; -} - -.emojione-diversity._1f936-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 0%; -} - -.emojione-diversity._1f936-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 2.857142857142857%; -} - -.emojione-diversity._1f385-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 8.571428571428571%; -} - -.emojione-diversity._1f385-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 11.428571428571429%; -} - -.emojione-diversity._1f385-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 14.285714285714286%; -} - -.emojione-diversity._1f385-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 17.142857142857142%; -} - -.emojione-diversity._1f385-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 20%; -} - -.emojione-diversity._1f9b8-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 25.714285714285715%; -} - -.emojione-diversity._1f9b8-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 28.571428571428573%; -} - -.emojione-diversity._1f9b8-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 31.428571428571427%; -} - -.emojione-diversity._1f9b8-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 34.285714285714285%; -} - -.emojione-diversity._1f9b8-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 37.142857142857146%; -} - -.emojione-diversity._1f9b8-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 42.857142857142854%; -} - -.emojione-diversity._1f9b8-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 45.714285714285715%; -} - -.emojione-diversity._1f9b8-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 48.57142857142857%; -} - -.emojione-diversity._1f9b8-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 51.42857142857143%; -} - -.emojione-diversity._1f9b8-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 54.285714285714285%; -} - -.emojione-diversity._1f9b8-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 60%; -} - -.emojione-diversity._1f9b8-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 62.857142857142854%; -} - -.emojione-diversity._1f9b8-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 65.71428571428571%; -} - -.emojione-diversity._1f9b8-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 68.57142857142857%; -} - -.emojione-diversity._1f9b8-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 71.42857142857143%; -} - -.emojione-diversity._1f9b9-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 77.14285714285714%; -} - -.emojione-diversity._1f9b9-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 80%; -} - -.emojione-diversity._1f9b9-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 80%; -} - -.emojione-diversity._1f9b9-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 80%; -} - -.emojione-diversity._1f9b9-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 80%; -} - -.emojione-diversity._1f9b9-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 80%; -} - -.emojione-diversity._1f9b9-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 80%; -} - -.emojione-diversity._1f9b9-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 80%; -} - -.emojione-diversity._1f9b9-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 80%; -} - -.emojione-diversity._1f9b9-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 80%; -} - -.emojione-diversity._1f9b9-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 80%; -} - -.emojione-diversity._1f9b9-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 80%; -} - -.emojione-diversity._1f9b9-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 80%; -} - -.emojione-diversity._1f9b9-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 80%; -} - -.emojione-diversity._1f9b9-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 80%; -} - -.emojione-diversity._1f9d9-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 80%; -} - -.emojione-diversity._1f9d9-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 80%; -} - -.emojione-diversity._1f9d9-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 80%; -} - -.emojione-diversity._1f9d9-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 80%; -} - -.emojione-diversity._1f9d9-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 80%; -} - -.emojione-diversity._1f9d9-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 80%; -} - -.emojione-diversity._1f9d9-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 80%; -} - -.emojione-diversity._1f9d9-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 80%; -} - -.emojione-diversity._1f9d9-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 80%; -} - -.emojione-diversity._1f9d9-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 80%; -} - -.emojione-diversity._1f9d9-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 0%; -} - -.emojione-diversity._1f9d9-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 2.857142857142857%; -} - -.emojione-diversity._1f9d9-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 5.714285714285714%; -} - -.emojione-diversity._1f9d9-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 8.571428571428571%; -} - -.emojione-diversity._1f9d9-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 11.428571428571429%; -} - -.emojione-diversity._1f9dd-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 17.142857142857142%; -} - -.emojione-diversity._1f9dd-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 20%; -} - -.emojione-diversity._1f9dd-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 22.857142857142858%; -} - -.emojione-diversity._1f9dd-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 25.714285714285715%; -} - -.emojione-diversity._1f9dd-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 28.571428571428573%; -} - -.emojione-diversity._1f9dd-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 34.285714285714285%; -} - -.emojione-diversity._1f9dd-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 37.142857142857146%; -} - -.emojione-diversity._1f9dd-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 40%; -} - -.emojione-diversity._1f9dd-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 42.857142857142854%; -} - -.emojione-diversity._1f9dd-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 45.714285714285715%; -} - -.emojione-diversity._1f9dd-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 51.42857142857143%; -} - -.emojione-diversity._1f9dd-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 54.285714285714285%; -} - -.emojione-diversity._1f9dd-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 57.142857142857146%; -} - -.emojione-diversity._1f9dd-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 60%; -} - -.emojione-diversity._1f9dd-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 62.857142857142854%; -} - -.emojione-diversity._1f9db-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 68.57142857142857%; -} - -.emojione-diversity._1f9db-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 71.42857142857143%; -} - -.emojione-diversity._1f9db-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 74.28571428571429%; -} - -.emojione-diversity._1f9db-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 77.14285714285714%; -} - -.emojione-diversity._1f9db-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 80%; -} - -.emojione-diversity._1f9db-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 82.85714285714286%; -} - -.emojione-diversity._1f9db-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 82.85714285714286%; -} - -.emojione-diversity._1f9db-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 82.85714285714286%; -} - -.emojione-diversity._1f9db-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 82.85714285714286%; -} - -.emojione-diversity._1f9db-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 82.85714285714286%; -} - -.emojione-diversity._1f9db-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 82.85714285714286%; -} - -.emojione-diversity._1f9db-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 82.85714285714286%; -} - -.emojione-diversity._1f9db-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 82.85714285714286%; -} - -.emojione-diversity._1f9db-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 82.85714285714286%; -} - -.emojione-diversity._1f9db-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 82.85714285714286%; -} - -.emojione-diversity._1f9dc-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 82.85714285714286%; -} - -.emojione-diversity._1f9dc-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 82.85714285714286%; -} - -.emojione-diversity._1f9dc-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 82.85714285714286%; -} - -.emojione-diversity._1f9dc-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 82.85714285714286%; -} - -.emojione-diversity._1f9dc-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 82.85714285714286%; -} - -.emojione-diversity._1f9dc-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 82.85714285714286%; -} - -.emojione-diversity._1f9dc-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 82.85714285714286%; -} - -.emojione-diversity._1f9dc-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 82.85714285714286%; -} - -.emojione-diversity._1f9dc-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 82.85714285714286%; -} - -.emojione-diversity._1f9dc-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 82.85714285714286%; -} - -.emojione-diversity._1f9dc-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 2.857142857142857%; -} - -.emojione-diversity._1f9dc-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 5.714285714285714%; -} - -.emojione-diversity._1f9dc-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 8.571428571428571%; -} - -.emojione-diversity._1f9dc-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 11.428571428571429%; -} - -.emojione-diversity._1f9dc-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 14.285714285714286%; -} - -.emojione-diversity._1f9da-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 20%; -} - -.emojione-diversity._1f9da-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 22.857142857142858%; -} - -.emojione-diversity._1f9da-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 25.714285714285715%; -} - -.emojione-diversity._1f9da-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 28.571428571428573%; -} - -.emojione-diversity._1f9da-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 31.428571428571427%; -} - -.emojione-diversity._1f9da-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 37.142857142857146%; -} - -.emojione-diversity._1f9da-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 40%; -} - -.emojione-diversity._1f9da-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 42.857142857142854%; -} - -.emojione-diversity._1f9da-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 45.714285714285715%; -} - -.emojione-diversity._1f9da-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 48.57142857142857%; -} - -.emojione-diversity._1f9da-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 54.285714285714285%; -} - -.emojione-diversity._1f9da-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 57.142857142857146%; -} - -.emojione-diversity._1f9da-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 60%; -} - -.emojione-diversity._1f9da-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 62.857142857142854%; -} - -.emojione-diversity._1f9da-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 65.71428571428571%; -} - -.emojione-diversity._1f47c-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 71.42857142857143%; -} - -.emojione-diversity._1f47c-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 74.28571428571429%; -} - -.emojione-diversity._1f47c-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 77.14285714285714%; -} - -.emojione-diversity._1f47c-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 80%; -} - -.emojione-diversity._1f47c-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 82.85714285714286%; -} - -.emojione-diversity._1f930-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 85.71428571428571%; -} - -.emojione-diversity._1f930-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 85.71428571428571%; -} - -.emojione-diversity._1f930-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 85.71428571428571%; -} - -.emojione-diversity._1f930-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 85.71428571428571%; -} - -.emojione-diversity._1f930-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 85.71428571428571%; -} - -.emojione-diversity._1f931-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 85.71428571428571%; -} - -.emojione-diversity._1f931-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 85.71428571428571%; -} - -.emojione-diversity._1f931-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 85.71428571428571%; -} - -.emojione-diversity._1f931-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 85.71428571428571%; -} - -.emojione-diversity._1f931-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 85.71428571428571%; -} - -.emojione-diversity._1f647-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 85.71428571428571%; -} - -.emojione-diversity._1f647-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 85.71428571428571%; -} - -.emojione-diversity._1f647-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 85.71428571428571%; -} - -.emojione-diversity._1f647-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 85.71428571428571%; -} - -.emojione-diversity._1f647-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 85.71428571428571%; -} - -.emojione-diversity._1f647-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 85.71428571428571%; -} - -.emojione-diversity._1f647-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 85.71428571428571%; -} - -.emojione-diversity._1f647-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 85.71428571428571%; -} - -.emojione-diversity._1f647-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 85.71428571428571%; -} - -.emojione-diversity._1f647-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 85.71428571428571%; -} - -.emojione-diversity._1f647-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 85.71428571428571%; -} - -.emojione-diversity._1f647-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 85.71428571428571%; -} - -.emojione-diversity._1f647-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 85.71428571428571%; -} - -.emojione-diversity._1f647-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 85.71428571428571%; -} - -.emojione-diversity._1f647-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 85.71428571428571%; -} - -.emojione-diversity._1f481-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 0%; -} - -.emojione-diversity._1f481-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 2.857142857142857%; -} - -.emojione-diversity._1f481-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 5.714285714285714%; -} - -.emojione-diversity._1f481-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 8.571428571428571%; -} - -.emojione-diversity._1f481-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 11.428571428571429%; -} - -.emojione-diversity._1f481-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 17.142857142857142%; -} - -.emojione-diversity._1f481-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 20%; -} - -.emojione-diversity._1f481-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 22.857142857142858%; -} - -.emojione-diversity._1f481-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 25.714285714285715%; -} - -.emojione-diversity._1f481-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 28.571428571428573%; -} - -.emojione-diversity._1f481-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 34.285714285714285%; -} - -.emojione-diversity._1f481-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 37.142857142857146%; -} - -.emojione-diversity._1f481-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 40%; -} - -.emojione-diversity._1f481-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 42.857142857142854%; -} - -.emojione-diversity._1f481-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 45.714285714285715%; -} - -.emojione-diversity._1f645-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 51.42857142857143%; -} - -.emojione-diversity._1f645-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 54.285714285714285%; -} - -.emojione-diversity._1f645-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 57.142857142857146%; -} - -.emojione-diversity._1f645-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 60%; -} - -.emojione-diversity._1f645-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 62.857142857142854%; -} - -.emojione-diversity._1f645-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 68.57142857142857%; -} - -.emojione-diversity._1f645-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 71.42857142857143%; -} - -.emojione-diversity._1f645-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 74.28571428571429%; -} - -.emojione-diversity._1f645-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 77.14285714285714%; -} - -.emojione-diversity._1f645-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 80%; -} - -.emojione-diversity._1f645-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 85.71428571428571%; -} - -.emojione-diversity._1f645-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 88.57142857142857%; -} - -.emojione-diversity._1f645-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 88.57142857142857%; -} - -.emojione-diversity._1f645-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 88.57142857142857%; -} - -.emojione-diversity._1f645-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 88.57142857142857%; -} - -.emojione-diversity._1f646-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 88.57142857142857%; -} - -.emojione-diversity._1f646-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 88.57142857142857%; -} - -.emojione-diversity._1f646-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 88.57142857142857%; -} - -.emojione-diversity._1f646-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 88.57142857142857%; -} - -.emojione-diversity._1f646-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 88.57142857142857%; -} - -.emojione-diversity._1f646-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 88.57142857142857%; -} - -.emojione-diversity._1f646-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 88.57142857142857%; -} - -.emojione-diversity._1f646-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 88.57142857142857%; -} - -.emojione-diversity._1f646-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 88.57142857142857%; -} - -.emojione-diversity._1f646-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 88.57142857142857%; -} - -.emojione-diversity._1f646-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 88.57142857142857%; -} - -.emojione-diversity._1f646-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 88.57142857142857%; -} - -.emojione-diversity._1f646-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 88.57142857142857%; -} - -.emojione-diversity._1f646-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 88.57142857142857%; -} - -.emojione-diversity._1f646-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 88.57142857142857%; -} - -.emojione-diversity._1f64b-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 88.57142857142857%; -} - -.emojione-diversity._1f64b-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 88.57142857142857%; -} - -.emojione-diversity._1f64b-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 88.57142857142857%; -} - -.emojione-diversity._1f64b-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 88.57142857142857%; -} - -.emojione-diversity._1f64b-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 88.57142857142857%; -} - -.emojione-diversity._1f64b-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 88.57142857142857%; -} - -.emojione-diversity._1f64b-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 88.57142857142857%; -} - -.emojione-diversity._1f64b-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 88.57142857142857%; -} - -.emojione-diversity._1f64b-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 0%; -} - -.emojione-diversity._1f64b-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 2.857142857142857%; -} - -.emojione-diversity._1f64b-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 8.571428571428571%; -} - -.emojione-diversity._1f64b-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 11.428571428571429%; -} - -.emojione-diversity._1f64b-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 14.285714285714286%; -} - -.emojione-diversity._1f64b-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 17.142857142857142%; -} - -.emojione-diversity._1f64b-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 20%; -} - -.emojione-diversity._1f926-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 25.714285714285715%; -} - -.emojione-diversity._1f926-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 28.571428571428573%; -} - -.emojione-diversity._1f926-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 31.428571428571427%; -} - -.emojione-diversity._1f926-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 34.285714285714285%; -} - -.emojione-diversity._1f926-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 37.142857142857146%; -} - -.emojione-diversity._1f926-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 42.857142857142854%; -} - -.emojione-diversity._1f926-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 45.714285714285715%; -} - -.emojione-diversity._1f926-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 48.57142857142857%; -} - -.emojione-diversity._1f926-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 51.42857142857143%; -} - -.emojione-diversity._1f926-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 54.285714285714285%; -} - -.emojione-diversity._1f926-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 60%; -} - -.emojione-diversity._1f926-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 62.857142857142854%; -} - -.emojione-diversity._1f926-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 65.71428571428571%; -} - -.emojione-diversity._1f926-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 68.57142857142857%; -} - -.emojione-diversity._1f926-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 71.42857142857143%; -} - -.emojione-diversity._1f937-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 77.14285714285714%; -} - -.emojione-diversity._1f937-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 80%; -} - -.emojione-diversity._1f937-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 82.85714285714286%; -} - -.emojione-diversity._1f937-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 85.71428571428571%; -} - -.emojione-diversity._1f937-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 88.57142857142857%; -} - -.emojione-diversity._1f937-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 91.42857142857143%; -} - -.emojione-diversity._1f937-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 91.42857142857143%; -} - -.emojione-diversity._1f937-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 91.42857142857143%; -} - -.emojione-diversity._1f937-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 91.42857142857143%; -} - -.emojione-diversity._1f937-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 91.42857142857143%; -} - -.emojione-diversity._1f937-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 91.42857142857143%; -} - -.emojione-diversity._1f937-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 91.42857142857143%; -} - -.emojione-diversity._1f937-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 91.42857142857143%; -} - -.emojione-diversity._1f937-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 91.42857142857143%; -} - -.emojione-diversity._1f937-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 91.42857142857143%; -} - -.emojione-diversity._1f64e-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 91.42857142857143%; -} - -.emojione-diversity._1f64e-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 91.42857142857143%; -} - -.emojione-diversity._1f64e-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 91.42857142857143%; -} - -.emojione-diversity._1f64e-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 91.42857142857143%; -} - -.emojione-diversity._1f64e-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 91.42857142857143%; -} - -.emojione-diversity._1f64e-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 91.42857142857143%; -} - -.emojione-diversity._1f64e-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 91.42857142857143%; -} - -.emojione-diversity._1f64e-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 91.42857142857143%; -} - -.emojione-diversity._1f64e-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 91.42857142857143%; -} - -.emojione-diversity._1f64e-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 91.42857142857143%; -} - -.emojione-diversity._1f64e-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 91.42857142857143%; -} - -.emojione-diversity._1f64e-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 91.42857142857143%; -} - -.emojione-diversity._1f64e-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 91.42857142857143%; -} - -.emojione-diversity._1f64e-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 91.42857142857143%; -} - -.emojione-diversity._1f64e-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 91.42857142857143%; -} - -.emojione-diversity._1f64d-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 91.42857142857143%; -} - -.emojione-diversity._1f64d-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 91.42857142857143%; -} - -.emojione-diversity._1f64d-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 0%; -} - -.emojione-diversity._1f64d-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 2.857142857142857%; -} - -.emojione-diversity._1f64d-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 5.714285714285714%; -} - -.emojione-diversity._1f64d-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 11.428571428571429%; -} - -.emojione-diversity._1f64d-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 14.285714285714286%; -} - -.emojione-diversity._1f64d-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 17.142857142857142%; -} - -.emojione-diversity._1f64d-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 20%; -} - -.emojione-diversity._1f64d-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 22.857142857142858%; -} - -.emojione-diversity._1f64d-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 28.571428571428573%; -} - -.emojione-diversity._1f64d-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 31.428571428571427%; -} - -.emojione-diversity._1f64d-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 34.285714285714285%; -} - -.emojione-diversity._1f64d-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 37.142857142857146%; -} - -.emojione-diversity._1f64d-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 40%; -} - -.emojione-diversity._1f487-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 45.714285714285715%; -} - -.emojione-diversity._1f487-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 48.57142857142857%; -} - -.emojione-diversity._1f487-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 51.42857142857143%; -} - -.emojione-diversity._1f487-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 54.285714285714285%; -} - -.emojione-diversity._1f487-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 57.142857142857146%; -} - -.emojione-diversity._1f487-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 62.857142857142854%; -} - -.emojione-diversity._1f487-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 65.71428571428571%; -} - -.emojione-diversity._1f487-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 68.57142857142857%; -} - -.emojione-diversity._1f487-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 71.42857142857143%; -} - -.emojione-diversity._1f487-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 74.28571428571429%; -} - -.emojione-diversity._1f487-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 80%; -} - -.emojione-diversity._1f487-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 82.85714285714286%; -} - -.emojione-diversity._1f487-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 85.71428571428571%; -} - -.emojione-diversity._1f487-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 88.57142857142857%; -} - -.emojione-diversity._1f487-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 91.42857142857143%; -} - -.emojione-diversity._1f486-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 2.7777777777777777% 94.28571428571429%; -} - -.emojione-diversity._1f486-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 94.28571428571429%; -} - -.emojione-diversity._1f486-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 94.28571428571429%; -} - -.emojione-diversity._1f486-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 94.28571428571429%; -} - -.emojione-diversity._1f486-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 94.28571428571429%; -} - -.emojione-diversity._1f486-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 19.444444444444443% 94.28571428571429%; -} - -.emojione-diversity._1f486-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 94.28571428571429%; -} - -.emojione-diversity._1f486-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 94.28571428571429%; -} - -.emojione-diversity._1f486-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 94.28571428571429%; -} - -.emojione-diversity._1f486-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 94.28571428571429%; -} - -.emojione-diversity._1f486-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 36.111111111111114% 94.28571428571429%; -} - -.emojione-diversity._1f486-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 94.28571428571429%; -} - -.emojione-diversity._1f486-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 94.28571428571429%; -} - -.emojione-diversity._1f486-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 94.28571428571429%; -} - -.emojione-diversity._1f486-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 94.28571428571429%; -} - -.emojione-diversity._1f9d6-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 52.77777777777778% 94.28571428571429%; -} - -.emojione-diversity._1f9d6-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 94.28571428571429%; -} - -.emojione-diversity._1f9d6-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 94.28571428571429%; -} - -.emojione-diversity._1f9d6-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 94.28571428571429%; -} - -.emojione-diversity._1f9d6-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 94.28571428571429%; -} - -.emojione-diversity._1f9d6-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 69.44444444444444% 94.28571428571429%; -} - -.emojione-diversity._1f9d6-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 94.28571428571429%; -} - -.emojione-diversity._1f9d6-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 94.28571428571429%; -} - -.emojione-diversity._1f9d6-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 94.28571428571429%; -} - -.emojione-diversity._1f9d6-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 94.28571428571429%; -} - -.emojione-diversity._1f9d6-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 86.11111111111111% 94.28571428571429%; -} - -.emojione-diversity._1f9d6-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 94.28571428571429%; -} - -.emojione-diversity._1f9d6-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 94.28571428571429%; -} - -.emojione-diversity._1f9d6-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 0%; -} - -.emojione-diversity._1f9d6-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 2.857142857142857%; -} - -.emojione-diversity._1f485-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 8.571428571428571%; -} - -.emojione-diversity._1f485-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 11.428571428571429%; -} - -.emojione-diversity._1f485-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 14.285714285714286%; -} - -.emojione-diversity._1f485-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 17.142857142857142%; -} - -.emojione-diversity._1f485-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 20%; -} - -.emojione-diversity._1f933-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 25.714285714285715%; -} - -.emojione-diversity._1f933-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 28.571428571428573%; -} - -.emojione-diversity._1f933-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 31.428571428571427%; -} - -.emojione-diversity._1f933-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 34.285714285714285%; -} - -.emojione-diversity._1f933-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 37.142857142857146%; -} - -.emojione-diversity._1f483-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 42.857142857142854%; -} - -.emojione-diversity._1f483-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 45.714285714285715%; -} - -.emojione-diversity._1f483-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 48.57142857142857%; -} - -.emojione-diversity._1f483-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 51.42857142857143%; -} - -.emojione-diversity._1f483-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 54.285714285714285%; -} - -.emojione-diversity._1f57a-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 60%; -} - -.emojione-diversity._1f57a-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 62.857142857142854%; -} - -.emojione-diversity._1f57a-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 65.71428571428571%; -} - -.emojione-diversity._1f57a-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 68.57142857142857%; -} - -.emojione-diversity._1f57a-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 71.42857142857143%; -} - -.emojione-diversity._1f574-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 85.71428571428571%; -} - -.emojione-diversity._1f574-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 88.57142857142857%; -} - -.emojione-diversity._1f574-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 91.42857142857143%; -} - -.emojione-diversity._1f574-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 94.28571428571429%; -} - -.emojione-diversity._1f574-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 0% 97.14285714285714%; -} - -.emojione-diversity._1f6b6-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 5.555555555555555% 97.14285714285714%; -} - -.emojione-diversity._1f6b6-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 8.333333333333334% 97.14285714285714%; -} - -.emojione-diversity._1f6b6-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 11.11111111111111% 97.14285714285714%; -} - -.emojione-diversity._1f6b6-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 13.88888888888889% 97.14285714285714%; -} - -.emojione-diversity._1f6b6-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 16.666666666666668% 97.14285714285714%; -} - -.emojione-diversity._1f6b6-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 22.22222222222222% 97.14285714285714%; -} - -.emojione-diversity._1f6b6-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 25% 97.14285714285714%; -} - -.emojione-diversity._1f6b6-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 27.77777777777778% 97.14285714285714%; -} - -.emojione-diversity._1f6b6-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 30.555555555555557% 97.14285714285714%; -} - -.emojione-diversity._1f6b6-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 33.333333333333336% 97.14285714285714%; -} - -.emojione-diversity._1f6b6-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 38.888888888888886% 97.14285714285714%; -} - -.emojione-diversity._1f6b6-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 41.666666666666664% 97.14285714285714%; -} - -.emojione-diversity._1f6b6-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 44.44444444444444% 97.14285714285714%; -} - -.emojione-diversity._1f6b6-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 47.22222222222222% 97.14285714285714%; -} - -.emojione-diversity._1f6b6-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 50% 97.14285714285714%; -} - -.emojione-diversity._1f3c3-1f3fb { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 55.55555555555556% 97.14285714285714%; -} - -.emojione-diversity._1f3c3-1f3fc { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 58.333333333333336% 97.14285714285714%; -} - -.emojione-diversity._1f3c3-1f3fd { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 61.111111111111114% 97.14285714285714%; -} - -.emojione-diversity._1f3c3-1f3fe { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 63.888888888888886% 97.14285714285714%; -} - -.emojione-diversity._1f3c3-1f3ff { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 66.66666666666667% 97.14285714285714%; -} - -.emojione-diversity._1f3c3-1f3fb-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 72.22222222222223% 97.14285714285714%; -} - -.emojione-diversity._1f3c3-1f3fc-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 75% 97.14285714285714%; -} - -.emojione-diversity._1f3c3-1f3fd-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 77.77777777777777% 97.14285714285714%; -} - -.emojione-diversity._1f3c3-1f3fe-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 80.55555555555556% 97.14285714285714%; -} - -.emojione-diversity._1f3c3-1f3ff-2640 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 83.33333333333333% 97.14285714285714%; -} - -.emojione-diversity._1f3c3-1f3fb-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 88.88888888888889% 97.14285714285714%; -} - -.emojione-diversity._1f3c3-1f3fc-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 91.66666666666667% 97.14285714285714%; -} - -.emojione-diversity._1f3c3-1f3fd-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 94.44444444444444% 97.14285714285714%; -} - -.emojione-diversity._1f3c3-1f3fe-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 0%; -} - -.emojione-diversity._1f3c3-1f3ff-2642 { - background-image: url('/packages/emojione/people-sprites.png'); - background-repeat: no-repeat; - background-size: 3700% 3600%; - background-position: 97.22222222222223% 2.857142857142857%; -} diff --git a/apps/meteor/app/emoji-emojione/client/regional-sprites.css b/apps/meteor/app/emoji-emojione/client/regional-sprites.css deleted file mode 100644 index c9666b0cf7ace..0000000000000 --- a/apps/meteor/app/emoji-emojione/client/regional-sprites.css +++ /dev/null @@ -1,163 +0,0 @@ -/* stylelint-disable */ - -.emojione-regional { - background-image: url('/packages/emojione/regional-sprites.png'); - - &._1f1f2 { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 0% 0%; - } - - &._1f1ff { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 20% 0%; - } - - &._1f1fd { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 0% 25%; - } - - &._1f1fc { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 20% 25%; - } - - &._1f1fb { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 40% 0%; - } - - &._1f1fa { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 40% 25%; - } - - &._1f1f9 { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 0% 50%; - } - - &._1f1f8 { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 20% 50%; - } - - &._1f1f7 { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 40% 50%; - } - - &._1f1f6 { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 60% 0%; - } - - &._1f1f5 { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 60% 25%; - } - - &._1f1f4 { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 60% 50%; - } - - &._1f1f3 { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 0% 75%; - } - - &._1f1fe { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 20% 75%; - } - - &._1f1f1 { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 40% 75%; - } - - &._1f1f0 { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 60% 75%; - } - - &._1f1ef { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 80% 0%; - } - - &._1f1ee { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 80% 25%; - } - - &._1f1ed { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 80% 50%; - } - - &._1f1ec { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 80% 75%; - } - - &._1f1eb { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 0% 100%; - } - - &._1f1ea { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 20% 100%; - } - - &._1f1e9 { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 40% 100%; - } - - &._1f1e8 { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 60% 100%; - } - - &._1f1e7 { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 80% 100%; - } - - &._1f1e6 { - background-repeat: no-repeat; - background-size: 600% 500%; - background-position: 100% 0%; - } - -} - diff --git a/apps/meteor/app/emoji-emojione/client/symbols-sprites.css b/apps/meteor/app/emoji-emojione/client/symbols-sprites.css deleted file mode 100644 index 0910d13a88e18..0000000000000 --- a/apps/meteor/app/emoji-emojione/client/symbols-sprites.css +++ /dev/null @@ -1,1747 +0,0 @@ -/* stylelint-disable */ - -.emojione-symbols { - background-image: url('/packages/emojione/symbols-sprites.png'); - - &._1f6c5 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 0% 0%; - } - - &._2049 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 5.882352941176471% 0%; - } - - &._2139 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 0% 6.25%; - } - - &._2194 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 5.882352941176471% 6.25%; - } - - &._2195 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 11.764705882352942% 0%; - } - - &._2196 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 11.764705882352942% 6.25%; - } - - &._2197 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 0% 12.5%; - } - - &._2198 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 5.882352941176471% 12.5%; - } - - &._2199 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 11.764705882352942% 12.5%; - } - - &._2611 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 17.647058823529413% 0%; - } - - &._2622 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 17.647058823529413% 6.25%; - } - - &._2623 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 17.647058823529413% 12.5%; - } - - &._2626 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 0% 18.75%; - } - - &._2638 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 5.882352941176471% 18.75%; - } - - &._2640 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 11.764705882352942% 18.75%; - } - - &._2642 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 17.647058823529413% 18.75%; - } - - &._2648 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 23.529411764705884% 0%; - } - - &._2649 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 23.529411764705884% 6.25%; - } - - &._2650 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 23.529411764705884% 12.5%; - } - - &._2651 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 23.529411764705884% 18.75%; - } - - &._2652 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 0% 25%; - } - - &._2653 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 5.882352941176471% 25%; - } - - &._2660 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 11.764705882352942% 25%; - } - - &._2663 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 17.647058823529413% 25%; - } - - &._2665 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 23.529411764705884% 25%; - } - - &._2666 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 29.41176470588235% 0%; - } - - &._2668 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 29.41176470588235% 6.25%; - } - - &._2695 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 29.41176470588235% 12.5%; - } - - &._2705 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 29.41176470588235% 18.75%; - } - - &._2714 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 29.41176470588235% 25%; - } - - &._2716 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 0% 31.25%; - } - - &._2721 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 5.882352941176471% 31.25%; - } - - &._2733 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 11.764705882352942% 31.25%; - } - - &._2734 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 17.647058823529413% 31.25%; - } - - &._2747 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 23.529411764705884% 31.25%; - } - - &._2753 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 29.41176470588235% 31.25%; - } - - &._2754 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 35.294117647058826% 0%; - } - - &._2755 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 35.294117647058826% 6.25%; - } - - &._2757 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 35.294117647058826% 12.5%; - } - - &._2763 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 35.294117647058826% 18.75%; - } - - &._2764 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 35.294117647058826% 25%; - } - - &._2795 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 35.294117647058826% 31.25%; - } - - &._2796 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 0% 37.5%; - } - - &._2797 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 5.882352941176471% 37.5%; - } - - &._2934 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 11.764705882352942% 37.5%; - } - - &._2935 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 17.647058823529413% 37.5%; - } - - &._3030 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 23.529411764705884% 37.5%; - } - - &._3297 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 29.41176470588235% 37.5%; - } - - &._3299 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 35.294117647058826% 37.5%; - } - - &._1f9e1 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 41.1764705882353% 0%; - } - - &._1f49b { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 41.1764705882353% 6.25%; - } - - &._1f49a { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 41.1764705882353% 12.5%; - } - - &._1f499 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 41.1764705882353% 18.75%; - } - - &._1f49c { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 41.1764705882353% 25%; - } - - &._1f5a4 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 41.1764705882353% 31.25%; - } - - &._1f494 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 41.1764705882353% 37.5%; - } - - &._1f495 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 0% 43.75%; - } - - &._1f49e { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 5.882352941176471% 43.75%; - } - - &._1f493 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 11.764705882352942% 43.75%; - } - - &._1f497 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 17.647058823529413% 43.75%; - } - - &._1f496 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 23.529411764705884% 43.75%; - } - - &._1f498 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 29.41176470588235% 43.75%; - } - - &._1f49d { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 35.294117647058826% 43.75%; - } - - &._1f49f { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 41.1764705882353% 43.75%; - } - - &._262e { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 47.05882352941177% 0%; - } - - &._271d { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 47.05882352941177% 6.25%; - } - - &._262a { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 47.05882352941177% 12.5%; - } - - &._1f549 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 47.05882352941177% 18.75%; - } - - &._1f52f { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 47.05882352941177% 25%; - } - - &._1f54e { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 47.05882352941177% 31.25%; - } - - &._262f { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 47.05882352941177% 37.5%; - } - - &._1f6d0 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 47.05882352941177% 43.75%; - } - - &._26ce { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 0% 50%; - } - - &._264a { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 5.882352941176471% 50%; - } - - &._264b { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 11.764705882352942% 50%; - } - - &._264c { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 17.647058823529413% 50%; - } - - &._264d { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 23.529411764705884% 50%; - } - - &._264e { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 29.41176470588235% 50%; - } - - &._264f { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 35.294117647058826% 50%; - } - - &._1f194 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 41.1764705882353% 50%; - } - - &._269b { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 47.05882352941177% 50%; - } - - &._267e { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 52.94117647058823% 0%; - } - - &._1f251 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 52.94117647058823% 6.25%; - } - - &._1f4f4 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 52.94117647058823% 12.5%; - } - - &._1f4f3 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 52.94117647058823% 18.75%; - } - - &._1f236 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 52.94117647058823% 25%; - } - - &._1f21a { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 52.94117647058823% 31.25%; - } - - &._1f238 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 52.94117647058823% 37.5%; - } - - &._1f23a { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 52.94117647058823% 43.75%; - } - - &._1f237 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 52.94117647058823% 50%; - } - - &._1f19a { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 0% 56.25%; - } - - &._1f4ae { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 5.882352941176471% 56.25%; - } - - &._1f250 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 11.764705882352942% 56.25%; - } - - &._1f234 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 17.647058823529413% 56.25%; - } - - &._1f235 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 23.529411764705884% 56.25%; - } - - &._1f239 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 29.41176470588235% 56.25%; - } - - &._1f232 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 35.294117647058826% 56.25%; - } - - &._1f170 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 41.1764705882353% 56.25%; - } - - &._1f171 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 47.05882352941177% 56.25%; - } - - &._1f18e { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 52.94117647058823% 56.25%; - } - - &._1f191 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 58.8235294117647% 0%; - } - - &._1f17e { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 58.8235294117647% 6.25%; - } - - &._1f198 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 58.8235294117647% 12.5%; - } - - &._274c { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 58.8235294117647% 18.75%; - } - - &._2b55 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 58.8235294117647% 25%; - } - - &._1f6d1 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 58.8235294117647% 31.25%; - } - - &._26d4 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 58.8235294117647% 37.5%; - } - - &._1f4db { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 58.8235294117647% 43.75%; - } - - &._1f6ab { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 58.8235294117647% 50%; - } - - &._1f4af { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 58.8235294117647% 56.25%; - } - - &._1f4a2 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 0% 62.5%; - } - - &._1f6b7 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 5.882352941176471% 62.5%; - } - - &._1f6af { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 11.764705882352942% 62.5%; - } - - &._1f6b3 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 17.647058823529413% 62.5%; - } - - &._1f6b1 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 23.529411764705884% 62.5%; - } - - &._1f51e { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 29.41176470588235% 62.5%; - } - - &._1f4f5 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 35.294117647058826% 62.5%; - } - - &._1f6ad { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 41.1764705882353% 62.5%; - } - - &._203c { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 47.05882352941177% 62.5%; - } - - &._1f505 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 52.94117647058823% 62.5%; - } - - &._1f506 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 58.8235294117647% 62.5%; - } - - &._303d { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 64.70588235294117% 0%; - } - - &._26a0 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 64.70588235294117% 6.25%; - } - - &._1f6b8 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 64.70588235294117% 12.5%; - } - - &._1f531 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 64.70588235294117% 18.75%; - } - - &._269c { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 64.70588235294117% 25%; - } - - &._1f530 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 64.70588235294117% 31.25%; - } - - &._267b { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 64.70588235294117% 37.5%; - } - - &._1f22f { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 64.70588235294117% 43.75%; - } - - &._1f4b9 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 64.70588235294117% 50%; - } - - &._274e { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 64.70588235294117% 56.25%; - } - - &._1f310 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 64.70588235294117% 62.5%; - } - - &._1f4a0 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 0% 68.75%; - } - - &._24c2 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 5.882352941176471% 68.75%; - } - - &._1f300 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 11.764705882352942% 68.75%; - } - - &._1f4a4 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 17.647058823529413% 68.75%; - } - - &._1f3e7 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 23.529411764705884% 68.75%; - } - - &._1f6be { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 29.41176470588235% 68.75%; - } - - &._267f { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 35.294117647058826% 68.75%; - } - - &._1f17f { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 41.1764705882353% 68.75%; - } - - &._1f233 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 47.05882352941177% 68.75%; - } - - &._1f202 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 52.94117647058823% 68.75%; - } - - &._1f6c2 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 58.8235294117647% 68.75%; - } - - &._1f6c3 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 64.70588235294117% 68.75%; - } - - &._1f6c4 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 70.58823529411765% 0%; - } - - &._2122 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 70.58823529411765% 6.25%; - } - - &._1f6b9 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 70.58823529411765% 12.5%; - } - - &._1f6ba { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 70.58823529411765% 18.75%; - } - - &._1f6bc { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 70.58823529411765% 25%; - } - - &._1f6bb { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 70.58823529411765% 31.25%; - } - - &._1f6ae { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 70.58823529411765% 37.5%; - } - - &._1f3a6 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 70.58823529411765% 43.75%; - } - - &._1f4f6 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 70.58823529411765% 50%; - } - - &._1f201 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 70.58823529411765% 56.25%; - } - - &._1f523 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 70.58823529411765% 62.5%; - } - - &._1f524 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 70.58823529411765% 68.75%; - } - - &._1f521 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 0% 75%; - } - - &._1f520 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 5.882352941176471% 75%; - } - - &._1f196 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 11.764705882352942% 75%; - } - - &._1f197 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 17.647058823529413% 75%; - } - - &._1f199 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 23.529411764705884% 75%; - } - - &._1f192 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 29.41176470588235% 75%; - } - - &._1f195 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 35.294117647058826% 75%; - } - - &._1f193 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 41.1764705882353% 75%; - } - - &._0030-20e3 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 47.05882352941177% 75%; - } - - &._0031-20e3 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 52.94117647058823% 75%; - } - - &._0032-20e3 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 58.8235294117647% 75%; - } - - &._0033-20e3 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 64.70588235294117% 75%; - } - - &._0034-20e3 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 70.58823529411765% 75%; - } - - &._0035-20e3 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 76.47058823529412% 0%; - } - - &._0036-20e3 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 76.47058823529412% 6.25%; - } - - &._0037-20e3 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 76.47058823529412% 12.5%; - } - - &._0038-20e3 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 76.47058823529412% 18.75%; - } - - &._0039-20e3 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 76.47058823529412% 25%; - } - - &._1f51f { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 76.47058823529412% 31.25%; - } - - &._1f522 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 76.47058823529412% 37.5%; - } - - &._0023-20e3 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 76.47058823529412% 43.75%; - } - - &._002a-20e3 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 76.47058823529412% 50%; - } - - &._23cf { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 76.47058823529412% 56.25%; - } - - &._25b6 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 76.47058823529412% 62.5%; - } - - &._23f8 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 76.47058823529412% 68.75%; - } - - &._23ef { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 76.47058823529412% 75%; - } - - &._23f9 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 0% 81.25%; - } - - &._23fa { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 5.882352941176471% 81.25%; - } - - &._23ed { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 11.764705882352942% 81.25%; - } - - &._23ee { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 17.647058823529413% 81.25%; - } - - &._23e9 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 23.529411764705884% 81.25%; - } - - &._23ea { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 29.41176470588235% 81.25%; - } - - &._23eb { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 35.294117647058826% 81.25%; - } - - &._23ec { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 41.1764705882353% 81.25%; - } - - &._25c0 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 47.05882352941177% 81.25%; - } - - &._1f53c { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 52.94117647058823% 81.25%; - } - - &._1f53d { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 58.8235294117647% 81.25%; - } - - &._27a1 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 64.70588235294117% 81.25%; - } - - &._2b05 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 70.58823529411765% 81.25%; - } - - &._2b06 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 76.47058823529412% 81.25%; - } - - &._2b07 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 82.3529411764706% 0%; - } - - &._21aa { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 82.3529411764706% 6.25%; - } - - &._21a9 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 82.3529411764706% 12.5%; - } - - &._1f500 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 82.3529411764706% 18.75%; - } - - &._1f501 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 82.3529411764706% 25%; - } - - &._1f502 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 82.3529411764706% 31.25%; - } - - &._1f504 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 82.3529411764706% 37.5%; - } - - &._1f503 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 82.3529411764706% 43.75%; - } - - &._1f3b5 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 82.3529411764706% 50%; - } - - &._1f3b6 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 82.3529411764706% 56.25%; - } - - &._1f4b2 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 82.3529411764706% 62.5%; - } - - &._1f4b1 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 82.3529411764706% 68.75%; - } - - &._00a9 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 82.3529411764706% 75%; - } - - &._00ae { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 82.3529411764706% 81.25%; - } - - &._27b0 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 0% 87.5%; - } - - &._27bf { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 5.882352941176471% 87.5%; - } - - &._1f51a { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 11.764705882352942% 87.5%; - } - - &._1f519 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 17.647058823529413% 87.5%; - } - - &._1f51b { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 23.529411764705884% 87.5%; - } - - &._1f51d { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 29.41176470588235% 87.5%; - } - - &._1f51c { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 35.294117647058826% 87.5%; - } - - &._1f518 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 41.1764705882353% 87.5%; - } - - &._26aa { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 47.05882352941177% 87.5%; - } - - &._26ab { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 52.94117647058823% 87.5%; - } - - &._1f534 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 58.8235294117647% 87.5%; - } - - &._1f535 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 64.70588235294117% 87.5%; - } - - &._1f53a { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 70.58823529411765% 87.5%; - } - - &._1f53b { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 76.47058823529412% 87.5%; - } - - &._1f538 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 82.3529411764706% 87.5%; - } - - &._1f539 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 88.23529411764706% 0%; - } - - &._1f536 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 88.23529411764706% 6.25%; - } - - &._1f537 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 88.23529411764706% 12.5%; - } - - &._1f533 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 88.23529411764706% 18.75%; - } - - &._1f532 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 88.23529411764706% 25%; - } - - &._25aa { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 88.23529411764706% 31.25%; - } - - &._25ab { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 88.23529411764706% 37.5%; - } - - &._25fe { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 88.23529411764706% 43.75%; - } - - &._25fd { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 88.23529411764706% 50%; - } - - &._25fc { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 88.23529411764706% 56.25%; - } - - &._25fb { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 88.23529411764706% 62.5%; - } - - &._2b1b { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 88.23529411764706% 68.75%; - } - - &._2b1c { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 88.23529411764706% 75%; - } - - &._1f508 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 88.23529411764706% 81.25%; - } - - &._1f507 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 88.23529411764706% 87.5%; - } - - &._1f509 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 0% 93.75%; - } - - &._1f50a { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 5.882352941176471% 93.75%; - } - - &._1f514 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 11.764705882352942% 93.75%; - } - - &._1f515 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 17.647058823529413% 93.75%; - } - - &._1f4e3 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 23.529411764705884% 93.75%; - } - - &._1f4e2 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 29.41176470588235% 93.75%; - } - - &._1f5e8 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 35.294117647058826% 93.75%; - } - - &._1f441-1f5e8 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 41.1764705882353% 93.75%; - } - - &._1f4ac { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 47.05882352941177% 93.75%; - } - - &._1f4ad { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 52.94117647058823% 93.75%; - } - - &._1f5ef { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 58.8235294117647% 93.75%; - } - - &._1f0cf { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 64.70588235294117% 93.75%; - } - - &._1f3b4 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 70.58823529411765% 93.75%; - } - - &._1f004 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 76.47058823529412% 93.75%; - } - - &._1f550 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 82.3529411764706% 93.75%; - } - - &._1f551 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 88.23529411764706% 93.75%; - } - - &._1f552 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 94.11764705882354% 0%; - } - - &._1f553 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 94.11764705882354% 6.25%; - } - - &._1f554 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 94.11764705882354% 12.5%; - } - - &._1f555 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 94.11764705882354% 18.75%; - } - - &._1f556 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 94.11764705882354% 25%; - } - - &._1f557 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 94.11764705882354% 31.25%; - } - - &._1f558 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 94.11764705882354% 37.5%; - } - - &._1f559 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 94.11764705882354% 43.75%; - } - - &._1f55a { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 94.11764705882354% 50%; - } - - &._1f55b { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 94.11764705882354% 56.25%; - } - - &._1f55c { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 94.11764705882354% 62.5%; - } - - &._1f55d { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 94.11764705882354% 68.75%; - } - - &._1f55e { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 94.11764705882354% 75%; - } - - &._1f55f { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 94.11764705882354% 81.25%; - } - - &._1f560 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 94.11764705882354% 87.5%; - } - - &._1f561 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 94.11764705882354% 93.75%; - } - - &._1f562 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 0% 100%; - } - - &._1f563 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 5.882352941176471% 100%; - } - - &._1f564 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 11.764705882352942% 100%; - } - - &._1f565 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 17.647058823529413% 100%; - } - - &._1f566 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 23.529411764705884% 100%; - } - - &._1f567 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 29.41176470588235% 100%; - } - - &._0030 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 35.294117647058826% 100%; - } - - &._0031 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 41.1764705882353% 100%; - } - - &._0032 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 47.05882352941177% 100%; - } - - &._0033 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 52.94117647058823% 100%; - } - - &._0034 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 58.8235294117647% 100%; - } - - &._0035 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 64.70588235294117% 100%; - } - - &._0036 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 70.58823529411765% 100%; - } - - &._0037 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 76.47058823529412% 100%; - } - - &._0038 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 82.3529411764706% 100%; - } - - &._0039 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 88.23529411764706% 100%; - } - - &._0023 { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 94.11764705882354% 100%; - } - - &._002a { - background-repeat: no-repeat; - background-size: 1800% 1700%; - background-position: 100% 0%; - } - -} - diff --git a/apps/meteor/app/emoji-emojione/client/travel-sprites.css b/apps/meteor/app/emoji-emojione/client/travel-sprites.css deleted file mode 100644 index 93fc40ec635ea..0000000000000 --- a/apps/meteor/app/emoji-emojione/client/travel-sprites.css +++ /dev/null @@ -1,739 +0,0 @@ -/* stylelint-disable */ - -.emojione-travel { - background-image: url('/packages/emojione/travel-sprites.png'); - - &._1f5ff { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 0% 0%; - } - - &._2693 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 9.090909090909092% 0%; - } - - &._1f697 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 0% 10%; - } - - &._1f695 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 9.090909090909092% 10%; - } - - &._1f699 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 18.181818181818183% 0%; - } - - &._1f68c { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 18.181818181818183% 10%; - } - - &._1f68e { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 0% 20%; - } - - &._1f3ce { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 9.090909090909092% 20%; - } - - &._1f693 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 18.181818181818183% 20%; - } - - &._1f691 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 27.272727272727273% 0%; - } - - &._1f692 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 27.272727272727273% 10%; - } - - &._1f690 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 27.272727272727273% 20%; - } - - &._1f69a { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 0% 30%; - } - - &._1f69b { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 9.090909090909092% 30%; - } - - &._1f69c { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 18.181818181818183% 30%; - } - - &._1f6f4 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 27.272727272727273% 30%; - } - - &._1f6b2 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 36.36363636363637% 0%; - } - - &._1f6f5 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 36.36363636363637% 10%; - } - - &._1f3cd { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 36.36363636363637% 20%; - } - - &._1f6a8 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 36.36363636363637% 30%; - } - - &._1f694 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 0% 40%; - } - - &._1f68d { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 9.090909090909092% 40%; - } - - &._1f698 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 18.181818181818183% 40%; - } - - &._1f696 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 27.272727272727273% 40%; - } - - &._1f6a1 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 36.36363636363637% 40%; - } - - &._1f6a0 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 45.45454545454545% 0%; - } - - &._1f69f { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 45.45454545454545% 10%; - } - - &._1f683 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 45.45454545454545% 20%; - } - - &._1f68b { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 45.45454545454545% 30%; - } - - &._1f69e { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 45.45454545454545% 40%; - } - - &._1f69d { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 0% 50%; - } - - &._1f684 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 9.090909090909092% 50%; - } - - &._1f685 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 18.181818181818183% 50%; - } - - &._1f688 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 27.272727272727273% 50%; - } - - &._1f682 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 36.36363636363637% 50%; - } - - &._1f686 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 45.45454545454545% 50%; - } - - &._1f687 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 54.54545454545455% 0%; - } - - &._1f68a { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 54.54545454545455% 10%; - } - - &._1f689 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 54.54545454545455% 20%; - } - - &._1f6eb { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 54.54545454545455% 30%; - } - - &._1f6ec { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 54.54545454545455% 40%; - } - - &._1f6e9 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 54.54545454545455% 50%; - } - - &._1f4ba { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 0% 60%; - } - - &._1f9f3 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 9.090909090909092% 60%; - } - - &._1f6f0 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 18.181818181818183% 60%; - } - - &._1f680 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 27.272727272727273% 60%; - } - - &._1f6f8 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 36.36363636363637% 60%; - } - - &._1f681 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 45.45454545454545% 60%; - } - - &._1f6f6 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 54.54545454545455% 60%; - } - - &._26f5 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 63.63636363636363% 0%; - } - - &._1f6a4 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 63.63636363636363% 10%; - } - - &._1f6e5 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 63.63636363636363% 20%; - } - - &._1f6f3 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 63.63636363636363% 30%; - } - - &._26f4 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 63.63636363636363% 40%; - } - - &._1f6a2 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 63.63636363636363% 50%; - } - - &._26fd { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 63.63636363636363% 60%; - } - - &._1f6a7 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 0% 70%; - } - - &._1f6a6 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 9.090909090909092% 70%; - } - - &._1f6a5 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 18.181818181818183% 70%; - } - - &._1f68f { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 27.272727272727273% 70%; - } - - &._1f5fa { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 36.36363636363637% 70%; - } - - &._2708 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 45.45454545454545% 70%; - } - - &._1f5fd { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 54.54545454545455% 70%; - } - - &._1f5fc { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 63.63636363636363% 70%; - } - - &._1f3f0 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 72.72727272727273% 0%; - } - - &._1f3ef { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 72.72727272727273% 10%; - } - - &._1f3df { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 72.72727272727273% 20%; - } - - &._1f3a1 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 72.72727272727273% 30%; - } - - &._1f3a2 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 72.72727272727273% 40%; - } - - &._1f3a0 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 72.72727272727273% 50%; - } - - &._26f2 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 72.72727272727273% 60%; - } - - &._26f1 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 72.72727272727273% 70%; - } - - &._1f3d6 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 0% 80%; - } - - &._1f3dd { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 9.090909090909092% 80%; - } - - &._1f3dc { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 18.181818181818183% 80%; - } - - &._1f30b { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 27.272727272727273% 80%; - } - - &._26f0 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 36.36363636363637% 80%; - } - - &._1f3d4 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 45.45454545454545% 80%; - } - - &._1f5fb { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 54.54545454545455% 80%; - } - - &._1f3d5 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 63.63636363636363% 80%; - } - - &._26fa { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 72.72727272727273% 80%; - } - - &._1f3e0 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 81.81818181818181% 0%; - } - - &._1f3e1 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 81.81818181818181% 10%; - } - - &._1f3d8 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 81.81818181818181% 20%; - } - - &._1f3da { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 81.81818181818181% 30%; - } - - &._1f3d7 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 81.81818181818181% 40%; - } - - &._1f3ed { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 81.81818181818181% 50%; - } - - &._1f3e2 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 81.81818181818181% 60%; - } - - &._1f3ec { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 81.81818181818181% 70%; - } - - &._1f3e3 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 81.81818181818181% 80%; - } - - &._1f3e4 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 0% 90%; - } - - &._1f3e5 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 9.090909090909092% 90%; - } - - &._1f3e6 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 18.181818181818183% 90%; - } - - &._1f3e8 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 27.272727272727273% 90%; - } - - &._1f3ea { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 36.36363636363637% 90%; - } - - &._1f3eb { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 45.45454545454545% 90%; - } - - &._1f3e9 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 54.54545454545455% 90%; - } - - &._1f492 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 63.63636363636363% 90%; - } - - &._1f3db { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 72.72727272727273% 90%; - } - - &._26ea { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 81.81818181818181% 90%; - } - - &._1f54c { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 90.9090909090909% 0%; - } - - &._1f54d { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 90.9090909090909% 10%; - } - - &._1f54b { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 90.9090909090909% 20%; - } - - &._26e9 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 90.9090909090909% 30%; - } - - &._1f6e4 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 90.9090909090909% 40%; - } - - &._1f6e3 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 90.9090909090909% 50%; - } - - &._1f5fe { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 90.9090909090909% 60%; - } - - &._1f391 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 90.9090909090909% 70%; - } - - &._1f3de { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 90.9090909090909% 80%; - } - - &._1f305 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 90.9090909090909% 90%; - } - - &._1f304 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 0% 100%; - } - - &._1f320 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 9.090909090909092% 100%; - } - - &._1f387 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 18.181818181818183% 100%; - } - - &._1f386 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 27.272727272727273% 100%; - } - - &._1f9e8 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 36.36363636363637% 100%; - } - - &._1f307 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 45.45454545454545% 100%; - } - - &._1f306 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 54.54545454545455% 100%; - } - - &._1f3d9 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 63.63636363636363% 100%; - } - - &._1f303 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 72.72727272727273% 100%; - } - - &._1f30c { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 81.81818181818181% 100%; - } - - &._1f309 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 90.9090909090909% 100%; - } - - &._1f301 { - background-repeat: no-repeat; - background-size: 1200% 1100%; - background-position: 100% 0%; - } - -} - diff --git a/apps/meteor/app/emoji-emojione/lib/emojiPicker.ts b/apps/meteor/app/emoji-emojione/lib/emojiPicker.ts deleted file mode 100644 index ecc0c35eb8487..0000000000000 --- a/apps/meteor/app/emoji-emojione/lib/emojiPicker.ts +++ /dev/null @@ -1,1951 +0,0 @@ -/* - * This file is automatically generated from generateEmojiIndex.mjs - * Last generated Tue Apr 09 2019 15:27:21 GMT-0300 (-03) - * - * Mapping category hashes into human readable and translated names - */ - -export const emojiCategories = [ - { key: 'people', i18n: 'Smileys_and_People' }, - { key: 'nature', i18n: 'Animals_and_Nature' }, - { key: 'food', i18n: 'Food_and_Drink' }, - { key: 'activity', i18n: 'Activity' }, - { key: 'travel', i18n: 'Travel_and_Places' }, - { key: 'objects', i18n: 'Objects' }, - { key: 'symbols', i18n: 'Symbols' }, - { key: 'flags', i18n: 'Flags' }, -]; - -export const toneList = { - 'snowboarder': 1, - 'person_lifting_weights': 1, - 'woman_lifting_weights': 1, - 'man_lifting_weights': 1, - 'person_doing_cartwheel': 1, - 'woman_cartwheeling': 1, - 'man_cartwheeling': 1, - 'person_bouncing_ball': 1, - 'woman_bouncing_ball': 1, - 'man_bouncing_ball': 1, - 'person_playing_handball': 1, - 'woman_playing_handball': 1, - 'man_playing_handball': 1, - 'person_golfing': 1, - 'woman_golfing': 1, - 'man_golfing': 1, - 'horse_racing': 1, - 'person_in_lotus_position': 1, - 'woman_in_lotus_position': 1, - 'man_in_lotus_position': 1, - 'person_surfing': 1, - 'woman_surfing': 1, - 'man_surfing': 1, - 'person_swimming': 1, - 'woman_swimming': 1, - 'man_swimming': 1, - 'person_playing_water_polo': 1, - 'woman_playing_water_polo': 1, - 'man_playing_water_polo': 1, - 'person_rowing_boat': 1, - 'woman_rowing_boat': 1, - 'man_rowing_boat': 1, - 'person_climbing': 1, - 'woman_climbing': 1, - 'man_climbing': 1, - 'person_mountain_biking': 1, - 'woman_mountain_biking': 1, - 'man_mountain_biking': 1, - 'person_biking': 1, - 'woman_biking': 1, - 'man_biking': 1, - 'person_juggling': 1, - 'woman_juggling': 1, - 'man_juggling': 1, - 'bath': 1, - 'person_in_bed': 1, - 'palms_up_together': 1, - 'open_hands': 1, - 'raised_hands': 1, - 'clap': 1, - 'thumbsup': 1, - 'thumbsdown': 1, - 'punch': 1, - 'fist': 1, - 'left_facing_fist': 1, - 'right_facing_fist': 1, - 'fingers_crossed': 1, - 'v': 1, - 'love_you_gesture': 1, - 'metal': 1, - 'ok_hand': 1, - 'point_left': 1, - 'point_right': 1, - 'point_up_2': 1, - 'point_down': 1, - 'point_up': 1, - 'raised_hand': 1, - 'raised_back_of_hand': 1, - 'hand_splayed': 1, - 'vulcan': 1, - 'wave': 1, - 'call_me': 1, - 'muscle': 1, - 'leg': 1, - 'foot': 1, - 'middle_finger': 1, - 'writing_hand': 1, - 'pray': 1, - 'ear': 1, - 'nose': 1, - 'baby': 1, - 'girl': 1, - 'child': 1, - 'boy': 1, - 'woman': 1, - 'adult': 1, - 'man': 1, - 'blond_haired_person': 1, - 'blond-haired_woman': 1, - 'blond-haired_man': 1, - 'woman_red_haired': 1, - 'man_red_haired': 1, - 'woman_curly_haired': 1, - 'man_curly_haired': 1, - 'woman_white_haired': 1, - 'man_white_haired': 1, - 'woman_bald': 1, - 'man_bald': 1, - 'bearded_person': 1, - 'older_woman': 1, - 'older_adult': 1, - 'older_man': 1, - 'man_with_chinese_cap': 1, - 'person_wearing_turban': 1, - 'woman_wearing_turban': 1, - 'man_wearing_turban': 1, - 'woman_with_headscarf': 1, - 'police_officer': 1, - 'woman_police_officer': 1, - 'man_police_officer': 1, - 'construction_worker': 1, - 'woman_construction_worker': 1, - 'man_construction_worker': 1, - 'guard': 1, - 'woman_guard': 1, - 'man_guard': 1, - 'detective': 1, - 'woman_detective': 1, - 'man_detective': 1, - 'woman_health_worker': 1, - 'man_health_worker': 1, - 'woman_farmer': 1, - 'man_farmer': 1, - 'woman_cook': 1, - 'man_cook': 1, - 'woman_student': 1, - 'man_student': 1, - 'woman_singer': 1, - 'man_singer': 1, - 'woman_teacher': 1, - 'man_teacher': 1, - 'woman_factory_worker': 1, - 'man_factory_worker': 1, - 'woman_technologist': 1, - 'man_technologist': 1, - 'woman_office_worker': 1, - 'man_office_worker': 1, - 'woman_mechanic': 1, - 'man_mechanic': 1, - 'woman_scientist': 1, - 'man_scientist': 1, - 'woman_artist': 1, - 'man_artist': 1, - 'woman_firefighter': 1, - 'man_firefighter': 1, - 'woman_pilot': 1, - 'man_pilot': 1, - 'woman_astronaut': 1, - 'man_astronaut': 1, - 'woman_judge': 1, - 'man_judge': 1, - 'bride_with_veil': 1, - 'man_in_tuxedo': 1, - 'princess': 1, - 'prince': 1, - 'mrs_claus': 1, - 'santa': 1, - 'superhero': 1, - 'woman_superhero': 1, - 'man_superhero': 1, - 'supervillain': 1, - 'woman_supervillain': 1, - 'man_supervillain': 1, - 'mage': 1, - 'woman_mage': 1, - 'man_mage': 1, - 'elf': 1, - 'woman_elf': 1, - 'man_elf': 1, - 'vampire': 1, - 'woman_vampire': 1, - 'man_vampire': 1, - 'merperson': 1, - 'mermaid': 1, - 'merman': 1, - 'fairy': 1, - 'woman_fairy': 1, - 'man_fairy': 1, - 'angel': 1, - 'pregnant_woman': 1, - 'breast_feeding': 1, - 'person_bowing': 1, - 'woman_bowing': 1, - 'man_bowing': 1, - 'person_tipping_hand': 1, - 'woman_tipping_hand': 1, - 'man_tipping_hand': 1, - 'person_gesturing_no': 1, - 'woman_gesturing_no': 1, - 'man_gesturing_no': 1, - 'person_gesturing_ok': 1, - 'woman_gesturing_ok': 1, - 'man_gesturing_ok': 1, - 'person_raising_hand': 1, - 'woman_raising_hand': 1, - 'man_raising_hand': 1, - 'person_facepalming': 1, - 'woman_facepalming': 1, - 'man_facepalming': 1, - 'person_shrugging': 1, - 'woman_shrugging': 1, - 'man_shrugging': 1, - 'person_pouting': 1, - 'woman_pouting': 1, - 'man_pouting': 1, - 'person_frowning': 1, - 'woman_frowning': 1, - 'man_frowning': 1, - 'person_getting_haircut': 1, - 'woman_getting_haircut': 1, - 'man_getting_haircut': 1, - 'person_getting_massage': 1, - 'woman_getting_face_massage': 1, - 'man_getting_face_massage': 1, - 'person_in_steamy_room': 1, - 'woman_in_steamy_room': 1, - 'man_in_steamy_room': 1, - 'nail_care': 1, - 'selfie': 1, - 'dancer': 1, - 'man_dancing': 1, - 'levitate': 1, - 'person_walking': 1, - 'woman_walking': 1, - 'man_walking': 1, - 'person_running': 1, - 'woman_running': 1, - 'man_running': 1, -}; -export const emojisByCategory = { - symbols: [ - 'heart', - 'orange_heart', - 'yellow_heart', - 'green_heart', - 'blue_heart', - 'purple_heart', - 'black_heart', - 'broken_heart', - 'heart_exclamation', - 'two_hearts', - 'revolving_hearts', - 'heartbeat', - 'heartpulse', - 'sparkling_heart', - 'cupid', - 'gift_heart', - 'heart_decoration', - 'peace', - 'cross', - 'star_and_crescent', - 'om_symbol', - 'wheel_of_dharma', - 'star_of_david', - 'six_pointed_star', - 'menorah', - 'yin_yang', - 'orthodox_cross', - 'place_of_worship', - 'ophiuchus', - 'aries', - 'taurus', - 'gemini', - 'cancer', - 'leo', - 'virgo', - 'libra', - 'scorpius', - 'sagittarius', - 'capricorn', - 'aquarius', - 'pisces', - 'id', - 'atom', - 'infinity', - 'accept', - 'radioactive', - 'biohazard', - 'mobile_phone_off', - 'vibration_mode', - 'u6709', - 'u7121', - 'u7533', - 'u55b6', - 'u6708', - 'eight_pointed_black_star', - 'vs', - 'white_flower', - 'ideograph_advantage', - 'secret', - 'congratulations', - 'u5408', - 'u6e80', - 'u5272', - 'u7981', - 'a', - 'b', - 'ab', - 'cl', - 'o2', - 'sos', - 'x', - 'o', - 'octagonal_sign', - 'no_entry', - 'name_badge', - 'no_entry_sign', - '100', - 'anger', - 'hotsprings', - 'no_pedestrians', - 'do_not_litter', - 'no_bicycles', - 'non-potable_water', - 'underage', - 'no_mobile_phones', - 'no_smoking', - 'exclamation', - 'grey_exclamation', - 'question', - 'grey_question', - 'bangbang', - 'interrobang', - 'low_brightness', - 'high_brightness', - 'part_alternation_mark', - 'warning', - 'children_crossing', - 'trident', - 'fleur-de-lis', - 'beginner', - 'recycle', - 'white_check_mark', - 'u6307', - 'chart', - 'sparkle', - 'eight_spoked_asterisk', - 'negative_squared_cross_mark', - 'globe_with_meridians', - 'diamond_shape_with_a_dot_inside', - 'm', - 'cyclone', - 'zzz', - 'atm', - 'wc', - 'wheelchair', - 'parking', - 'u7a7a', - 'sa', - 'passport_control', - 'customs', - 'baggage_claim', - 'left_luggage', - 'mens', - 'womens', - 'baby_symbol', - 'restroom', - 'put_litter_in_its_place', - 'cinema', - 'signal_strength', - 'koko', - 'symbols', - 'information_source', - 'abc', - 'abcd', - 'capital_abcd', - 'ng', - 'ok', - 'up', - 'cool', - 'new', - 'free', - 'zero', - 'one', - 'two', - 'three', - 'four', - 'five', - 'six', - 'seven', - 'eight', - 'nine', - 'keycap_ten', - '1234', - 'hash', - 'asterisk', - 'eject', - 'arrow_forward', - 'pause_button', - 'play_pause', - 'stop_button', - 'record_button', - 'track_next', - 'track_previous', - 'fast_forward', - 'rewind', - 'arrow_double_up', - 'arrow_double_down', - 'arrow_backward', - 'arrow_up_small', - 'arrow_down_small', - 'arrow_right', - 'arrow_left', - 'arrow_up', - 'arrow_down', - 'arrow_upper_right', - 'arrow_lower_right', - 'arrow_lower_left', - 'arrow_upper_left', - 'arrow_up_down', - 'left_right_arrow', - 'arrow_right_hook', - 'leftwards_arrow_with_hook', - 'arrow_heading_up', - 'arrow_heading_down', - 'twisted_rightwards_arrows', - 'repeat', - 'repeat_one', - 'arrows_counterclockwise', - 'arrows_clockwise', - 'musical_note', - 'notes', - 'heavy_plus_sign', - 'heavy_minus_sign', - 'heavy_division_sign', - 'heavy_multiplication_x', - 'heavy_dollar_sign', - 'currency_exchange', - 'tm', - 'copyright', - 'registered', - 'wavy_dash', - 'curly_loop', - 'loop', - 'end', - 'back', - 'on', - 'top', - 'soon', - 'heavy_check_mark', - 'ballot_box_with_check', - 'radio_button', - 'white_circle', - 'black_circle', - 'red_circle', - 'blue_circle', - 'small_red_triangle', - 'small_red_triangle_down', - 'small_orange_diamond', - 'small_blue_diamond', - 'large_orange_diamond', - 'large_blue_diamond', - 'white_square_button', - 'black_square_button', - 'black_small_square', - 'white_small_square', - 'black_medium_small_square', - 'white_medium_small_square', - 'black_medium_square', - 'white_medium_square', - 'black_large_square', - 'white_large_square', - 'speaker', - 'mute', - 'sound', - 'loud_sound', - 'bell', - 'no_bell', - 'mega', - 'loudspeaker', - 'speech_left', - 'eye_in_speech_bubble', - 'speech_balloon', - 'thought_balloon', - 'anger_right', - 'spades', - 'clubs', - 'hearts', - 'diamonds', - 'black_joker', - 'flower_playing_cards', - 'mahjong', - 'clock1', - 'clock2', - 'clock3', - 'clock4', - 'clock5', - 'clock6', - 'clock7', - 'clock8', - 'clock9', - 'clock10', - 'clock11', - 'clock12', - 'clock130', - 'clock230', - 'clock330', - 'clock430', - 'clock530', - 'clock630', - 'clock730', - 'clock830', - 'clock930', - 'clock1030', - 'clock1130', - 'clock1230', - 'digit_zero', - 'digit_one', - 'digit_two', - 'digit_three', - 'digit_four', - 'digit_five', - 'digit_six', - 'digit_seven', - 'digit_eight', - 'digit_nine', - 'pound_symbol', - 'asterisk_symbol', - 'female_sign', - 'male_sign', - 'medical_symbol', - ], - activity: [ - 'soccer', - 'basketball', - 'football', - 'baseball', - 'softball', - 'tennis', - 'volleyball', - 'rugby_football', - '8ball', - 'ping_pong', - 'badminton', - 'goal', - 'hockey', - 'field_hockey', - 'cricket_game', - 'lacrosse', - 'golf', - 'flying_disc', - 'bow_and_arrow', - 'fishing_pole_and_fish', - 'boxing_glove', - 'martial_arts_uniform', - 'running_shirt_with_sash', - 'skateboard', - 'ice_skate', - 'curling_stone', - 'sled', - 'ski', - 'skier', - 'snowboarder', - 'person_lifting_weights', - 'woman_lifting_weights', - 'man_lifting_weights', - 'people_wrestling', - 'women_wrestling', - 'men_wrestling', - 'person_doing_cartwheel', - 'woman_cartwheeling', - 'man_cartwheeling', - 'person_bouncing_ball', - 'woman_bouncing_ball', - 'man_bouncing_ball', - 'person_fencing', - 'person_playing_handball', - 'woman_playing_handball', - 'man_playing_handball', - 'person_golfing', - 'woman_golfing', - 'man_golfing', - 'horse_racing', - 'person_in_lotus_position', - 'woman_in_lotus_position', - 'man_in_lotus_position', - 'person_surfing', - 'woman_surfing', - 'man_surfing', - 'person_swimming', - 'woman_swimming', - 'man_swimming', - 'person_playing_water_polo', - 'woman_playing_water_polo', - 'man_playing_water_polo', - 'person_rowing_boat', - 'woman_rowing_boat', - 'man_rowing_boat', - 'person_climbing', - 'woman_climbing', - 'man_climbing', - 'person_mountain_biking', - 'woman_mountain_biking', - 'man_mountain_biking', - 'person_biking', - 'woman_biking', - 'man_biking', - 'trophy', - 'first_place', - 'second_place', - 'third_place', - 'medal', - 'military_medal', - 'rosette', - 'reminder_ribbon', - 'ticket', - 'tickets', - 'circus_tent', - 'person_juggling', - 'woman_juggling', - 'man_juggling', - 'performing_arts', - 'art', - 'clapper', - 'microphone', - 'headphones', - 'musical_score', - 'musical_keyboard', - 'drum', - 'saxophone', - 'trumpet', - 'guitar', - 'violin', - 'game_die', - 'dart', - 'bowling', - 'video_game', - 'slot_machine', - ], - objects: [ - 'watch', - 'iphone', - 'calling', - 'computer', - 'keyboard', - 'desktop', - 'printer', - 'mouse_three_button', - 'trackball', - 'joystick', - 'chess_pawn', - 'jigsaw', - 'compression', - 'minidisc', - 'floppy_disk', - 'cd', - 'dvd', - 'vhs', - 'camera', - 'camera_with_flash', - 'video_camera', - 'movie_camera', - 'projector', - 'film_frames', - 'telephone_receiver', - 'telephone', - 'pager', - 'fax', - 'tv', - 'radio', - 'microphone2', - 'level_slider', - 'control_knobs', - 'stopwatch', - 'timer', - 'alarm_clock', - 'clock', - 'hourglass', - 'hourglass_flowing_sand', - 'satellite', - 'compass', - 'battery', - 'electric_plug', - 'magnet', - 'bulb', - 'flashlight', - 'candle', - 'fire_extinguisher', - 'wastebasket', - 'oil', - 'money_with_wings', - 'dollar', - 'yen', - 'euro', - 'pound', - 'moneybag', - 'credit_card', - 'gem', - 'nazar_amulet', - 'bricks', - 'scales', - 'toolbox', - 'wrench', - 'hammer', - 'hammer_pick', - 'tools', - 'pick', - 'nut_and_bolt', - 'gear', - 'chains', - 'gun', - 'bomb', - 'knife', - 'dagger', - 'crossed_swords', - 'shield', - 'smoking', - 'coffin', - 'urn', - 'amphora', - 'crystal_ball', - 'prayer_beads', - 'barber', - 'alembic', - 'test_tube', - 'petri_dish', - 'dna', - 'abacus', - 'telescope', - 'microscope', - 'hole', - 'pill', - 'syringe', - 'thermometer', - 'toilet', - 'potable_water', - 'shower', - 'bathtub', - 'bath', - 'broom', - 'basket', - 'roll_of_paper', - 'soap', - 'sponge', - 'squeeze_bottle', - 'thread', - 'yarn', - 'bellhop', - 'key', - 'key2', - 'door', - 'couch', - 'bed', - 'sleeping_accommodation', - 'teddy_bear', - 'frame_photo', - 'shopping_bags', - 'shopping_cart', - 'gift', - 'balloon', - 'flags', - 'ribbon', - 'confetti_ball', - 'tada', - 'dolls', - 'izakaya_lantern', - 'wind_chime', - 'red_envelope', - 'envelope', - 'envelope_with_arrow', - 'incoming_envelope', - 'e-mail', - 'love_letter', - 'inbox_tray', - 'outbox_tray', - 'package', - 'label', - 'mailbox_closed', - 'mailbox', - 'mailbox_with_mail', - 'mailbox_with_no_mail', - 'postbox', - 'postal_horn', - 'scroll', - 'page_with_curl', - 'page_facing_up', - 'receipt', - 'bookmark_tabs', - 'bar_chart', - 'chart_with_upwards_trend', - 'chart_with_downwards_trend', - 'notepad_spiral', - 'calendar_spiral', - 'calendar', - 'date', - 'card_index', - 'card_box', - 'ballot_box', - 'file_cabinet', - 'clipboard', - 'file_folder', - 'open_file_folder', - 'dividers', - 'newspaper2', - 'newspaper', - 'notebook', - 'notebook_with_decorative_cover', - 'ledger', - 'closed_book', - 'green_book', - 'blue_book', - 'orange_book', - 'books', - 'book', - 'bookmark', - 'link', - 'paperclip', - 'paperclips', - 'triangular_ruler', - 'straight_ruler', - 'safety_pin', - 'pushpin', - 'round_pushpin', - 'scissors', - 'pen_ballpoint', - 'pen_fountain', - 'black_nib', - 'paintbrush', - 'crayon', - 'pencil', - 'pencil2', - 'mag', - 'mag_right', - 'lock_with_ink_pen', - 'closed_lock_with_key', - 'lock', - 'unlock', - ], - nature: [ - 'dog', - 'cat', - 'mouse', - 'hamster', - 'rabbit', - 'fox', - 'raccoon', - 'bear', - 'panda_face', - 'kangaroo', - 'badger', - 'koala', - 'tiger', - 'lion_face', - 'cow', - 'pig', - 'pig_nose', - 'frog', - 'monkey_face', - 'see_no_evil', - 'hear_no_evil', - 'speak_no_evil', - 'monkey', - 'chicken', - 'penguin', - 'bird', - 'baby_chick', - 'hatching_chick', - 'hatched_chick', - 'duck', - 'swan', - 'eagle', - 'owl', - 'parrot', - 'peacock', - 'bat', - 'wolf', - 'boar', - 'horse', - 'unicorn', - 'bee', - 'bug', - 'butterfly', - 'snail', - 'shell', - 'beetle', - 'ant', - 'cricket', - 'spider', - 'spider_web', - 'scorpion', - 'mosquito', - 'microbe', - 'turtle', - 'snake', - 'lizard', - 't_rex', - 'sauropod', - 'octopus', - 'squid', - 'shrimp', - 'crab', - 'lobster', - 'blowfish', - 'tropical_fish', - 'fish', - 'dolphin', - 'whale', - 'whale2', - 'shark', - 'crocodile', - 'tiger2', - 'leopard', - 'zebra', - 'gorilla', - 'elephant', - 'rhino', - 'hippopotamus', - 'dromedary_camel', - 'camel', - 'giraffe', - 'llama', - 'water_buffalo', - 'ox', - 'cow2', - 'racehorse', - 'pig2', - 'ram', - 'sheep', - 'goat', - 'deer', - 'dog2', - 'poodle', - 'cat2', - 'rooster', - 'turkey', - 'dove', - 'rabbit2', - 'mouse2', - 'rat', - 'chipmunk', - 'hedgehog', - 'feet', - 'dragon', - 'dragon_face', - 'cactus', - 'christmas_tree', - 'evergreen_tree', - 'deciduous_tree', - 'palm_tree', - 'seedling', - 'herb', - 'shamrock', - 'four_leaf_clover', - 'bamboo', - 'tanabata_tree', - 'leaves', - 'fallen_leaf', - 'maple_leaf', - 'mushroom', - 'ear_of_rice', - 'bouquet', - 'tulip', - 'rose', - 'wilted_rose', - 'hibiscus', - 'cherry_blossom', - 'blossom', - 'sunflower', - 'sun_with_face', - 'full_moon_with_face', - 'first_quarter_moon_with_face', - 'last_quarter_moon_with_face', - 'new_moon_with_face', - 'full_moon', - 'waning_gibbous_moon', - 'last_quarter_moon', - 'waning_crescent_moon', - 'new_moon', - 'waxing_crescent_moon', - 'first_quarter_moon', - 'waxing_gibbous_moon', - 'crescent_moon', - 'earth_americas', - 'earth_africa', - 'earth_asia', - 'dizzy', - 'star', - 'star2', - 'sparkles', - 'zap', - 'comet', - 'boom', - 'fire', - 'cloud_tornado', - 'rainbow', - 'sunny', - 'white_sun_small_cloud', - 'partly_sunny', - 'white_sun_cloud', - 'cloud', - 'white_sun_rain_cloud', - 'cloud_rain', - 'thunder_cloud_rain', - 'cloud_lightning', - 'cloud_snow', - 'snowflake', - 'snowman2', - 'snowman', - 'wind_blowing_face', - 'dash', - 'droplet', - 'sweat_drops', - 'umbrella', - 'umbrella2', - 'ocean', - 'fog', - ], - food: [ - 'green_apple', - 'apple', - 'pear', - 'tangerine', - 'lemon', - 'banana', - 'watermelon', - 'grapes', - 'strawberry', - 'melon', - 'cherries', - 'peach', - 'mango', - 'pineapple', - 'coconut', - 'kiwi', - 'tomato', - 'eggplant', - 'avocado', - 'broccoli', - 'leafy_green', - 'cucumber', - 'hot_pepper', - 'corn', - 'carrot', - 'potato', - 'sweet_potato', - 'croissant', - 'bread', - 'french_bread', - 'pretzel', - 'bagel', - 'cheese', - 'egg', - 'cooking', - 'pancakes', - 'bacon', - 'cut_of_meat', - 'poultry_leg', - 'meat_on_bone', - 'hotdog', - 'hamburger', - 'fries', - 'pizza', - 'sandwich', - 'stuffed_flatbread', - 'taco', - 'burrito', - 'salad', - 'shallow_pan_of_food', - 'canned_food', - 'spaghetti', - 'ramen', - 'stew', - 'curry', - 'sushi', - 'bento', - 'fried_shrimp', - 'rice_ball', - 'rice', - 'rice_cracker', - 'fish_cake', - 'fortune_cookie', - 'oden', - 'dango', - 'shaved_ice', - 'ice_cream', - 'icecream', - 'pie', - 'cake', - 'birthday', - 'moon_cake', - 'cupcake', - 'custard', - 'lollipop', - 'candy', - 'chocolate_bar', - 'popcorn', - 'salt', - 'doughnut', - 'dumpling', - 'cookie', - 'chestnut', - 'peanuts', - 'honey_pot', - 'milk', - 'baby_bottle', - 'coffee', - 'tea', - 'cup_with_straw', - 'sake', - 'beer', - 'beers', - 'champagne_glass', - 'wine_glass', - 'tumbler_glass', - 'cocktail', - 'tropical_drink', - 'champagne', - 'spoon', - 'fork_and_knife', - 'fork_knife_plate', - 'bowl_with_spoon', - 'takeout_box', - 'chopsticks', - ], - people: [ - 'grinning', - 'smiley', - 'smile', - 'grin', - 'laughing', - 'sweat_smile', - 'joy', - 'rofl', - 'relaxed', - 'blush', - 'innocent', - 'slight_smile', - 'upside_down', - 'wink', - 'relieved', - 'heart_eyes', - 'kissing_heart', - 'smiling_face_with_3_hearts', - 'kissing', - 'kissing_smiling_eyes', - 'kissing_closed_eyes', - 'yum', - 'stuck_out_tongue', - 'stuck_out_tongue_closed_eyes', - 'stuck_out_tongue_winking_eye', - 'zany_face', - 'face_with_raised_eyebrow', - 'face_with_monocle', - 'nerd', - 'sunglasses', - 'star_struck', - 'partying_face', - 'smirk', - 'unamused', - 'disappointed', - 'pensive', - 'worried', - 'confused', - 'slight_frown', - 'frowning2', - 'persevere', - 'confounded', - 'tired_face', - 'weary', - 'cry', - 'sob', - 'triumph', - 'angry', - 'rage', - 'face_with_symbols_over_mouth', - 'exploding_head', - 'flushed', - 'scream', - 'fearful', - 'cold_sweat', - 'hot_face', - 'cold_face', - 'pleading_face', - 'disappointed_relieved', - 'sweat', - 'hugging', - 'thinking', - 'face_with_hand_over_mouth', - 'shushing_face', - 'lying_face', - 'no_mouth', - 'neutral_face', - 'expressionless', - 'grimacing', - 'rolling_eyes', - 'hushed', - 'frowning', - 'anguished', - 'open_mouth', - 'astonished', - 'sleeping', - 'drooling_face', - 'sleepy', - 'dizzy_face', - 'zipper_mouth', - 'woozy_face', - 'nauseated_face', - 'face_vomiting', - 'sneezing_face', - 'mask', - 'thermometer_face', - 'head_bandage', - 'money_mouth', - 'cowboy', - 'smiling_imp', - 'imp', - 'japanese_ogre', - 'japanese_goblin', - 'clown', - 'poop', - 'ghost', - 'skull', - 'skull_crossbones', - 'alien', - 'space_invader', - 'robot', - 'jack_o_lantern', - 'smiley_cat', - 'smile_cat', - 'joy_cat', - 'heart_eyes_cat', - 'smirk_cat', - 'kissing_cat', - 'scream_cat', - 'crying_cat_face', - 'pouting_cat', - 'palms_up_together', - 'open_hands', - 'raised_hands', - 'clap', - 'handshake', - 'thumbsup', - 'thumbsdown', - 'punch', - 'fist', - 'left_facing_fist', - 'right_facing_fist', - 'fingers_crossed', - 'v', - 'love_you_gesture', - 'metal', - 'ok_hand', - 'point_left', - 'point_right', - 'point_up_2', - 'point_down', - 'point_up', - 'raised_hand', - 'raised_back_of_hand', - 'hand_splayed', - 'vulcan', - 'wave', - 'call_me', - 'muscle', - 'leg', - 'foot', - 'middle_finger', - 'writing_hand', - 'pray', - 'ring', - 'lipstick', - 'kiss', - 'lips', - 'tongue', - 'ear', - 'nose', - 'footprints', - 'eye', - 'eyes', - 'brain', - 'bone', - 'tooth', - 'speaking_head', - 'bust_in_silhouette', - 'busts_in_silhouette', - 'baby', - 'girl', - 'child', - 'boy', - 'woman', - 'adult', - 'man', - 'blond_haired_person', - 'blond-haired_woman', - 'blond-haired_man', - 'woman_red_haired', - 'man_red_haired', - 'woman_curly_haired', - 'man_curly_haired', - 'woman_white_haired', - 'man_white_haired', - 'woman_bald', - 'man_bald', - 'bearded_person', - 'older_woman', - 'older_adult', - 'older_man', - 'man_with_chinese_cap', - 'person_wearing_turban', - 'woman_wearing_turban', - 'man_wearing_turban', - 'woman_with_headscarf', - 'police_officer', - 'woman_police_officer', - 'man_police_officer', - 'construction_worker', - 'woman_construction_worker', - 'man_construction_worker', - 'guard', - 'woman_guard', - 'man_guard', - 'detective', - 'woman_detective', - 'man_detective', - 'woman_health_worker', - 'man_health_worker', - 'woman_farmer', - 'man_farmer', - 'woman_cook', - 'man_cook', - 'woman_student', - 'man_student', - 'woman_singer', - 'man_singer', - 'woman_teacher', - 'man_teacher', - 'woman_factory_worker', - 'man_factory_worker', - 'woman_technologist', - 'man_technologist', - 'woman_office_worker', - 'man_office_worker', - 'woman_mechanic', - 'man_mechanic', - 'woman_scientist', - 'man_scientist', - 'woman_artist', - 'man_artist', - 'woman_firefighter', - 'man_firefighter', - 'woman_pilot', - 'man_pilot', - 'woman_astronaut', - 'man_astronaut', - 'woman_judge', - 'man_judge', - 'bride_with_veil', - 'man_in_tuxedo', - 'princess', - 'prince', - 'mrs_claus', - 'santa', - 'superhero', - 'woman_superhero', - 'man_superhero', - 'supervillain', - 'woman_supervillain', - 'man_supervillain', - 'mage', - 'woman_mage', - 'man_mage', - 'elf', - 'woman_elf', - 'man_elf', - 'vampire', - 'woman_vampire', - 'man_vampire', - 'zombie', - 'woman_zombie', - 'man_zombie', - 'genie', - 'woman_genie', - 'man_genie', - 'merperson', - 'mermaid', - 'merman', - 'fairy', - 'woman_fairy', - 'man_fairy', - 'angel', - 'pregnant_woman', - 'breast_feeding', - 'person_bowing', - 'woman_bowing', - 'man_bowing', - 'person_tipping_hand', - 'woman_tipping_hand', - 'man_tipping_hand', - 'person_gesturing_no', - 'woman_gesturing_no', - 'man_gesturing_no', - 'person_gesturing_ok', - 'woman_gesturing_ok', - 'man_gesturing_ok', - 'person_raising_hand', - 'woman_raising_hand', - 'man_raising_hand', - 'person_facepalming', - 'woman_facepalming', - 'man_facepalming', - 'person_shrugging', - 'woman_shrugging', - 'man_shrugging', - 'person_pouting', - 'woman_pouting', - 'man_pouting', - 'person_frowning', - 'woman_frowning', - 'man_frowning', - 'person_getting_haircut', - 'woman_getting_haircut', - 'man_getting_haircut', - 'person_getting_massage', - 'woman_getting_face_massage', - 'man_getting_face_massage', - 'person_in_steamy_room', - 'woman_in_steamy_room', - 'man_in_steamy_room', - 'nail_care', - 'selfie', - 'dancer', - 'man_dancing', - 'people_with_bunny_ears_partying', - 'women_with_bunny_ears_partying', - 'men_with_bunny_ears_partying', - 'levitate', - 'person_walking', - 'woman_walking', - 'man_walking', - 'person_running', - 'woman_running', - 'man_running', - 'couple', - 'two_women_holding_hands', - 'two_men_holding_hands', - 'couple_with_heart', - 'couple_with_heart_woman_man', - 'couple_ww', - 'couple_mm', - 'couplekiss', - 'kiss_woman_man', - 'kiss_ww', - 'kiss_mm', - 'family', - 'family_man_woman_boy', - 'family_mwg', - 'family_mwgb', - 'family_mwbb', - 'family_mwgg', - 'family_wwb', - 'family_wwg', - 'family_wwgb', - 'family_wwbb', - 'family_wwgg', - 'family_mmb', - 'family_mmg', - 'family_mmgb', - 'family_mmbb', - 'family_mmgg', - 'family_woman_boy', - 'family_woman_girl', - 'family_woman_girl_boy', - 'family_woman_boy_boy', - 'family_woman_girl_girl', - 'family_man_boy', - 'family_man_girl', - 'family_man_girl_boy', - 'family_man_boy_boy', - 'family_man_girl_girl', - 'coat', - 'womans_clothes', - 'shirt', - 'jeans', - 'necktie', - 'dress', - 'bikini', - 'kimono', - 'lab_coat', - 'high_heel', - 'sandal', - 'boot', - 'mans_shoe', - 'athletic_shoe', - 'hiking_boot', - 'womans_flat_shoe', - 'socks', - 'gloves', - 'scarf', - 'tophat', - 'billed_cap', - 'womans_hat', - 'mortar_board', - 'helmet_with_cross', - 'crown', - 'pouch', - 'purse', - 'handbag', - 'briefcase', - 'school_satchel', - 'eyeglasses', - 'dark_sunglasses', - 'goggles', - 'closed_umbrella', - 'red_haired', - 'curly_haired', - 'white_haired', - 'bald', - ], - regional: [ - 'regional_indicator_z', - 'regional_indicator_y', - 'regional_indicator_x', - 'regional_indicator_w', - 'regional_indicator_v', - 'regional_indicator_u', - 'regional_indicator_t', - 'regional_indicator_s', - 'regional_indicator_r', - 'regional_indicator_q', - 'regional_indicator_p', - 'regional_indicator_o', - 'regional_indicator_n', - 'regional_indicator_m', - 'regional_indicator_l', - 'regional_indicator_k', - 'regional_indicator_j', - 'regional_indicator_i', - 'regional_indicator_h', - 'regional_indicator_g', - 'regional_indicator_f', - 'regional_indicator_e', - 'regional_indicator_d', - 'regional_indicator_c', - 'regional_indicator_b', - 'regional_indicator_a', - ], - travel: [ - 'red_car', - 'taxi', - 'blue_car', - 'bus', - 'trolleybus', - 'race_car', - 'police_car', - 'ambulance', - 'fire_engine', - 'minibus', - 'truck', - 'articulated_lorry', - 'tractor', - 'scooter', - 'bike', - 'motor_scooter', - 'motorcycle', - 'rotating_light', - 'oncoming_police_car', - 'oncoming_bus', - 'oncoming_automobile', - 'oncoming_taxi', - 'aerial_tramway', - 'mountain_cableway', - 'suspension_railway', - 'railway_car', - 'train', - 'mountain_railway', - 'monorail', - 'bullettrain_side', - 'bullettrain_front', - 'light_rail', - 'steam_locomotive', - 'train2', - 'metro', - 'tram', - 'station', - 'airplane', - 'airplane_departure', - 'airplane_arriving', - 'airplane_small', - 'seat', - 'luggage', - 'satellite_orbital', - 'rocket', - 'flying_saucer', - 'helicopter', - 'canoe', - 'sailboat', - 'speedboat', - 'motorboat', - 'cruise_ship', - 'ferry', - 'ship', - 'anchor', - 'fuelpump', - 'construction', - 'vertical_traffic_light', - 'traffic_light', - 'busstop', - 'map', - 'moyai', - 'statue_of_liberty', - 'tokyo_tower', - 'european_castle', - 'japanese_castle', - 'stadium', - 'ferris_wheel', - 'roller_coaster', - 'carousel_horse', - 'fountain', - 'beach_umbrella', - 'beach', - 'island', - 'desert', - 'volcano', - 'mountain', - 'mountain_snow', - 'mount_fuji', - 'camping', - 'tent', - 'house', - 'house_with_garden', - 'homes', - 'house_abandoned', - 'construction_site', - 'factory', - 'office', - 'department_store', - 'post_office', - 'european_post_office', - 'hospital', - 'bank', - 'hotel', - 'convenience_store', - 'school', - 'love_hotel', - 'wedding', - 'classical_building', - 'church', - 'mosque', - 'synagogue', - 'kaaba', - 'shinto_shrine', - 'railway_track', - 'motorway', - 'japan', - 'rice_scene', - 'park', - 'sunrise', - 'sunrise_over_mountains', - 'stars', - 'sparkler', - 'fireworks', - 'firecracker', - 'city_sunset', - 'city_dusk', - 'cityscape', - 'night_with_stars', - 'milky_way', - 'bridge_at_night', - 'foggy', - ], - flags: [ - 'flag_white', - 'flag_black', - 'checkered_flag', - 'triangular_flag_on_post', - 'rainbow_flag', - 'pirate_flag', - 'flag_af', - 'flag_ax', - 'flag_al', - 'flag_dz', - 'flag_as', - 'flag_ad', - 'flag_ao', - 'flag_ai', - 'flag_aq', - 'flag_ag', - 'flag_ar', - 'flag_am', - 'flag_aw', - 'flag_au', - 'flag_at', - 'flag_az', - 'flag_bs', - 'flag_bh', - 'flag_bd', - 'flag_bb', - 'flag_by', - 'flag_be', - 'flag_bz', - 'flag_bj', - 'flag_bm', - 'flag_bt', - 'flag_bo', - 'flag_ba', - 'flag_bw', - 'flag_br', - 'flag_io', - 'flag_vg', - 'flag_bn', - 'flag_bg', - 'flag_bf', - 'flag_bi', - 'flag_kh', - 'flag_cm', - 'flag_ca', - 'flag_ic', - 'flag_cv', - 'flag_bq', - 'flag_ky', - 'flag_cf', - 'flag_td', - 'flag_cl', - 'flag_cn', - 'flag_cx', - 'flag_cc', - 'flag_co', - 'flag_km', - 'flag_cg', - 'flag_cd', - 'flag_ck', - 'flag_cr', - 'flag_ci', - 'flag_hr', - 'flag_cu', - 'flag_cw', - 'flag_cy', - 'flag_cz', - 'flag_dk', - 'flag_dj', - 'flag_dm', - 'flag_do', - 'flag_ec', - 'flag_eg', - 'flag_sv', - 'flag_gq', - 'flag_er', - 'flag_ee', - 'flag_et', - 'flag_eu', - 'flag_fk', - 'flag_fo', - 'flag_fj', - 'flag_fi', - 'flag_fr', - 'flag_gf', - 'flag_pf', - 'flag_tf', - 'flag_ga', - 'flag_gm', - 'flag_ge', - 'flag_de', - 'flag_gh', - 'flag_gi', - 'flag_gr', - 'flag_gl', - 'flag_gd', - 'flag_gp', - 'flag_gu', - 'flag_gt', - 'flag_gg', - 'flag_gn', - 'flag_gw', - 'flag_gy', - 'flag_ht', - 'flag_hn', - 'flag_hk', - 'flag_hu', - 'flag_is', - 'flag_in', - 'flag_id', - 'flag_ir', - 'flag_iq', - 'flag_ie', - 'flag_im', - 'flag_il', - 'flag_it', - 'flag_jm', - 'flag_jp', - 'crossed_flags', - 'flag_je', - 'flag_jo', - 'flag_kz', - 'flag_ke', - 'flag_ki', - 'flag_xk', - 'flag_kw', - 'flag_kg', - 'flag_la', - 'flag_lv', - 'flag_lb', - 'flag_ls', - 'flag_lr', - 'flag_ly', - 'flag_li', - 'flag_lt', - 'flag_lu', - 'flag_mo', - 'flag_mk', - 'flag_mg', - 'flag_mw', - 'flag_my', - 'flag_mv', - 'flag_ml', - 'flag_mt', - 'flag_mh', - 'flag_mq', - 'flag_mr', - 'flag_mu', - 'flag_yt', - 'flag_mx', - 'flag_fm', - 'flag_md', - 'flag_mc', - 'flag_mn', - 'flag_me', - 'flag_ms', - 'flag_ma', - 'flag_mz', - 'flag_mm', - 'flag_na', - 'flag_nr', - 'flag_np', - 'flag_nl', - 'flag_nc', - 'flag_nz', - 'flag_ni', - 'flag_ne', - 'flag_ng', - 'flag_nu', - 'flag_nf', - 'flag_kp', - 'flag_mp', - 'flag_no', - 'flag_om', - 'flag_pk', - 'flag_pw', - 'flag_ps', - 'flag_pa', - 'flag_pg', - 'flag_py', - 'flag_pe', - 'flag_ph', - 'flag_pn', - 'flag_pl', - 'flag_pt', - 'flag_pr', - 'flag_qa', - 'flag_re', - 'flag_ro', - 'flag_ru', - 'flag_rw', - 'flag_ws', - 'flag_sm', - 'flag_st', - 'flag_sa', - 'flag_sn', - 'flag_rs', - 'flag_sc', - 'flag_sl', - 'flag_sg', - 'flag_sx', - 'flag_sk', - 'flag_si', - 'flag_gs', - 'flag_sb', - 'flag_so', - 'flag_za', - 'flag_kr', - 'flag_ss', - 'flag_es', - 'flag_lk', - 'flag_bl', - 'flag_sh', - 'flag_kn', - 'flag_lc', - 'flag_pm', - 'flag_vc', - 'flag_sd', - 'flag_sr', - 'flag_sz', - 'flag_se', - 'flag_ch', - 'flag_sy', - 'flag_tw', - 'flag_tj', - 'flag_tz', - 'flag_th', - 'flag_tl', - 'flag_tg', - 'flag_tk', - 'flag_to', - 'flag_tt', - 'flag_tn', - 'flag_tr', - 'flag_tm', - 'flag_tc', - 'flag_vi', - 'flag_tv', - 'flag_ug', - 'flag_ua', - 'flag_ae', - 'flag_gb', - 'england', - 'scotland', - 'wales', - 'flag_us', - 'flag_uy', - 'flag_uz', - 'flag_vu', - 'flag_va', - 'flag_ve', - 'flag_vn', - 'flag_wf', - 'flag_eh', - 'flag_ye', - 'flag_zm', - 'flag_zw', - 'flag_ac', - 'flag_bv', - 'flag_cp', - 'flag_ea', - 'flag_dg', - 'flag_hm', - 'flag_mf', - 'flag_sj', - 'flag_ta', - 'flag_um', - 'united_nations', - ], - modifier: ['tone1', 'tone2', 'tone3', 'tone4', 'tone5'], -}; diff --git a/apps/meteor/app/emoji-emojione/lib/emojione.tpl b/apps/meteor/app/emoji-emojione/lib/emojione.tpl deleted file mode 100644 index b7426fb4370fe..0000000000000 --- a/apps/meteor/app/emoji-emojione/lib/emojione.tpl +++ /dev/null @@ -1,30 +0,0 @@ -/* stylelint-disable */ - -.emojione-<%= options.category %> { - background-image: url('<%= options.spritePath %>'); -<% -layout.images.forEach(function (image) { - if(options.diversityList[image.className]){ - return; - } -%> - &._<%= image.className %> { - background-repeat: no-repeat; - background-size: <%= (layout.width / image.width * 100) %>% <%= (layout.height / image.height * 100) %>%; - background-position: <%= (image.x * 100 / (layout.width - image.width )) %>% <%= (image.y * 100 / (layout.height - image.height)) %>%; - } -<% }); %> -} -<% -layout.images.forEach(function (image) { - if(!options.diversityList[image.className]){ - return; - } -%> - -.emojione-diversity._<%= image.className %> { - background-image: url('<%= options.spritePath %>'); - background-repeat: no-repeat; - background-size: <%= (layout.width / image.width * 100) %>% <%= (layout.height / image.height * 100) %>%; - background-position: <%= (image.x * 100 / (layout.width - image.width )) %>% <%= (image.y * 100 / (layout.height - image.height)) %>%; -}<% }); %> diff --git a/apps/meteor/app/emoji-emojione/lib/generateEmojiIndex.mjs b/apps/meteor/app/emoji-emojione/lib/generateEmojiIndex.mjs deleted file mode 100644 index fbed7b93cd9b1..0000000000000 --- a/apps/meteor/app/emoji-emojione/lib/generateEmojiIndex.mjs +++ /dev/null @@ -1,182 +0,0 @@ -/* eslint-disable */ - -// before using this script make sure to run: npm i --no-save node-sprite-generator - -// node --experimental-modules generateEmojiIndex.mjs -import fs from 'node:fs'; -import nsg from 'node-sprite-generator'; -import _ from 'underscore'; -import gm from 'gm'; // lgtm[js/unused-local-variable] - -const assetFolder = '../../../node_modules/emojione-assets'; -const emojiJsonFile = `${assetFolder}/emoji.json`; - -if (!fs.existsSync(emojiJsonFile)) { - console.error(`${emojiJsonFile} doesn't exist.`); - console.error("Maybe you need to run 'meteor npm install emojione-assets' or 'meteor npm install'?"); -} else { - const emojiJson = fs.readFileSync(emojiJsonFile); - generateEmojiPicker(emojiJson); -} - -function generateEmojiPicker(data) { - const emojiList = JSON.parse(data); - console.log(`${Object.keys(emojiList).length} emojis found.`); - - let toneList = []; - let emojisByCategory = {}; - - _.sortBy(Object.entries(emojiList), (a) => a[1].order).forEach(([code, emoji]) => { - if (emoji && emoji.shortname) { - const toneIndex = emoji.shortname.indexOf('_tone'); - if (toneIndex !== -1) { - const tone = emoji.shortname.substr(1, toneIndex - 1); - if (!toneList.includes(tone)) { - toneList.push(tone); - } - return; - } - } - - if (!emojisByCategory[emoji.category]) { - emojisByCategory[emoji.category] = []; - } - emojisByCategory[emoji.category].push(code); - }); - - let output = `/* - * This file is automatically generated from generateEmojiIndex.mjs - * Last generated ${Date().toString()} - * - * Mapping category hashes into human readable and translated names - */\n\n`; - - const emojiCategoriesMapping = [ - { key: 'people', i18n: 'Smileys_and_People' }, - { key: 'nature', i18n: 'Animals_and_Nature' }, - { key: 'food', i18n: 'Food_and_Drink' }, - { key: 'activity', i18n: 'Activity' }, - { key: 'travel', i18n: 'Travel_and_Places' }, - { key: 'objects', i18n: 'Objects' }, - { key: 'symbols', i18n: 'Symbols' }, - { key: 'flags', i18n: 'Flags' }, - ]; - - // emojiCategories - output += `export const emojiCategories = [\n`; - for (let category in emojisByCategory) { - const map = emojiCategoriesMapping.find((o) => o.key === category); - if (map) { - output += `\t{ key: '${category}', i18n: '${map.i18n}' },\n`; - } else { - if (category !== 'modifier' || category !== 'regional') { - console.error(`No emojiCategory mapping for ${category}`); - } - } - } - output += `];\n`; - - // toneList - const needsQuotes = ['-']; - output += `export const toneList = {\n`; - for (let tone in toneList) { - if (toneList[tone].includes(needsQuotes)) { - output += `\t'${toneList[tone]}': 1,\n`; - } else { - output += `\t${toneList[tone]}: 1,\n`; - } - } - output += `};\n`; - - // emojisByCategory - output += `export const emojisByCategory = {\n`; - for (let category in emojisByCategory) { - output += `\t${category}: [\n`; - - for (let emoji in emojisByCategory[category]) { - output += `\t\t'${emojiList[emojisByCategory[category][emoji]].shortname.replace(/:/g, '')}',\n`; - } - - output += `\t],\n`; - } - output += `};\n`; - - fs.writeFileSync('emojiPicker.js', output, { - encoding: 'utf8', - flag: 'w', - }); - console.log('Generated emojiPicker.js!'); - - console.log('Generating sprite sheets....'); - - let spriteCss = ''; - - for (let category in emojisByCategory) { - let srcList = []; - let diversityList = []; - const emojis = _.filter(emojiList, (x) => x.category === category); - const spritePath = `../../../public/packages/emojione/${category}-sprites.png`; - - _.each(emojis, function (emoji) { - srcList.push(`${assetFolder}/png/64/${emoji.code_points.base}.png`); - if (emoji.diversity) { - diversityList[emoji.code_points.base] = true; - } - }); - spriteCss += `@import './${category}-sprites.css';\n`; - - nsg( - { - src: srcList, - spritePath: spritePath, - layout: 'packed', - stylesheet: 'emojione.tpl', - stylesheetPath: `../client/${category}-sprites.css`, - compositor: 'gm', - layoutOptions: { - scaling: 1, - }, - stylesheetOptions: { - prefix: '', - diversityList: diversityList, - category: category, - spritePath: `/packages/emojione/${category}-sprites.png`, - pixelRatio: 1, - }, - }, - function (err) { - if (err) { - console.error(err); - return; - } - console.log(`${category}'s sprite generated!`); - }, - ); - } - - spriteCss += ` -.emojione { - position: relative; - - display: inline-block; - overflow: hidden; - - width: 1.375rem; - height: 1.375rem; - margin: 0 0.15em; - - vertical-align: middle; - white-space: nowrap; - text-indent: 100%; - - font-size: inherit; - line-height: normal; - image-rendering: -webkit-optimize-contrast; - image-rendering: optimizeQuality; -} -`; - fs.writeFileSync('../client/emojione-sprites.css', spriteCss, { - encoding: 'utf8', - flag: 'w', - }); -} diff --git a/apps/meteor/app/emoji-emojione/lib/getEmojiConfig.ts b/apps/meteor/app/emoji-emojione/lib/getEmojiConfig.ts deleted file mode 100644 index 51d2fbc1c4efc..0000000000000 --- a/apps/meteor/app/emoji-emojione/lib/getEmojiConfig.ts +++ /dev/null @@ -1,281 +0,0 @@ -import emojione from 'emojione'; -import mem from 'mem'; - -import { emojisByCategory, emojiCategories, toneList } from './emojiPicker'; - -// TODO remove fix below when issue is solved: https://github.com/joypixels/emojione/issues/617 - -// add missing emojis not provided by JS object, but included on emoji.json -emojione.shortnames += - '|:tm:|:copyright:|:registered:|:digit_zero:|:digit_one:|:digit_two:|:digit_three:|:digit_four:|:digit_five:|:digit_six:|:digit_seven:|:digit_eight:|:digit_nine:|:pound_symbol:|:asterisk_symbol:'; -emojione.regShortNames = new RegExp( - `]*>.*?<\/object>|]*>.*?<\/span>|<(?:object|embed|svg|img|div|span|p|a)[^>]*>|(${emojione.shortnames})`, - 'gi', -); - -emojione.emojioneList[':tm:'] = { - uc_base: '2122', - uc_output: '2122-fe0f', - uc_match: '2122-fe0f', - uc_greedy: '2122-fe0f', - shortnames: [], - category: 'symbols', - emojiPackage: 'emojione', -}; - -emojione.emojioneList[':copyright:'] = { - uc_base: '00a9', - uc_output: '00a9-f0ef', - uc_match: '00a9-fe0f', - uc_greedy: '00a9-fe0f', - shortnames: [], - category: 'symbols', - emojiPackage: 'emojione', -}; - -emojione.emojioneList[':registered:'] = { - uc_base: '00ae', - uc_output: '00ae-fe0f', - uc_match: '00ae-fe0f', - uc_greedy: '00ae-fe0f', - shortnames: [], - category: 'symbols', - emojiPackage: 'emojione', -}; - -emojione.emojioneList[':digit_zero:'] = { - uc_base: '0030', - uc_output: '0030-fe0f', - uc_match: '0030-fe0f', - uc_greedy: '0030-fe0f', - shortnames: [], - category: 'symbols', - emojiPackage: 'emojione', -}; - -emojione.emojioneList[':digit_one:'] = { - uc_base: '0031', - uc_output: '0031-fe0f', - uc_match: '0031-fe0f', - uc_greedy: '0031-fe0f', - shortnames: [], - category: 'symbols', - emojiPackage: 'emojione', -}; - -emojione.emojioneList[':digit_two:'] = { - uc_base: '0032', - uc_output: '0032-fe0f', - uc_match: '0032-fe0f', - uc_greedy: '0032-fe0f', - shortnames: [], - category: 'symbols', - emojiPackage: 'emojione', -}; - -emojione.emojioneList[':digit_three:'] = { - uc_base: '0033', - uc_output: '0033-fe0f', - uc_match: '0033-fe0f', - uc_greedy: '0033-fe0f', - shortnames: [], - category: 'symbols', - emojiPackage: 'emojione', -}; - -emojione.emojioneList[':digit_four:'] = { - uc_base: '0034', - uc_output: '0034-fe0f', - uc_match: '0034-fe0f', - uc_greedy: '0034-fe0f', - shortnames: [], - category: 'symbols', - emojiPackage: 'emojione', -}; - -emojione.emojioneList[':digit_five:'] = { - uc_base: '0035', - uc_output: '0035-fe0f', - uc_match: '0035-fe0f', - uc_greedy: '0035-fe0f', - shortnames: [], - category: 'symbols', - emojiPackage: 'emojione', -}; - -emojione.emojioneList[':digit_six:'] = { - uc_base: '0036', - uc_output: '0036-fe0f', - uc_match: '0036-fe0f', - uc_greedy: '0036-fe0f', - shortnames: [], - category: 'symbols', - emojiPackage: 'emojione', -}; - -emojione.emojioneList[':digit_seven:'] = { - uc_base: '0037', - uc_output: '0037-fe0f', - uc_match: '0037-fe0f', - uc_greedy: '0037-fe0f', - shortnames: [], - category: 'symbols', - emojiPackage: 'emojione', -}; - -emojione.emojioneList[':digit_eight:'] = { - uc_base: '0038', - uc_output: '0038-fe0f', - uc_match: '0038-fe0f', - uc_greedy: '0038-fe0f', - shortnames: [], - category: 'symbols', - emojiPackage: 'emojione', -}; - -emojione.emojioneList[':digit_nine:'] = { - uc_base: '0039', - uc_output: '0039-fe0f', - uc_match: '0039-fe0f', - uc_greedy: '0039-fe0f', - shortnames: [], - category: 'symbols', - emojiPackage: 'emojione', -}; - -emojione.emojioneList[':pound_symbol:'] = { - uc_base: '0023', - uc_output: '0023-fe0f', - uc_match: '0023-fe0f', - uc_greedy: '0023-fe0f', - shortnames: [], - category: 'symbols', - emojiPackage: 'emojione', -}; - -emojione.emojioneList[':asterisk_symbol:'] = { - uc_base: '002a', - uc_output: '002a-fe0f', - uc_match: '002a-fe0f', - uc_greedy: '002a-fe0f', - shortnames: [], - category: 'symbols', - emojiPackage: 'emojione', -}; -// end fix - -// fix for :+1: - had to replace all function that does its conversion: https://github.com/joypixels/emojione/blob/4.5.0/lib/js/emojione.js#L249 - -emojione.shortnameConversionMap = mem(emojione.shortnameConversionMap, { maxAge: 1000 }); - -emojione.unicodeCharRegex = mem(emojione.unicodeCharRegex, { maxAge: 1000 }); - -const convertShortName = mem( - (shortname) => { - // the fix is basically adding this .replace(/[+]/g, '\\$&') - if (typeof shortname === 'undefined' || shortname === '' || emojione.shortnames.indexOf(shortname.replace(/[+]/g, '\\$&')) === -1) { - // if the shortname doesnt exist just return the entire match - return shortname; - } - - // map shortname to parent - if (!emojione.emojioneList[shortname]) { - for (const emoji in emojione.emojioneList) { - if (!emojione.emojioneList.hasOwnProperty(emoji) || emoji === '') { - continue; - } - if (emojione.emojioneList[emoji].shortnames.indexOf(shortname) === -1) { - continue; - } - shortname = emoji; - break; - } - } - - const unicode = emojione.emojioneList[shortname].uc_output; - const fname = emojione.emojioneList[shortname].uc_base; - const category = fname.indexOf('-1f3f') >= 0 ? 'diversity' : emojione.emojioneList[shortname].category; - const title = emojione.imageTitleTag ? `title="${shortname}"` : ''; - // const size = ns.spriteSize === '32' || ns.spriteSize === '64' ? ns.spriteSize : '32'; - // if the emoji path has been set, we'll use the provided path, otherwise we'll use the default path - const ePath = - emojione.defaultPathPNG !== emojione.imagePathPNG ? emojione.imagePathPNG : `${emojione.defaultPathPNG + emojione.emojiSize}/`; - - // depending on the settings, we'll either add the native unicode as the alt tag, otherwise the shortname - const alt = emojione.unicodeAlt ? emojione.convert(unicode.toUpperCase()) : shortname; - - if (emojione.sprites) { - return `${alt}`; - } - return `${alt}`; - }, - { maxAge: 1000 }, -); - -const convertUnicode = mem( - (entire, _m1, m2, m3) => { - const mappedUnicode = emojione.mapUnicodeToShort(); - - if (typeof m3 === 'undefined' || m3 === '' || !(emojione.unescapeHTML(m3) in emojione.asciiList)) { - // if the ascii doesnt exist just return the entire match - return entire; - } - - m3 = emojione.unescapeHTML(m3); - const unicode = emojione.asciiList[m3]; - const shortname = mappedUnicode[unicode]; - const category = unicode.indexOf('-1f3f') >= 0 ? 'diversity' : emojione.emojioneList[shortname].category; - const title = emojione.imageTitleTag ? `title="${emojione.escapeHTML(m3)}"` : ''; - // const size = ns.spriteSize === '32' || ns.spriteSize === '64' ? ns.spriteSize : '32'; - // if the emoji path has been set, we'll use the provided path, otherwise we'll use the default path - const ePath = - emojione.defaultPathPNG !== emojione.imagePathPNG ? emojione.imagePathPNG : `${emojione.defaultPathPNG + emojione.emojiSize}/`; - - // depending on the settings, we'll either add the native unicode as the alt tag, otherwise the shortname - const alt = emojione.unicodeAlt ? emojione.convert(unicode.toUpperCase()) : emojione.escapeHTML(m3); - - if (emojione.sprites) { - return `${m2}${alt}`; - } - return `${m2}${alt}`; - }, - { maxAge: 1000, cacheKey: JSON.stringify }, -); - -emojione.shortnameToImage = (str) => { - // replace regular shortnames first - str = str.replace(emojione.regShortNames, convertShortName); - - // if ascii smileys are turned on, then we'll replace them! - if (emojione.ascii) { - const asciiRX = emojione.riskyMatchAscii ? emojione.regAsciiRisky : emojione.regAscii; - - return str.replace(asciiRX, convertUnicode); - } - - return str; -}; - -const isEmojiSupported = (str: string) => { - str = str.replace(emojione.regShortNames, convertShortName); - - // if ascii smileys are turned on, then we'll replace them! - if (emojione.ascii) { - const asciiRX = emojione.riskyMatchAscii ? emojione.regAsciiRisky : emojione.regAscii; - - return str.replace(asciiRX, convertUnicode); - } - - return str; -}; - -export const getEmojiConfig = () => ({ - emojione, - emojisByCategory, - emojiCategories, - toneList, - render: emojione.toImage, - renderPicker: emojione.shortnameToImage, - sprites: true, - isEmojiSupported, -}); diff --git a/apps/meteor/app/emoji-emojione/lib/isSetNotNull.ts b/apps/meteor/app/emoji-emojione/lib/isSetNotNull.ts deleted file mode 100644 index 000be1279e684..0000000000000 --- a/apps/meteor/app/emoji-emojione/lib/isSetNotNull.ts +++ /dev/null @@ -1,10 +0,0 @@ -// http://stackoverflow.com/a/26990347 function isSet() from Gajus -export const isSetNotNull = async (fn: () => unknown) => { - let value; - try { - value = await fn(); - } catch (e) { - value = null; - } - return value !== null && value !== undefined; -}; diff --git a/apps/meteor/app/emoji-emojione/server/callbacks.ts b/apps/meteor/app/emoji-emojione/server/callbacks.ts deleted file mode 100644 index 081e115a152a5..0000000000000 --- a/apps/meteor/app/emoji-emojione/server/callbacks.ts +++ /dev/null @@ -1,13 +0,0 @@ -import emojione from 'emojione'; -import { Meteor } from 'meteor/meteor'; - -import { callbacks } from '../../../server/lib/callbacks'; - -Meteor.startup(() => { - callbacks.add( - 'beforeSendMessageNotifications', - (message) => emojione.shortnameToUnicode(message), - callbacks.priority.MEDIUM, - 'emojione-shortnameToUnicode', - ); -}); diff --git a/apps/meteor/app/emoji-emojione/server/index.ts b/apps/meteor/app/emoji-emojione/server/index.ts deleted file mode 100644 index 723d879892f7d..0000000000000 --- a/apps/meteor/app/emoji-emojione/server/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './lib'; -import './callbacks'; diff --git a/apps/meteor/app/emoji-emojione/server/lib.ts b/apps/meteor/app/emoji-emojione/server/lib.ts deleted file mode 100644 index 57d366529526a..0000000000000 --- a/apps/meteor/app/emoji-emojione/server/lib.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { emoji } from '../../emoji/server'; -import { getUserPreference } from '../../utils/server/lib/getUserPreference'; -import { getEmojiConfig } from '../lib/getEmojiConfig'; -import { isSetNotNull } from '../lib/isSetNotNull'; - -const config = getEmojiConfig(); - -emoji.packages.emojione = config.emojione as any; -if (emoji.packages.emojione) { - emoji.packages.emojione.sprites = config.sprites; - emoji.packages.emojione.emojisByCategory = config.emojisByCategory; - emoji.packages.emojione.emojiCategories = config.emojiCategories as typeof emoji.packages.emojione.emojiCategories; - emoji.packages.emojione.toneList = config.toneList; - - emoji.packages.emojione.render = config.render; - emoji.packages.emojione.renderPicker = config.renderPicker; - // TODO: check types - // RocketChat.emoji.list is the collection of emojis from all emoji packages - for (const key in config.emojione.emojioneList) { - if (config.emojione.emojioneList.hasOwnProperty(key)) { - const currentEmoji = config.emojione.emojioneList[key]; - currentEmoji.emojiPackage = 'emojione'; - emoji.list[key] = currentEmoji; - - if (currentEmoji.shortnames) { - currentEmoji.shortnames.forEach((shortname: string) => { - emoji.list[shortname] = currentEmoji; - }); - } - } - } - - // Additional settings -- ascii emojis - Meteor.startup(async () => { - if ((await isSetNotNull(() => emoji.packages.emojione)) && emoji.packages.emojione) { - if (await isSetNotNull(() => getUserPreference(Meteor.userId() as string, 'convertAsciiEmoji'))) { - emoji.packages.emojione.ascii = await getUserPreference(Meteor.userId() as string, 'convertAsciiEmoji'); - } else { - emoji.packages.emojione.ascii = true; - } - } - }); -} diff --git a/apps/meteor/app/emoji-native/lib/generateEmojiData.ts b/apps/meteor/app/emoji-native/lib/generateEmojiData.ts new file mode 100644 index 0000000000000..9fc5a5bc0f46c --- /dev/null +++ b/apps/meteor/app/emoji-native/lib/generateEmojiData.ts @@ -0,0 +1,141 @@ +import type { Emoji } from 'emojibase'; +import data from 'emojibase-data/en/data.json'; +import shortcodes from 'emojibase-data/en/shortcodes/emojibase.json'; + +// Map emojibase group numbers to our category keys +const groupToCategory: Record = { + 0: 'people', // smileys & emotion + 1: 'people', // people & body + 2: 'people', // component - skin tones etc (we skip these) + 3: 'nature', // animals & nature + 4: 'food', // food & drink + 5: 'travel', // travel & places + 6: 'activity', // activities + 7: 'objects', // objects + 8: 'symbols', // symbols + 9: 'flags', // flags +}; + +export type EmojiEntry = { + uc_base: string; + uc_output: string; + uc_match: string; + uc_greedy: string; + shortnames: string[]; + category: string; + emojiPackage: string; + unicode: string; +}; + +function hexFromEmoji(emoji: string): string { + return [...emoji].map((cp) => cp.codePointAt(0)!.toString(16)).join('-'); +} + +function isRegionalIndicator(hexcode: string): boolean { + if (hexcode.includes('-')) return false; + const cp = parseInt(hexcode, 16); + return cp >= 0x1f1e6 && cp <= 0x1f1ff; +} + +function getShortcodes(hexcode: string): string[] { + const entry = (shortcodes as Record)[hexcode]; + if (!entry) return []; + return Array.isArray(entry) ? entry : [entry]; +} + +function buildEmojiData() { + const emojiList: Record = {}; + const emojisByCategory: Record = { + people: [], + nature: [], + food: [], + activity: [], + travel: [], + objects: [], + symbols: [], + flags: [], + }; + const toneList: Record = {}; + + for (const emojiData of data as Emoji[]) { + // Skip component group (skin tones, hair styles) + if (emojiData.group === 2) continue; + + const isRegional = isRegionalIndicator(emojiData.hexcode); + const category = groupToCategory[emojiData.group ?? -1] ?? (isRegional ? 'flags' : undefined); + if (!category) continue; + + const codes = getShortcodes(emojiData.hexcode); + if (codes.length === 0) continue; + + const primaryShortcode = codes[0]; + const altShortcodes = codes.slice(1).map((s) => `:${s}:`); + const hex = hexFromEmoji(emojiData.emoji); + + const entry: EmojiEntry = { + uc_base: hex, + uc_output: hex, + uc_match: hex, + uc_greedy: hex, + shortnames: altShortcodes, + category, + emojiPackage: 'native', + unicode: emojiData.emoji, + }; + + const key = `:${primaryShortcode}:`; + emojiList[key] = entry; + + // Only add to category if it's NOT a skin tone variant + if (!emojiData.tone && !isRegional) { + emojisByCategory[category].push(primaryShortcode); + } + + // Register alternate shortcodes + for (const alt of altShortcodes) { + emojiList[alt] = entry; + } + + // Track skin tone support + if (emojiData.skins && emojiData.skins.length > 0) { + toneList[primaryShortcode] = 1; + + // Register skin tone variants + for (const skin of emojiData.skins) { + if (!skin.tone) continue; + + const tones = Array.isArray(skin.tone) ? skin.tone : [skin.tone]; + const skinHex = hexFromEmoji(skin.emoji); + + const skinEntry: EmojiEntry = { + uc_base: skinHex, + uc_output: skinHex, + uc_match: skinHex, + uc_greedy: skinHex, + shortnames: [], + category, + emojiPackage: 'native', + unicode: skin.emoji, + }; + + const toneShortcodes = getShortcodes(skin.hexcode); + const fallbackShortcode = `${primaryShortcode}_tone${tones.join('-')}`; + for (const code of [...toneShortcodes, fallbackShortcode]) { + emojiList[`:${code}:`] = skinEntry; + } + } + } + } + + return { emojiList, emojisByCategory, toneList }; +} + +// Build once and cache +let _cached: ReturnType | null = null; + +export function getEmojiData() { + if (!_cached) { + _cached = buildEmojiData(); + } + return _cached; +} diff --git a/apps/meteor/app/emoji-native/lib/getEmojiConfig.ts b/apps/meteor/app/emoji-native/lib/getEmojiConfig.ts new file mode 100644 index 0000000000000..d54e1b88e565d --- /dev/null +++ b/apps/meteor/app/emoji-native/lib/getEmojiConfig.ts @@ -0,0 +1,108 @@ +import type { EmojiEntry } from './generateEmojiData'; +import { getEmojiData } from './generateEmojiData'; +import { legacyEmojioneMap } from './legacyEmojioneMap'; +import { shortnameToUnicode } from './shortnameToUnicode'; +import type { EmojiPackages } from '../../emoji/lib/rocketchat'; + +const emojiCategories = [ + { key: 'people', i18n: 'Smileys_and_People' }, + { key: 'nature', i18n: 'Animals_and_Nature' }, + { key: 'food', i18n: 'Food_and_Drink' }, + { key: 'activity', i18n: 'Activity' }, + { key: 'travel', i18n: 'Travel_and_Places' }, + { key: 'objects', i18n: 'Objects' }, + { key: 'symbols', i18n: 'Symbols' }, + { key: 'flags', i18n: 'Flags' }, +]; + +// Build a reverse mapping from unicode emoji to shortcode for detection +let unicodeToShortcodeMap: Map | null = null; +let emojiRegex: RegExp | null = null; + +function getUnicodeToShortcodeMap(): Map { + if (!unicodeToShortcodeMap) { + const { emojiList } = getEmojiData(); + unicodeToShortcodeMap = new Map(); + + for (const [shortcode, entry] of Object.entries(emojiList)) { + const emojiEntry = entry; + if (emojiEntry.unicode && !unicodeToShortcodeMap.has(emojiEntry.unicode)) { + unicodeToShortcodeMap.set(emojiEntry.unicode, shortcode); + } + } + } + return unicodeToShortcodeMap; +} + +function getEmojiRegex(): RegExp { + if (!emojiRegex) { + const unicodeMap = getUnicodeToShortcodeMap(); + // Sort emojis by length (longest first) to match multi-codepoint emojis correctly + const unicodeEmojis = [...unicodeMap.keys()].sort((a, b) => b.length - a.length); + const unicodePattern = unicodeEmojis.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'); + // Combine shortcode and unicode emoji patterns + emojiRegex = new RegExp(`(:([a-zA-Z0-9_+-]+):)|(${unicodePattern})`, 'g'); + } + return emojiRegex; +} + +function renderEmoji(text: string, emojiPackages: EmojiPackages): string { + const { emojiList } = getEmojiData(); + const unicodeMap = getUnicodeToShortcodeMap(); + const pattern = getEmojiRegex(); + + return text.replace(pattern, (match, shortcodeGroup, shortcodeName, unicodeGroup) => { + // If it's a shortcode pattern (:emoji:) + if (shortcodeGroup) { + if (emojiPackages.list[`:${shortcodeName}:`]?.emojiPackage === 'emojiCustom') { + return match; // Don't render custom emojis as native + } + const key = `:${shortcodeName}:`; + const emoji = emojiList[key] as EmojiEntry | undefined; + if (emoji?.unicode) { + return `${emoji.unicode}`; + } + + // Fallback to legacy emojione shortcodes for backward compatibility + const legacy = legacyEmojioneMap[shortcodeName]; + if (legacy) { + return `${legacy}`; + } + + return match; + } + + // If it's a unicode emoji character (unicodeGroup will be defined) + if (unicodeGroup) { + const shortcode = unicodeMap.get(unicodeGroup); + if (shortcode) { + return `${unicodeGroup}`; + } + } + + return match; + }); +} + +function renderPicker(emojiToRender: string): string | undefined { + const { emojiList } = getEmojiData(); + const emoji = emojiList[emojiToRender] as EmojiEntry | undefined; + if (!emoji?.unicode) return undefined; + + return `${emoji.unicode}`; +} + +export const getEmojiConfig = (emojiPackages: EmojiPackages) => { + const { emojiList, emojisByCategory, toneList } = getEmojiData(); + + return { + emojiList, + emojisByCategory, + emojiCategories, + toneList, + render: (text: string) => renderEmoji(text, emojiPackages), + renderPicker, + sprites: false, + shortnameToUnicode, + }; +}; diff --git a/apps/meteor/app/emoji-native/lib/legacyEmojioneMap.ts b/apps/meteor/app/emoji-native/lib/legacyEmojioneMap.ts new file mode 100644 index 0000000000000..f4f27d3cfd4f3 --- /dev/null +++ b/apps/meteor/app/emoji-native/lib/legacyEmojioneMap.ts @@ -0,0 +1,1427 @@ +// Auto-generated legacy emojione shortcode mapping +// These shortcodes existed in emojione but are not in emojibase. +// They are used to ensure backward compatibility with old stored messages. + +export const legacyEmojioneMap: Record = { + 'ac': '๐Ÿ‡ฆ๐Ÿ‡จ', + 'ad': '๐Ÿ‡ฆ๐Ÿ‡ฉ', + 'adult_dark_skin_tone': '๐Ÿง‘๐Ÿฟ', + 'adult_light_skin_tone': '๐Ÿง‘๐Ÿป', + 'adult_medium_dark_skin_tone': '๐Ÿง‘๐Ÿพ', + 'adult_medium_light_skin_tone': '๐Ÿง‘๐Ÿผ', + 'adult_medium_skin_tone': '๐Ÿง‘๐Ÿฝ', + 'ae': '๐Ÿ‡ฆ๐Ÿ‡ช', + 'af': '๐Ÿ‡ฆ๐Ÿ‡ซ', + 'ag': '๐Ÿ‡ฆ๐Ÿ‡ฌ', + 'ai': '๐Ÿ‡ฆ๐Ÿ‡ฎ', + 'airplane_small': '๐Ÿ›ฉ', + 'al': '๐Ÿ‡ฆ๐Ÿ‡ฑ', + 'am': '๐Ÿ‡ฆ๐Ÿ‡ฒ', + 'anger_right': '๐Ÿ—ฏ', + 'ao': '๐Ÿ‡ฆ๐Ÿ‡ด', + 'aq': '๐Ÿ‡ฆ๐Ÿ‡ถ', + 'ar': '๐Ÿ‡ฆ๐Ÿ‡ท', + 'archery': '๐Ÿน', + 'as': '๐Ÿ‡ฆ๐Ÿ‡ธ', + 'at': '๐Ÿ‡ฆ๐Ÿ‡น', + 'au': '๐Ÿ‡ฆ๐Ÿ‡บ', + 'aw': '๐Ÿ‡ฆ๐Ÿ‡ผ', + 'ax': '๐Ÿ‡ฆ๐Ÿ‡ฝ', + 'az': '๐Ÿ‡ฆ๐Ÿ‡ฟ', + 'ba': '๐Ÿ‡ง๐Ÿ‡ฆ', + 'back_of_hand_tone1': '๐Ÿคš๐Ÿป', + 'back_of_hand_tone2': '๐Ÿคš๐Ÿผ', + 'back_of_hand_tone3': '๐Ÿคš๐Ÿฝ', + 'back_of_hand_tone4': '๐Ÿคš๐Ÿพ', + 'back_of_hand_tone5': '๐Ÿคš๐Ÿฟ', + 'back_of_hand': '๐Ÿคš', + 'ballot_box_with_ballot': '๐Ÿ—ณ', + 'basketball_player_tone1': 'โ›น๐Ÿป', + 'basketball_player_tone2': 'โ›น๐Ÿผ', + 'basketball_player_tone3': 'โ›น๐Ÿฝ', + 'basketball_player_tone4': 'โ›น๐Ÿพ', + 'basketball_player_tone5': 'โ›น๐Ÿฟ', + 'basketball_player': 'โ›น', + 'bb': '๐Ÿ‡ง๐Ÿ‡ง', + 'bd': '๐Ÿ‡ง๐Ÿ‡ฉ', + 'be': '๐Ÿ‡ง๐Ÿ‡ช', + 'bearded_person_dark_skin_tone': '๐Ÿง”๐Ÿฟ', + 'bearded_person_light_skin_tone': '๐Ÿง”๐Ÿป', + 'bearded_person_medium_dark_skin_tone': '๐Ÿง”๐Ÿพ', + 'bearded_person_medium_light_skin_tone': '๐Ÿง”๐Ÿผ', + 'bearded_person_medium_skin_tone': '๐Ÿง”๐Ÿฝ', + 'bearded_person_tone1': '๐Ÿง”๐Ÿป', + 'bearded_person_tone2': '๐Ÿง”๐Ÿผ', + 'bearded_person_tone3': '๐Ÿง”๐Ÿฝ', + 'bearded_person_tone4': '๐Ÿง”๐Ÿพ', + 'bearded_person_tone5': '๐Ÿง”๐Ÿฟ', + 'bearded_person': '๐Ÿง”', + 'bellhop_bell': '๐Ÿ›Ž', + 'bf': '๐Ÿ‡ง๐Ÿ‡ซ', + 'bg': '๐Ÿ‡ง๐Ÿ‡ฌ', + 'bh': '๐Ÿ‡ง๐Ÿ‡ญ', + 'bi': '๐Ÿ‡ง๐Ÿ‡ฎ', + 'biohazard_sign': 'โ˜ฃ', + 'bj': '๐Ÿ‡ง๐Ÿ‡ฏ', + 'bl': '๐Ÿ‡ง๐Ÿ‡ฑ', + 'blond_haired_person_tone1': '๐Ÿ‘ฑ๐Ÿป', + 'blond_haired_person_tone2': '๐Ÿ‘ฑ๐Ÿผ', + 'blond_haired_person_tone3': '๐Ÿ‘ฑ๐Ÿฝ', + 'blond_haired_person_tone4': '๐Ÿ‘ฑ๐Ÿพ', + 'blond_haired_person_tone5': '๐Ÿ‘ฑ๐Ÿฟ', + 'blond_haired_person': '๐Ÿ‘ฑ', + 'blond-haired_man_dark_skin_tone': '๐Ÿ‘ฑ๐Ÿฟโ€โ™‚๏ธ', + 'blond-haired_man_light_skin_tone': '๐Ÿ‘ฑ๐Ÿปโ€โ™‚๏ธ', + 'blond-haired_man_medium_dark_skin_tone': '๐Ÿ‘ฑ๐Ÿพโ€โ™‚๏ธ', + 'blond-haired_man_medium_light_skin_tone': '๐Ÿ‘ฑ๐Ÿผโ€โ™‚๏ธ', + 'blond-haired_man_medium_skin_tone': '๐Ÿ‘ฑ๐Ÿฝโ€โ™‚๏ธ', + 'blond-haired_man_tone1': '๐Ÿ‘ฑ๐Ÿปโ€โ™‚๏ธ', + 'blond-haired_man_tone2': '๐Ÿ‘ฑ๐Ÿผโ€โ™‚๏ธ', + 'blond-haired_man_tone3': '๐Ÿ‘ฑ๐Ÿฝโ€โ™‚๏ธ', + 'blond-haired_man_tone4': '๐Ÿ‘ฑ๐Ÿพโ€โ™‚๏ธ', + 'blond-haired_man_tone5': '๐Ÿ‘ฑ๐Ÿฟโ€โ™‚๏ธ', + 'blond-haired_man': '๐Ÿ‘ฑโ€โ™‚๏ธ', + 'blond-haired_woman_dark_skin_tone': '๐Ÿ‘ฑ๐Ÿฟโ€โ™€๏ธ', + 'blond-haired_woman_light_skin_tone': '๐Ÿ‘ฑ๐Ÿปโ€โ™€๏ธ', + 'blond-haired_woman_medium_dark_skin_tone': '๐Ÿ‘ฑ๐Ÿพโ€โ™€๏ธ', + 'blond-haired_woman_medium_light_skin_tone': '๐Ÿ‘ฑ๐Ÿผโ€โ™€๏ธ', + 'blond-haired_woman_medium_skin_tone': '๐Ÿ‘ฑ๐Ÿฝโ€โ™€๏ธ', + 'blond-haired_woman_tone1': '๐Ÿ‘ฑ๐Ÿปโ€โ™€๏ธ', + 'blond-haired_woman_tone2': '๐Ÿ‘ฑ๐Ÿผโ€โ™€๏ธ', + 'blond-haired_woman_tone3': '๐Ÿ‘ฑ๐Ÿฝโ€โ™€๏ธ', + 'blond-haired_woman_tone4': '๐Ÿ‘ฑ๐Ÿพโ€โ™€๏ธ', + 'blond-haired_woman_tone5': '๐Ÿ‘ฑ๐Ÿฟโ€โ™€๏ธ', + 'blond-haired_woman': '๐Ÿ‘ฑโ€โ™€๏ธ', + 'bm': '๐Ÿ‡ง๐Ÿ‡ฒ', + 'bn': '๐Ÿ‡ง๐Ÿ‡ณ', + 'bo': '๐Ÿ‡ง๐Ÿ‡ด', + 'bottle_with_popping_cork': '๐Ÿพ', + 'boxing_gloves': '๐ŸฅŠ', + 'bq': '๐Ÿ‡ง๐Ÿ‡ถ', + 'br': '๐Ÿ‡ง๐Ÿ‡ท', + 'breast_feeding_dark_skin_tone': '๐Ÿคฑ๐Ÿฟ', + 'breast_feeding_light_skin_tone': '๐Ÿคฑ๐Ÿป', + 'breast_feeding_medium_dark_skin_tone': '๐Ÿคฑ๐Ÿพ', + 'breast_feeding_medium_light_skin_tone': '๐Ÿคฑ๐Ÿผ', + 'breast_feeding_medium_skin_tone': '๐Ÿคฑ๐Ÿฝ', + 'bride_with_veil_tone1': '๐Ÿ‘ฐ๐Ÿป', + 'bride_with_veil_tone2': '๐Ÿ‘ฐ๐Ÿผ', + 'bride_with_veil_tone3': '๐Ÿ‘ฐ๐Ÿฝ', + 'bride_with_veil_tone4': '๐Ÿ‘ฐ๐Ÿพ', + 'bride_with_veil_tone5': '๐Ÿ‘ฐ๐Ÿฟ', + 'bride_with_veil': '๐Ÿ‘ฐ', + 'bs': '๐Ÿ‡ง๐Ÿ‡ธ', + 'bt': '๐Ÿ‡ง๐Ÿ‡น', + 'bv': '๐Ÿ‡ง๐Ÿ‡ป', + 'bw': '๐Ÿ‡ง๐Ÿ‡ผ', + 'by': '๐Ÿ‡ง๐Ÿ‡พ', + 'bz': '๐Ÿ‡ง๐Ÿ‡ฟ', + 'ca': '๐Ÿ‡จ๐Ÿ‡ฆ', + 'call_me_tone1': '๐Ÿค™๐Ÿป', + 'call_me_tone2': '๐Ÿค™๐Ÿผ', + 'call_me_tone3': '๐Ÿค™๐Ÿฝ', + 'call_me_tone4': '๐Ÿค™๐Ÿพ', + 'call_me_tone5': '๐Ÿค™๐Ÿฟ', + 'call_me': '๐Ÿค™', + 'card_box': '๐Ÿ—ƒ', + 'cartwheel_tone1': '๐Ÿคธ๐Ÿป', + 'cartwheel_tone2': '๐Ÿคธ๐Ÿผ', + 'cartwheel_tone3': '๐Ÿคธ๐Ÿฝ', + 'cartwheel_tone4': '๐Ÿคธ๐Ÿพ', + 'cartwheel_tone5': '๐Ÿคธ๐Ÿฟ', + 'cartwheel': '๐Ÿคธ', + 'cat2': '๐Ÿˆ', + 'cc': '๐Ÿ‡จ๐Ÿ‡จ', + 'cf': '๐Ÿ‡จ๐Ÿ‡ซ', + 'cg': '๐Ÿ‡จ๐Ÿ‡ฌ', + 'ch': '๐Ÿ‡จ๐Ÿ‡ญ', + 'champagne_glass': '๐Ÿฅ‚', + 'cheese_wedge': '๐Ÿง€', + 'child_dark_skin_tone': '๐Ÿง’๐Ÿฟ', + 'child_light_skin_tone': '๐Ÿง’๐Ÿป', + 'child_medium_dark_skin_tone': '๐Ÿง’๐Ÿพ', + 'child_medium_light_skin_tone': '๐Ÿง’๐Ÿผ', + 'child_medium_skin_tone': '๐Ÿง’๐Ÿฝ', + 'ci': '๐Ÿ‡จ๐Ÿ‡ฎ', + 'ck': '๐Ÿ‡จ๐Ÿ‡ฐ', + 'clinking_glass': '๐Ÿฅ‚', + 'cloud_lightning': '๐ŸŒฉ', + 'cloud_rain': '๐ŸŒง', + 'cloud_snow': '๐ŸŒจ', + 'cloud_tornado': '๐ŸŒช', + 'cloud_with_tornado': '๐ŸŒช', + 'cm': '๐Ÿ‡จ๐Ÿ‡ฒ', + 'cn': '๐Ÿ‡จ๐Ÿ‡ณ', + 'co': '๐Ÿ‡จ๐Ÿ‡ด', + 'congo': '๐Ÿ‡จ๐Ÿ‡ฉ', + 'couch': '๐Ÿ›‹', + 'couple_mm': '๐Ÿ‘จโ€โค๏ธโ€๐Ÿ‘จ', + 'couple_with_heart_woman_man': '๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ‘จ', + 'couple_ww': '๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ‘ฉ', + 'couplekiss_mm': '๐Ÿ‘จโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ', + 'couplekiss_ww': '๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘ฉ', + 'cow2': '๐Ÿ„', + 'cp': '๐Ÿ‡จ๐Ÿ‡ต', + 'cr': '๐Ÿ‡จ๐Ÿ‡ท', + 'cricket_bat_ball': '๐Ÿ', + 'cross': 'โœ', + 'crying_cat_face': '๐Ÿ˜ฟ', + 'cu': '๐Ÿ‡จ๐Ÿ‡บ', + 'cv': '๐Ÿ‡จ๐Ÿ‡ป', + 'cw': '๐Ÿ‡จ๐Ÿ‡ผ', + 'cx': '๐Ÿ‡จ๐Ÿ‡ฝ', + 'cy': '๐Ÿ‡จ๐Ÿ‡พ', + 'cz': '๐Ÿ‡จ๐Ÿ‡ฟ', + 'dagger_knife': '๐Ÿ—ก', + 'dark_sunglasses': '๐Ÿ•ถ', + 'de': '๐Ÿ‡ฉ๐Ÿ‡ช', + 'derelict_house_building': '๐Ÿš', + 'desktop': '๐Ÿ–ฅ', + 'dg': '๐Ÿ‡ฉ๐Ÿ‡ฌ', + 'dividers': '๐Ÿ—‚', + 'dj': '๐Ÿ‡ฉ๐Ÿ‡ฏ', + 'dk': '๐Ÿ‡ฉ๐Ÿ‡ฐ', + 'dm': '๐Ÿ‡ฉ๐Ÿ‡ฒ', + 'do': '๐Ÿ‡ฉ๐Ÿ‡ด', + 'dog2': '๐Ÿ•', + 'double_vertical_bar': 'โธ', + 'dove_of_peace': '๐Ÿ•Š', + 'drool': '๐Ÿคค', + 'drum_with_drumsticks': '๐Ÿฅ', + 'dz': '๐Ÿ‡ฉ๐Ÿ‡ฟ', + 'ea': '๐Ÿ‡ช๐Ÿ‡ฆ', + 'ec': '๐Ÿ‡ช๐Ÿ‡จ', + 'ee': '๐Ÿ‡ช๐Ÿ‡ช', + 'eg': '๐Ÿ‡ช๐Ÿ‡ฌ', + 'eh': '๐Ÿ‡ช๐Ÿ‡ญ', + 'eject_symbol': 'โ', + 'elf_dark_skin_tone': '๐Ÿง๐Ÿฟ', + 'elf_light_skin_tone': '๐Ÿง๐Ÿป', + 'elf_medium_dark_skin_tone': '๐Ÿง๐Ÿพ', + 'elf_medium_light_skin_tone': '๐Ÿง๐Ÿผ', + 'elf_medium_skin_tone': '๐Ÿง๐Ÿฝ', + 'er': '๐Ÿ‡ช๐Ÿ‡ท', + 'es': '๐Ÿ‡ช๐Ÿ‡ธ', + 'et': '๐Ÿ‡ช๐Ÿ‡น', + 'eu': '๐Ÿ‡ช๐Ÿ‡บ', + 'expecting_woman_tone1': '๐Ÿคฐ๐Ÿป', + 'expecting_woman_tone2': '๐Ÿคฐ๐Ÿผ', + 'expecting_woman_tone3': '๐Ÿคฐ๐Ÿฝ', + 'expecting_woman_tone4': '๐Ÿคฐ๐Ÿพ', + 'expecting_woman_tone5': '๐Ÿคฐ๐Ÿฟ', + 'expecting_woman': '๐Ÿคฐ', + 'face_palm_tone1': '๐Ÿคฆ๐Ÿป', + 'face_palm_tone2': '๐Ÿคฆ๐Ÿผ', + 'face_palm_tone3': '๐Ÿคฆ๐Ÿฝ', + 'face_palm_tone4': '๐Ÿคฆ๐Ÿพ', + 'face_palm_tone5': '๐Ÿคฆ๐Ÿฟ', + 'face_palm': '๐Ÿคฆ', + 'face_with_cowboy_hat': '๐Ÿค ', + 'face_with_rolling_eyes': '๐Ÿ™„', + 'face_with_symbols_over_mouth': '๐Ÿคฌ', + 'fairy_dark_skin_tone': '๐Ÿงš๐Ÿฟ', + 'fairy_light_skin_tone': '๐Ÿงš๐Ÿป', + 'fairy_medium_dark_skin_tone': '๐Ÿงš๐Ÿพ', + 'fairy_medium_light_skin_tone': '๐Ÿงš๐Ÿผ', + 'fairy_medium_skin_tone': '๐Ÿงš๐Ÿฝ', + 'family_man_boy_boy': '๐Ÿ‘จโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ', + 'family_man_boy': '๐Ÿ‘จโ€๐Ÿ‘ฆ', + 'family_man_girl_boy': '๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ', + 'family_man_girl_girl': '๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง', + 'family_man_girl': '๐Ÿ‘จโ€๐Ÿ‘ง', + 'family_man_woman_boy': '๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ', + 'family_woman_boy_boy': '๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ', + 'family_woman_boy': '๐Ÿ‘ฉโ€๐Ÿ‘ฆ', + 'family_woman_girl_boy': '๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ', + 'family_woman_girl_girl': '๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง', + 'family_woman_girl': '๐Ÿ‘ฉโ€๐Ÿ‘ง', + 'feet': '๐Ÿพ', + 'fi': '๐Ÿ‡ซ๐Ÿ‡ฎ', + 'first_place': '๐Ÿฅ‡', + 'fj': '๐Ÿ‡ซ๐Ÿ‡ฏ', + 'fk': '๐Ÿ‡ซ๐Ÿ‡ฐ', + 'flag_black': '๐Ÿด', + 'flag_white': '๐Ÿณ', + 'flame': '๐Ÿ”ฅ', + 'flan': '๐Ÿฎ', + 'fm': '๐Ÿ‡ซ๐Ÿ‡ฒ', + 'fo': '๐Ÿ‡ซ๐Ÿ‡ด', + 'foot_dark_skin_tone': '๐Ÿฆถ๐Ÿฟ', + 'foot_light_skin_tone': '๐Ÿฆถ๐Ÿป', + 'foot_medium_dark_skin_tone': '๐Ÿฆถ๐Ÿพ', + 'foot_medium_light_skin_tone': '๐Ÿฆถ๐Ÿผ', + 'foot_medium_skin_tone': '๐Ÿฆถ๐Ÿฝ', + 'fork_and_knife_with_plate': '๐Ÿฝ', + 'fr': '๐Ÿ‡ซ๐Ÿ‡ท', + 'frame_photo': '๐Ÿ–ผ', + 'french_bread': '๐Ÿฅ–', + 'frowning2': 'โ˜น', + 'ga': '๐Ÿ‡ฌ๐Ÿ‡ฆ', + 'gay_pride_flag': '๐Ÿณ๏ธโ€๐ŸŒˆ', + 'gb': '๐Ÿ‡ฌ๐Ÿ‡ง', + 'gd': '๐Ÿ‡ฌ๐Ÿ‡ฉ', + 'ge': '๐Ÿ‡ฌ๐Ÿ‡ช', + 'gf': '๐Ÿ‡ฌ๐Ÿ‡ซ', + 'gg': '๐Ÿ‡ฌ๐Ÿ‡ฌ', + 'gh': '๐Ÿ‡ฌ๐Ÿ‡ญ', + 'gi': '๐Ÿ‡ฌ๐Ÿ‡ฎ', + 'gl': '๐Ÿ‡ฌ๐Ÿ‡ฑ', + 'gm': '๐Ÿ‡ฌ๐Ÿ‡ฒ', + 'gn': '๐Ÿ‡ฌ๐Ÿ‡ณ', + 'goal': '๐Ÿฅ…', + 'gp': '๐Ÿ‡ฌ๐Ÿ‡ต', + 'gq': '๐Ÿ‡ฌ๐Ÿ‡ถ', + 'gr': '๐Ÿ‡ฌ๐Ÿ‡ท', + 'grandma_tone1': '๐Ÿ‘ต๐Ÿป', + 'grandma_tone2': '๐Ÿ‘ต๐Ÿผ', + 'grandma_tone3': '๐Ÿ‘ต๐Ÿฝ', + 'grandma_tone4': '๐Ÿ‘ต๐Ÿพ', + 'grandma_tone5': '๐Ÿ‘ต๐Ÿฟ', + 'grandma': '๐Ÿ‘ต', + 'grey_exclamation': 'โ•', + 'grey_question': 'โ”', + 'gs': '๐Ÿ‡ฌ๐Ÿ‡ธ', + 'gt': '๐Ÿ‡ฌ๐Ÿ‡น', + 'gu': '๐Ÿ‡ฌ๐Ÿ‡บ', + 'guardsman_tone1': '๐Ÿ’‚๐Ÿป', + 'guardsman_tone2': '๐Ÿ’‚๐Ÿผ', + 'guardsman_tone3': '๐Ÿ’‚๐Ÿฝ', + 'guardsman_tone4': '๐Ÿ’‚๐Ÿพ', + 'guardsman_tone5': '๐Ÿ’‚๐Ÿฟ', + 'guardsman': '๐Ÿ’‚', + 'gw': '๐Ÿ‡ฌ๐Ÿ‡ผ', + 'gy': '๐Ÿ‡ฌ๐Ÿ‡พ', + 'hammer_pick': 'โš’', + 'hand_splayed_tone1': '๐Ÿ–๐Ÿป', + 'hand_splayed_tone2': '๐Ÿ–๐Ÿผ', + 'hand_splayed_tone3': '๐Ÿ–๐Ÿฝ', + 'hand_splayed_tone4': '๐Ÿ–๐Ÿพ', + 'hand_splayed_tone5': '๐Ÿ–๐Ÿฟ', + 'hand_splayed': '๐Ÿ–', + 'hand_with_index_and_middle_finger_crossed': '๐Ÿคž', + 'hand_with_index_and_middle_fingers_crossed_tone1': '๐Ÿคž๐Ÿป', + 'hand_with_index_and_middle_fingers_crossed_tone2': '๐Ÿคž๐Ÿผ', + 'hand_with_index_and_middle_fingers_crossed_tone3': '๐Ÿคž๐Ÿฝ', + 'hand_with_index_and_middle_fingers_crossed_tone4': '๐Ÿคž๐Ÿพ', + 'hand_with_index_and_middle_fingers_crossed_tone5': '๐Ÿคž๐Ÿฟ', + 'hankey': '๐Ÿ’ฉ', + 'head_bandage': '๐Ÿค•', + 'heavy_division_sign': 'โž—', + 'heavy_heart_exclamation_mark_ornament': 'โฃ', + 'heavy_minus_sign': 'โž–', + 'heavy_multiplication_x': 'โœ–', + 'heavy_plus_sign': 'โž•', + 'helmet_with_white_cross': 'โ›‘', + 'hippopotamus': '๐Ÿฆ›', + 'hk': '๐Ÿ‡ญ๐Ÿ‡ฐ', + 'hm': '๐Ÿ‡ญ๐Ÿ‡ฒ', + 'hn': '๐Ÿ‡ญ๐Ÿ‡ณ', + 'hot_dog': '๐ŸŒญ', + 'house_buildings': '๐Ÿ˜', + 'hr': '๐Ÿ‡ญ๐Ÿ‡ท', + 'ht': '๐Ÿ‡ญ๐Ÿ‡น', + 'hu': '๐Ÿ‡ญ๐Ÿ‡บ', + 'ic': '๐Ÿ‡ฎ๐Ÿ‡จ', + 'ie': '๐Ÿ‡ฎ๐Ÿ‡ช', + 'il': '๐Ÿ‡ฎ๐Ÿ‡ฑ', + 'im': '๐Ÿ‡ฎ๐Ÿ‡ฒ', + 'in': '๐Ÿ‡ฎ๐Ÿ‡ณ', + 'information_desk_person_tone1': '๐Ÿ’๐Ÿป', + 'information_desk_person_tone2': '๐Ÿ’๐Ÿผ', + 'information_desk_person_tone3': '๐Ÿ’๐Ÿฝ', + 'information_desk_person_tone4': '๐Ÿ’๐Ÿพ', + 'information_desk_person_tone5': '๐Ÿ’๐Ÿฟ', + 'information_desk_person': '๐Ÿ’', + 'io': '๐Ÿ‡ฎ๐Ÿ‡ด', + 'iq': '๐Ÿ‡ฎ๐Ÿ‡ถ', + 'ir': '๐Ÿ‡ฎ๐Ÿ‡ท', + 'is': '๐Ÿ‡ฎ๐Ÿ‡ธ', + 'it': '๐Ÿ‡ฎ๐Ÿ‡น', + 'je': '๐Ÿ‡ฏ๐Ÿ‡ช', + 'jm': '๐Ÿ‡ฏ๐Ÿ‡ฒ', + 'jo': '๐Ÿ‡ฏ๐Ÿ‡ด', + 'jp': '๐Ÿ‡ฏ๐Ÿ‡ต', + 'karate_uniform': '๐Ÿฅ‹', + 'kayak': '๐Ÿ›ถ', + 'ke': '๐Ÿ‡ฐ๐Ÿ‡ช', + 'key2': '๐Ÿ—', + 'keycap_asterisk': '*๏ธโƒฃ', + 'keycap_ten': '๐Ÿ”Ÿ', + 'kg': '๐Ÿ‡ฐ๐Ÿ‡ฌ', + 'kh': '๐Ÿ‡ฐ๐Ÿ‡ญ', + 'ki': '๐Ÿ‡ฐ๐Ÿ‡ฎ', + 'kiss_woman_man': '๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ’‹โ€๐Ÿ‘จ', + 'kiwifruit': '๐Ÿฅ', + 'km': '๐Ÿ‡ฐ๐Ÿ‡ฒ', + 'kn': '๐Ÿ‡ฐ๐Ÿ‡ณ', + 'kp': '๐Ÿ‡ฐ๐Ÿ‡ต', + 'kr': '๐Ÿ‡ฐ๐Ÿ‡ท', + 'kw': '๐Ÿ‡ฐ๐Ÿ‡ผ', + 'ky': '๐Ÿ‡ฐ๐Ÿ‡พ', + 'kz': '๐Ÿ‡ฐ๐Ÿ‡ฟ', + 'la': '๐Ÿ‡ฑ๐Ÿ‡ฆ', + 'lb': '๐Ÿ‡ฑ๐Ÿ‡ง', + 'lc': '๐Ÿ‡ฑ๐Ÿ‡จ', + 'left_fist_tone1': '๐Ÿค›๐Ÿป', + 'left_fist_tone2': '๐Ÿค›๐Ÿผ', + 'left_fist_tone3': '๐Ÿค›๐Ÿฝ', + 'left_fist_tone4': '๐Ÿค›๐Ÿพ', + 'left_fist_tone5': '๐Ÿค›๐Ÿฟ', + 'left_fist': '๐Ÿค›', + 'leg_dark_skin_tone': '๐Ÿฆต๐Ÿฟ', + 'leg_light_skin_tone': '๐Ÿฆต๐Ÿป', + 'leg_medium_dark_skin_tone': '๐Ÿฆต๐Ÿพ', + 'leg_medium_light_skin_tone': '๐Ÿฆต๐Ÿผ', + 'leg_medium_skin_tone': '๐Ÿฆต๐Ÿฝ', + 'li': '๐Ÿ‡ฑ๐Ÿ‡ฎ', + 'liar': '๐Ÿคฅ', + 'lifter_tone1': '๐Ÿ‹๐Ÿป', + 'lifter_tone2': '๐Ÿ‹๐Ÿผ', + 'lifter_tone3': '๐Ÿ‹๐Ÿฝ', + 'lifter_tone4': '๐Ÿ‹๐Ÿพ', + 'lifter_tone5': '๐Ÿ‹๐Ÿฟ', + 'lifter': '๐Ÿ‹', + 'linked_paperclips': '๐Ÿ–‡', + 'lk': '๐Ÿ‡ฑ๐Ÿ‡ฐ', + 'love_you_gesture_dark_skin_tone': '๐ŸคŸ๐Ÿฟ', + 'love_you_gesture_light_skin_tone': '๐ŸคŸ๐Ÿป', + 'love_you_gesture_medium_dark_skin_tone': '๐ŸคŸ๐Ÿพ', + 'love_you_gesture_medium_light_skin_tone': '๐ŸคŸ๐Ÿผ', + 'love_you_gesture_medium_skin_tone': '๐ŸคŸ๐Ÿฝ', + 'lower_left_ballpoint_pen': '๐Ÿ–Š', + 'lower_left_crayon': '๐Ÿ–', + 'lower_left_fountain_pen': '๐Ÿ–‹', + 'lower_left_paintbrush': '๐Ÿ–Œ', + 'lr': '๐Ÿ‡ฑ๐Ÿ‡ท', + 'ls': '๐Ÿ‡ฑ๐Ÿ‡ธ', + 'lt': '๐Ÿ‡ฑ๐Ÿ‡น', + 'lu': '๐Ÿ‡ฑ๐Ÿ‡บ', + 'lv': '๐Ÿ‡ฑ๐Ÿ‡ป', + 'ly': '๐Ÿ‡ฑ๐Ÿ‡พ', + 'ma': '๐Ÿ‡ฒ๐Ÿ‡ฆ', + 'mage_dark_skin_tone': '๐Ÿง™๐Ÿฟ', + 'mage_light_skin_tone': '๐Ÿง™๐Ÿป', + 'mage_medium_dark_skin_tone': '๐Ÿง™๐Ÿพ', + 'mage_medium_light_skin_tone': '๐Ÿง™๐Ÿผ', + 'mage_medium_skin_tone': '๐Ÿง™๐Ÿฝ', + 'male_dancer_tone1': '๐Ÿ•บ๐Ÿป', + 'male_dancer_tone2': '๐Ÿ•บ๐Ÿผ', + 'male_dancer_tone3': '๐Ÿ•บ๐Ÿฝ', + 'male_dancer_tone4': '๐Ÿ•บ๐Ÿพ', + 'male_dancer_tone5': '๐Ÿ•บ๐Ÿฟ', + 'male_dancer': '๐Ÿ•บ', + 'man_artist_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€๐ŸŽจ', + 'man_artist_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€๐ŸŽจ', + 'man_artist_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€๐ŸŽจ', + 'man_artist_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€๐ŸŽจ', + 'man_artist_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€๐ŸŽจ', + 'man_astronaut_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€๐Ÿš€', + 'man_astronaut_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€๐Ÿš€', + 'man_astronaut_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€๐Ÿš€', + 'man_astronaut_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€๐Ÿš€', + 'man_astronaut_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€๐Ÿš€', + 'man_bald_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆฒ', + 'man_bald_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฒ', + 'man_bald_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€๐Ÿฆฒ', + 'man_bald_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€๐Ÿฆฒ', + 'man_bald_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆฒ', + 'man_biking_dark_skin_tone': '๐Ÿšด๐Ÿฟโ€โ™‚๏ธ', + 'man_biking_light_skin_tone': '๐Ÿšด๐Ÿปโ€โ™‚๏ธ', + 'man_biking_medium_dark_skin_tone': '๐Ÿšด๐Ÿพโ€โ™‚๏ธ', + 'man_biking_medium_light_skin_tone': '๐Ÿšด๐Ÿผโ€โ™‚๏ธ', + 'man_biking_medium_skin_tone': '๐Ÿšด๐Ÿฝโ€โ™‚๏ธ', + 'man_bouncing_ball_dark_skin_tone': 'โ›น๐Ÿฟโ€โ™‚๏ธ', + 'man_bouncing_ball_light_skin_tone': 'โ›น๐Ÿปโ€โ™‚๏ธ', + 'man_bouncing_ball_medium_dark_skin_tone': 'โ›น๐Ÿพโ€โ™‚๏ธ', + 'man_bouncing_ball_medium_light_skin_tone': 'โ›น๐Ÿผโ€โ™‚๏ธ', + 'man_bouncing_ball_medium_skin_tone': 'โ›น๐Ÿฝโ€โ™‚๏ธ', + 'man_bowing_dark_skin_tone': '๐Ÿ™‡๐Ÿฟโ€โ™‚๏ธ', + 'man_bowing_light_skin_tone': '๐Ÿ™‡๐Ÿปโ€โ™‚๏ธ', + 'man_bowing_medium_dark_skin_tone': '๐Ÿ™‡๐Ÿพโ€โ™‚๏ธ', + 'man_bowing_medium_light_skin_tone': '๐Ÿ™‡๐Ÿผโ€โ™‚๏ธ', + 'man_bowing_medium_skin_tone': '๐Ÿ™‡๐Ÿฝโ€โ™‚๏ธ', + 'man_cartwheeling_dark_skin_tone': '๐Ÿคธ๐Ÿฟโ€โ™‚๏ธ', + 'man_cartwheeling_light_skin_tone': '๐Ÿคธ๐Ÿปโ€โ™‚๏ธ', + 'man_cartwheeling_medium_dark_skin_tone': '๐Ÿคธ๐Ÿพโ€โ™‚๏ธ', + 'man_cartwheeling_medium_light_skin_tone': '๐Ÿคธ๐Ÿผโ€โ™‚๏ธ', + 'man_cartwheeling_medium_skin_tone': '๐Ÿคธ๐Ÿฝโ€โ™‚๏ธ', + 'man_climbing_dark_skin_tone': '๐Ÿง—๐Ÿฟโ€โ™‚๏ธ', + 'man_climbing_light_skin_tone': '๐Ÿง—๐Ÿปโ€โ™‚๏ธ', + 'man_climbing_medium_dark_skin_tone': '๐Ÿง—๐Ÿพโ€โ™‚๏ธ', + 'man_climbing_medium_light_skin_tone': '๐Ÿง—๐Ÿผโ€โ™‚๏ธ', + 'man_climbing_medium_skin_tone': '๐Ÿง—๐Ÿฝโ€โ™‚๏ธ', + 'man_construction_worker_dark_skin_tone': '๐Ÿ‘ท๐Ÿฟโ€โ™‚๏ธ', + 'man_construction_worker_light_skin_tone': '๐Ÿ‘ท๐Ÿปโ€โ™‚๏ธ', + 'man_construction_worker_medium_dark_skin_tone': '๐Ÿ‘ท๐Ÿพโ€โ™‚๏ธ', + 'man_construction_worker_medium_light_skin_tone': '๐Ÿ‘ท๐Ÿผโ€โ™‚๏ธ', + 'man_construction_worker_medium_skin_tone': '๐Ÿ‘ท๐Ÿฝโ€โ™‚๏ธ', + 'man_cook_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€๐Ÿณ', + 'man_cook_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€๐Ÿณ', + 'man_cook_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€๐Ÿณ', + 'man_cook_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€๐Ÿณ', + 'man_cook_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€๐Ÿณ', + 'man_curly_haired_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆฑ', + 'man_curly_haired_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฑ', + 'man_curly_haired_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€๐Ÿฆฑ', + 'man_curly_haired_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€๐Ÿฆฑ', + 'man_curly_haired_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆฑ', + 'man_detective_dark_skin_tone': '๐Ÿ•ต๐Ÿฟโ€โ™‚๏ธ', + 'man_detective_light_skin_tone': '๐Ÿ•ต๐Ÿปโ€โ™‚๏ธ', + 'man_detective_medium_dark_skin_tone': '๐Ÿ•ต๐Ÿพโ€โ™‚๏ธ', + 'man_detective_medium_light_skin_tone': '๐Ÿ•ต๐Ÿผโ€โ™‚๏ธ', + 'man_detective_medium_skin_tone': '๐Ÿ•ต๐Ÿฝโ€โ™‚๏ธ', + 'man_elf_dark_skin_tone': '๐Ÿง๐Ÿฟโ€โ™‚๏ธ', + 'man_elf_light_skin_tone': '๐Ÿง๐Ÿปโ€โ™‚๏ธ', + 'man_elf_medium_dark_skin_tone': '๐Ÿง๐Ÿพโ€โ™‚๏ธ', + 'man_elf_medium_light_skin_tone': '๐Ÿง๐Ÿผโ€โ™‚๏ธ', + 'man_elf_medium_skin_tone': '๐Ÿง๐Ÿฝโ€โ™‚๏ธ', + 'man_facepalming_dark_skin_tone': '๐Ÿคฆ๐Ÿฟโ€โ™‚๏ธ', + 'man_facepalming_light_skin_tone': '๐Ÿคฆ๐Ÿปโ€โ™‚๏ธ', + 'man_facepalming_medium_dark_skin_tone': '๐Ÿคฆ๐Ÿพโ€โ™‚๏ธ', + 'man_facepalming_medium_light_skin_tone': '๐Ÿคฆ๐Ÿผโ€โ™‚๏ธ', + 'man_facepalming_medium_skin_tone': '๐Ÿคฆ๐Ÿฝโ€โ™‚๏ธ', + 'man_factory_worker_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€๐Ÿญ', + 'man_factory_worker_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€๐Ÿญ', + 'man_factory_worker_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€๐Ÿญ', + 'man_factory_worker_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€๐Ÿญ', + 'man_factory_worker_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€๐Ÿญ', + 'man_fairy_dark_skin_tone': '๐Ÿงš๐Ÿฟโ€โ™‚๏ธ', + 'man_fairy_light_skin_tone': '๐Ÿงš๐Ÿปโ€โ™‚๏ธ', + 'man_fairy_medium_dark_skin_tone': '๐Ÿงš๐Ÿพโ€โ™‚๏ธ', + 'man_fairy_medium_light_skin_tone': '๐Ÿงš๐Ÿผโ€โ™‚๏ธ', + 'man_fairy_medium_skin_tone': '๐Ÿงš๐Ÿฝโ€โ™‚๏ธ', + 'man_farmer_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€๐ŸŒพ', + 'man_farmer_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€๐ŸŒพ', + 'man_farmer_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€๐ŸŒพ', + 'man_farmer_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€๐ŸŒพ', + 'man_farmer_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€๐ŸŒพ', + 'man_firefighter_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€๐Ÿš’', + 'man_firefighter_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€๐Ÿš’', + 'man_firefighter_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€๐Ÿš’', + 'man_firefighter_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€๐Ÿš’', + 'man_firefighter_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€๐Ÿš’', + 'man_frowning_dark_skin_tone': '๐Ÿ™๐Ÿฟโ€โ™‚๏ธ', + 'man_frowning_light_skin_tone': '๐Ÿ™๐Ÿปโ€โ™‚๏ธ', + 'man_frowning_medium_dark_skin_tone': '๐Ÿ™๐Ÿพโ€โ™‚๏ธ', + 'man_frowning_medium_light_skin_tone': '๐Ÿ™๐Ÿผโ€โ™‚๏ธ', + 'man_frowning_medium_skin_tone': '๐Ÿ™๐Ÿฝโ€โ™‚๏ธ', + 'man_gesturing_no_dark_skin_tone': '๐Ÿ™…๐Ÿฟโ€โ™‚๏ธ', + 'man_gesturing_no_light_skin_tone': '๐Ÿ™…๐Ÿปโ€โ™‚๏ธ', + 'man_gesturing_no_medium_dark_skin_tone': '๐Ÿ™…๐Ÿพโ€โ™‚๏ธ', + 'man_gesturing_no_medium_light_skin_tone': '๐Ÿ™…๐Ÿผโ€โ™‚๏ธ', + 'man_gesturing_no_medium_skin_tone': '๐Ÿ™…๐Ÿฝโ€โ™‚๏ธ', + 'man_gesturing_ok_dark_skin_tone': '๐Ÿ™†๐Ÿฟโ€โ™‚๏ธ', + 'man_gesturing_ok_light_skin_tone': '๐Ÿ™†๐Ÿปโ€โ™‚๏ธ', + 'man_gesturing_ok_medium_dark_skin_tone': '๐Ÿ™†๐Ÿพโ€โ™‚๏ธ', + 'man_gesturing_ok_medium_light_skin_tone': '๐Ÿ™†๐Ÿผโ€โ™‚๏ธ', + 'man_gesturing_ok_medium_skin_tone': '๐Ÿ™†๐Ÿฝโ€โ™‚๏ธ', + 'man_getting_face_massage_dark_skin_tone': '๐Ÿ’†๐Ÿฟโ€โ™‚๏ธ', + 'man_getting_face_massage_light_skin_tone': '๐Ÿ’†๐Ÿปโ€โ™‚๏ธ', + 'man_getting_face_massage_medium_dark_skin_tone': '๐Ÿ’†๐Ÿพโ€โ™‚๏ธ', + 'man_getting_face_massage_medium_light_skin_tone': '๐Ÿ’†๐Ÿผโ€โ™‚๏ธ', + 'man_getting_face_massage_medium_skin_tone': '๐Ÿ’†๐Ÿฝโ€โ™‚๏ธ', + 'man_getting_face_massage_tone1': '๐Ÿ’†๐Ÿปโ€โ™‚๏ธ', + 'man_getting_face_massage_tone2': '๐Ÿ’†๐Ÿผโ€โ™‚๏ธ', + 'man_getting_face_massage_tone3': '๐Ÿ’†๐Ÿฝโ€โ™‚๏ธ', + 'man_getting_face_massage_tone4': '๐Ÿ’†๐Ÿพโ€โ™‚๏ธ', + 'man_getting_face_massage_tone5': '๐Ÿ’†๐Ÿฟโ€โ™‚๏ธ', + 'man_getting_face_massage': '๐Ÿ’†โ€โ™‚๏ธ', + 'man_getting_haircut_dark_skin_tone': '๐Ÿ’‡๐Ÿฟโ€โ™‚๏ธ', + 'man_getting_haircut_light_skin_tone': '๐Ÿ’‡๐Ÿปโ€โ™‚๏ธ', + 'man_getting_haircut_medium_dark_skin_tone': '๐Ÿ’‡๐Ÿพโ€โ™‚๏ธ', + 'man_getting_haircut_medium_light_skin_tone': '๐Ÿ’‡๐Ÿผโ€โ™‚๏ธ', + 'man_getting_haircut_medium_skin_tone': '๐Ÿ’‡๐Ÿฝโ€โ™‚๏ธ', + 'man_golfing_dark_skin_tone': '๐ŸŒ๐Ÿฟโ€โ™‚๏ธ', + 'man_golfing_light_skin_tone': '๐ŸŒ๐Ÿปโ€โ™‚๏ธ', + 'man_golfing_medium_dark_skin_tone': '๐ŸŒ๐Ÿพโ€โ™‚๏ธ', + 'man_golfing_medium_light_skin_tone': '๐ŸŒ๐Ÿผโ€โ™‚๏ธ', + 'man_golfing_medium_skin_tone': '๐ŸŒ๐Ÿฝโ€โ™‚๏ธ', + 'man_guard_dark_skin_tone': '๐Ÿ’‚๐Ÿฟโ€โ™‚๏ธ', + 'man_guard_light_skin_tone': '๐Ÿ’‚๐Ÿปโ€โ™‚๏ธ', + 'man_guard_medium_dark_skin_tone': '๐Ÿ’‚๐Ÿพโ€โ™‚๏ธ', + 'man_guard_medium_light_skin_tone': '๐Ÿ’‚๐Ÿผโ€โ™‚๏ธ', + 'man_guard_medium_skin_tone': '๐Ÿ’‚๐Ÿฝโ€โ™‚๏ธ', + 'man_health_worker_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€โš•๏ธ', + 'man_health_worker_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€โš•๏ธ', + 'man_health_worker_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€โš•๏ธ', + 'man_health_worker_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€โš•๏ธ', + 'man_health_worker_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€โš•๏ธ', + 'man_in_business_suit_levitating_dark_skin_tone': '๐Ÿ•ด๐Ÿฟ', + 'man_in_business_suit_levitating_light_skin_tone': '๐Ÿ•ด๐Ÿป', + 'man_in_business_suit_levitating_medium_dark_skin_tone': '๐Ÿ•ด๐Ÿพ', + 'man_in_business_suit_levitating_medium_light_skin_tone': '๐Ÿ•ด๐Ÿผ', + 'man_in_business_suit_levitating_medium_skin_tone': '๐Ÿ•ด๐Ÿฝ', + 'man_in_business_suit_levitating_tone1': '๐Ÿ•ด๐Ÿป', + 'man_in_business_suit_levitating_tone2': '๐Ÿ•ด๐Ÿผ', + 'man_in_business_suit_levitating_tone3': '๐Ÿ•ด๐Ÿฝ', + 'man_in_business_suit_levitating_tone4': '๐Ÿ•ด๐Ÿพ', + 'man_in_business_suit_levitating_tone5': '๐Ÿ•ด๐Ÿฟ', + 'man_in_business_suit_levitating': '๐Ÿ•ด', + 'man_in_lotus_position_dark_skin_tone': '๐Ÿง˜๐Ÿฟโ€โ™‚๏ธ', + 'man_in_lotus_position_light_skin_tone': '๐Ÿง˜๐Ÿปโ€โ™‚๏ธ', + 'man_in_lotus_position_medium_dark_skin_tone': '๐Ÿง˜๐Ÿพโ€โ™‚๏ธ', + 'man_in_lotus_position_medium_light_skin_tone': '๐Ÿง˜๐Ÿผโ€โ™‚๏ธ', + 'man_in_lotus_position_medium_skin_tone': '๐Ÿง˜๐Ÿฝโ€โ™‚๏ธ', + 'man_in_steamy_room_dark_skin_tone': '๐Ÿง–๐Ÿฟโ€โ™‚๏ธ', + 'man_in_steamy_room_light_skin_tone': '๐Ÿง–๐Ÿปโ€โ™‚๏ธ', + 'man_in_steamy_room_medium_dark_skin_tone': '๐Ÿง–๐Ÿพโ€โ™‚๏ธ', + 'man_in_steamy_room_medium_light_skin_tone': '๐Ÿง–๐Ÿผโ€โ™‚๏ธ', + 'man_in_steamy_room_medium_skin_tone': '๐Ÿง–๐Ÿฝโ€โ™‚๏ธ', + 'man_judge_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€โš–๏ธ', + 'man_judge_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€โš–๏ธ', + 'man_judge_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€โš–๏ธ', + 'man_judge_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€โš–๏ธ', + 'man_judge_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€โš–๏ธ', + 'man_juggling_dark_skin_tone': '๐Ÿคน๐Ÿฟโ€โ™‚๏ธ', + 'man_juggling_light_skin_tone': '๐Ÿคน๐Ÿปโ€โ™‚๏ธ', + 'man_juggling_medium_dark_skin_tone': '๐Ÿคน๐Ÿพโ€โ™‚๏ธ', + 'man_juggling_medium_light_skin_tone': '๐Ÿคน๐Ÿผโ€โ™‚๏ธ', + 'man_juggling_medium_skin_tone': '๐Ÿคน๐Ÿฝโ€โ™‚๏ธ', + 'man_lifting_weights_dark_skin_tone': '๐Ÿ‹๐Ÿฟโ€โ™‚๏ธ', + 'man_lifting_weights_light_skin_tone': '๐Ÿ‹๐Ÿปโ€โ™‚๏ธ', + 'man_lifting_weights_medium_dark_skin_tone': '๐Ÿ‹๐Ÿพโ€โ™‚๏ธ', + 'man_lifting_weights_medium_light_skin_tone': '๐Ÿ‹๐Ÿผโ€โ™‚๏ธ', + 'man_lifting_weights_medium_skin_tone': '๐Ÿ‹๐Ÿฝโ€โ™‚๏ธ', + 'man_mage_dark_skin_tone': '๐Ÿง™๐Ÿฟโ€โ™‚๏ธ', + 'man_mage_light_skin_tone': '๐Ÿง™๐Ÿปโ€โ™‚๏ธ', + 'man_mage_medium_dark_skin_tone': '๐Ÿง™๐Ÿพโ€โ™‚๏ธ', + 'man_mage_medium_light_skin_tone': '๐Ÿง™๐Ÿผโ€โ™‚๏ธ', + 'man_mage_medium_skin_tone': '๐Ÿง™๐Ÿฝโ€โ™‚๏ธ', + 'man_mechanic_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€๐Ÿ”ง', + 'man_mechanic_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€๐Ÿ”ง', + 'man_mechanic_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€๐Ÿ”ง', + 'man_mechanic_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€๐Ÿ”ง', + 'man_mechanic_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€๐Ÿ”ง', + 'man_mountain_biking_dark_skin_tone': '๐Ÿšต๐Ÿฟโ€โ™‚๏ธ', + 'man_mountain_biking_light_skin_tone': '๐Ÿšต๐Ÿปโ€โ™‚๏ธ', + 'man_mountain_biking_medium_dark_skin_tone': '๐Ÿšต๐Ÿพโ€โ™‚๏ธ', + 'man_mountain_biking_medium_light_skin_tone': '๐Ÿšต๐Ÿผโ€โ™‚๏ธ', + 'man_mountain_biking_medium_skin_tone': '๐Ÿšต๐Ÿฝโ€โ™‚๏ธ', + 'man_office_worker_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€๐Ÿ’ผ', + 'man_office_worker_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ผ', + 'man_office_worker_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€๐Ÿ’ผ', + 'man_office_worker_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€๐Ÿ’ผ', + 'man_office_worker_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€๐Ÿ’ผ', + 'man_pilot_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€โœˆ๏ธ', + 'man_pilot_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€โœˆ๏ธ', + 'man_pilot_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€โœˆ๏ธ', + 'man_pilot_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€โœˆ๏ธ', + 'man_pilot_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€โœˆ๏ธ', + 'man_playing_handball_dark_skin_tone': '๐Ÿคพ๐Ÿฟโ€โ™‚๏ธ', + 'man_playing_handball_light_skin_tone': '๐Ÿคพ๐Ÿปโ€โ™‚๏ธ', + 'man_playing_handball_medium_dark_skin_tone': '๐Ÿคพ๐Ÿพโ€โ™‚๏ธ', + 'man_playing_handball_medium_light_skin_tone': '๐Ÿคพ๐Ÿผโ€โ™‚๏ธ', + 'man_playing_handball_medium_skin_tone': '๐Ÿคพ๐Ÿฝโ€โ™‚๏ธ', + 'man_playing_water_polo_dark_skin_tone': '๐Ÿคฝ๐Ÿฟโ€โ™‚๏ธ', + 'man_playing_water_polo_light_skin_tone': '๐Ÿคฝ๐Ÿปโ€โ™‚๏ธ', + 'man_playing_water_polo_medium_dark_skin_tone': '๐Ÿคฝ๐Ÿพโ€โ™‚๏ธ', + 'man_playing_water_polo_medium_light_skin_tone': '๐Ÿคฝ๐Ÿผโ€โ™‚๏ธ', + 'man_playing_water_polo_medium_skin_tone': '๐Ÿคฝ๐Ÿฝโ€โ™‚๏ธ', + 'man_police_officer_dark_skin_tone': '๐Ÿ‘ฎ๐Ÿฟโ€โ™‚๏ธ', + 'man_police_officer_light_skin_tone': '๐Ÿ‘ฎ๐Ÿปโ€โ™‚๏ธ', + 'man_police_officer_medium_dark_skin_tone': '๐Ÿ‘ฎ๐Ÿพโ€โ™‚๏ธ', + 'man_police_officer_medium_light_skin_tone': '๐Ÿ‘ฎ๐Ÿผโ€โ™‚๏ธ', + 'man_police_officer_medium_skin_tone': '๐Ÿ‘ฎ๐Ÿฝโ€โ™‚๏ธ', + 'man_pouting_dark_skin_tone': '๐Ÿ™Ž๐Ÿฟโ€โ™‚๏ธ', + 'man_pouting_light_skin_tone': '๐Ÿ™Ž๐Ÿปโ€โ™‚๏ธ', + 'man_pouting_medium_dark_skin_tone': '๐Ÿ™Ž๐Ÿพโ€โ™‚๏ธ', + 'man_pouting_medium_light_skin_tone': '๐Ÿ™Ž๐Ÿผโ€โ™‚๏ธ', + 'man_pouting_medium_skin_tone': '๐Ÿ™Ž๐Ÿฝโ€โ™‚๏ธ', + 'man_raising_hand_dark_skin_tone': '๐Ÿ™‹๐Ÿฟโ€โ™‚๏ธ', + 'man_raising_hand_light_skin_tone': '๐Ÿ™‹๐Ÿปโ€โ™‚๏ธ', + 'man_raising_hand_medium_dark_skin_tone': '๐Ÿ™‹๐Ÿพโ€โ™‚๏ธ', + 'man_raising_hand_medium_light_skin_tone': '๐Ÿ™‹๐Ÿผโ€โ™‚๏ธ', + 'man_raising_hand_medium_skin_tone': '๐Ÿ™‹๐Ÿฝโ€โ™‚๏ธ', + 'man_red_haired_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆฐ', + 'man_red_haired_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฐ', + 'man_red_haired_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€๐Ÿฆฐ', + 'man_red_haired_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€๐Ÿฆฐ', + 'man_red_haired_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆฐ', + 'man_rowing_boat_dark_skin_tone': '๐Ÿšฃ๐Ÿฟโ€โ™‚๏ธ', + 'man_rowing_boat_light_skin_tone': '๐Ÿšฃ๐Ÿปโ€โ™‚๏ธ', + 'man_rowing_boat_medium_dark_skin_tone': '๐Ÿšฃ๐Ÿพโ€โ™‚๏ธ', + 'man_rowing_boat_medium_light_skin_tone': '๐Ÿšฃ๐Ÿผโ€โ™‚๏ธ', + 'man_rowing_boat_medium_skin_tone': '๐Ÿšฃ๐Ÿฝโ€โ™‚๏ธ', + 'man_running_dark_skin_tone': '๐Ÿƒ๐Ÿฟโ€โ™‚๏ธ', + 'man_running_light_skin_tone': '๐Ÿƒ๐Ÿปโ€โ™‚๏ธ', + 'man_running_medium_dark_skin_tone': '๐Ÿƒ๐Ÿพโ€โ™‚๏ธ', + 'man_running_medium_light_skin_tone': '๐Ÿƒ๐Ÿผโ€โ™‚๏ธ', + 'man_running_medium_skin_tone': '๐Ÿƒ๐Ÿฝโ€โ™‚๏ธ', + 'man_scientist_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€๐Ÿ”ฌ', + 'man_scientist_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€๐Ÿ”ฌ', + 'man_scientist_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€๐Ÿ”ฌ', + 'man_scientist_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€๐Ÿ”ฌ', + 'man_scientist_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€๐Ÿ”ฌ', + 'man_shrugging_dark_skin_tone': '๐Ÿคท๐Ÿฟโ€โ™‚๏ธ', + 'man_shrugging_light_skin_tone': '๐Ÿคท๐Ÿปโ€โ™‚๏ธ', + 'man_shrugging_medium_dark_skin_tone': '๐Ÿคท๐Ÿพโ€โ™‚๏ธ', + 'man_shrugging_medium_light_skin_tone': '๐Ÿคท๐Ÿผโ€โ™‚๏ธ', + 'man_shrugging_medium_skin_tone': '๐Ÿคท๐Ÿฝโ€โ™‚๏ธ', + 'man_singer_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€๐ŸŽค', + 'man_singer_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€๐ŸŽค', + 'man_singer_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€๐ŸŽค', + 'man_singer_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€๐ŸŽค', + 'man_singer_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€๐ŸŽค', + 'man_student_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€๐ŸŽ“', + 'man_student_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€๐ŸŽ“', + 'man_student_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€๐ŸŽ“', + 'man_student_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€๐ŸŽ“', + 'man_student_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€๐ŸŽ“', + 'man_superhero_dark_skin_tone': '๐Ÿฆธ๐Ÿฟโ€โ™‚๏ธ', + 'man_superhero_light_skin_tone': '๐Ÿฆธ๐Ÿปโ€โ™‚๏ธ', + 'man_superhero_medium_dark_skin_tone': '๐Ÿฆธ๐Ÿพโ€โ™‚๏ธ', + 'man_superhero_medium_light_skin_tone': '๐Ÿฆธ๐Ÿผโ€โ™‚๏ธ', + 'man_superhero_medium_skin_tone': '๐Ÿฆธ๐Ÿฝโ€โ™‚๏ธ', + 'man_supervillain_dark_skin_tone': '๐Ÿฆน๐Ÿฟโ€โ™‚๏ธ', + 'man_supervillain_light_skin_tone': '๐Ÿฆน๐Ÿปโ€โ™‚๏ธ', + 'man_supervillain_medium_dark_skin_tone': '๐Ÿฆน๐Ÿพโ€โ™‚๏ธ', + 'man_supervillain_medium_light_skin_tone': '๐Ÿฆน๐Ÿผโ€โ™‚๏ธ', + 'man_supervillain_medium_skin_tone': '๐Ÿฆน๐Ÿฝโ€โ™‚๏ธ', + 'man_surfing_dark_skin_tone': '๐Ÿ„๐Ÿฟโ€โ™‚๏ธ', + 'man_surfing_light_skin_tone': '๐Ÿ„๐Ÿปโ€โ™‚๏ธ', + 'man_surfing_medium_dark_skin_tone': '๐Ÿ„๐Ÿพโ€โ™‚๏ธ', + 'man_surfing_medium_light_skin_tone': '๐Ÿ„๐Ÿผโ€โ™‚๏ธ', + 'man_surfing_medium_skin_tone': '๐Ÿ„๐Ÿฝโ€โ™‚๏ธ', + 'man_swimming_dark_skin_tone': '๐ŸŠ๐Ÿฟโ€โ™‚๏ธ', + 'man_swimming_light_skin_tone': '๐ŸŠ๐Ÿปโ€โ™‚๏ธ', + 'man_swimming_medium_dark_skin_tone': '๐ŸŠ๐Ÿพโ€โ™‚๏ธ', + 'man_swimming_medium_light_skin_tone': '๐ŸŠ๐Ÿผโ€โ™‚๏ธ', + 'man_swimming_medium_skin_tone': '๐ŸŠ๐Ÿฝโ€โ™‚๏ธ', + 'man_teacher_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€๐Ÿซ', + 'man_teacher_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€๐Ÿซ', + 'man_teacher_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€๐Ÿซ', + 'man_teacher_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€๐Ÿซ', + 'man_teacher_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€๐Ÿซ', + 'man_technologist_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€๐Ÿ’ป', + 'man_technologist_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป', + 'man_technologist_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€๐Ÿ’ป', + 'man_technologist_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€๐Ÿ’ป', + 'man_technologist_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€๐Ÿ’ป', + 'man_tipping_hand_dark_skin_tone': '๐Ÿ’๐Ÿฟโ€โ™‚๏ธ', + 'man_tipping_hand_light_skin_tone': '๐Ÿ’๐Ÿปโ€โ™‚๏ธ', + 'man_tipping_hand_medium_dark_skin_tone': '๐Ÿ’๐Ÿพโ€โ™‚๏ธ', + 'man_tipping_hand_medium_light_skin_tone': '๐Ÿ’๐Ÿผโ€โ™‚๏ธ', + 'man_tipping_hand_medium_skin_tone': '๐Ÿ’๐Ÿฝโ€โ™‚๏ธ', + 'man_vampire_dark_skin_tone': '๐Ÿง›๐Ÿฟโ€โ™‚๏ธ', + 'man_vampire_light_skin_tone': '๐Ÿง›๐Ÿปโ€โ™‚๏ธ', + 'man_vampire_medium_dark_skin_tone': '๐Ÿง›๐Ÿพโ€โ™‚๏ธ', + 'man_vampire_medium_light_skin_tone': '๐Ÿง›๐Ÿผโ€โ™‚๏ธ', + 'man_vampire_medium_skin_tone': '๐Ÿง›๐Ÿฝโ€โ™‚๏ธ', + 'man_walking_dark_skin_tone': '๐Ÿšถ๐Ÿฟโ€โ™‚๏ธ', + 'man_walking_light_skin_tone': '๐Ÿšถ๐Ÿปโ€โ™‚๏ธ', + 'man_walking_medium_dark_skin_tone': '๐Ÿšถ๐Ÿพโ€โ™‚๏ธ', + 'man_walking_medium_light_skin_tone': '๐Ÿšถ๐Ÿผโ€โ™‚๏ธ', + 'man_walking_medium_skin_tone': '๐Ÿšถ๐Ÿฝโ€โ™‚๏ธ', + 'man_wearing_turban_dark_skin_tone': '๐Ÿ‘ณ๐Ÿฟโ€โ™‚๏ธ', + 'man_wearing_turban_light_skin_tone': '๐Ÿ‘ณ๐Ÿปโ€โ™‚๏ธ', + 'man_wearing_turban_medium_dark_skin_tone': '๐Ÿ‘ณ๐Ÿพโ€โ™‚๏ธ', + 'man_wearing_turban_medium_light_skin_tone': '๐Ÿ‘ณ๐Ÿผโ€โ™‚๏ธ', + 'man_wearing_turban_medium_skin_tone': '๐Ÿ‘ณ๐Ÿฝโ€โ™‚๏ธ', + 'man_white_haired_dark_skin_tone': '๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆณ', + 'man_white_haired_light_skin_tone': '๐Ÿ‘จ๐Ÿปโ€๐Ÿฆณ', + 'man_white_haired_medium_dark_skin_tone': '๐Ÿ‘จ๐Ÿพโ€๐Ÿฆณ', + 'man_white_haired_medium_light_skin_tone': '๐Ÿ‘จ๐Ÿผโ€๐Ÿฆณ', + 'man_white_haired_medium_skin_tone': '๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆณ', + 'man_with_chinese_cap_tone1': '๐Ÿ‘ฒ๐Ÿป', + 'man_with_chinese_cap_tone2': '๐Ÿ‘ฒ๐Ÿผ', + 'man_with_chinese_cap_tone3': '๐Ÿ‘ฒ๐Ÿฝ', + 'man_with_chinese_cap_tone4': '๐Ÿ‘ฒ๐Ÿพ', + 'man_with_chinese_cap_tone5': '๐Ÿ‘ฒ๐Ÿฟ', + 'man_with_chinese_cap': '๐Ÿ‘ฒ', + 'man_with_gua_pi_mao_tone1': '๐Ÿ‘ฒ๐Ÿป', + 'man_with_gua_pi_mao_tone2': '๐Ÿ‘ฒ๐Ÿผ', + 'man_with_gua_pi_mao_tone3': '๐Ÿ‘ฒ๐Ÿฝ', + 'man_with_gua_pi_mao_tone4': '๐Ÿ‘ฒ๐Ÿพ', + 'man_with_gua_pi_mao_tone5': '๐Ÿ‘ฒ๐Ÿฟ', + 'man_with_gua_pi_mao': '๐Ÿ‘ฒ', + 'man_with_turban_tone1': '๐Ÿ‘ณ๐Ÿป', + 'man_with_turban_tone2': '๐Ÿ‘ณ๐Ÿผ', + 'man_with_turban_tone3': '๐Ÿ‘ณ๐Ÿฝ', + 'man_with_turban_tone4': '๐Ÿ‘ณ๐Ÿพ', + 'man_with_turban_tone5': '๐Ÿ‘ณ๐Ÿฟ', + 'man_with_turban': '๐Ÿ‘ณ', + 'mantlepiece_clock': '๐Ÿ•ฐ', + 'map': '๐Ÿ—บ', + 'mc': '๐Ÿ‡ฒ๐Ÿ‡จ', + 'md': '๐Ÿ‡ฒ๐Ÿ‡ฉ', + 'me': '๐Ÿ‡ฒ๐Ÿ‡ช', + 'medal': '๐Ÿ…', + 'mermaid_dark_skin_tone': '๐Ÿงœ๐Ÿฟโ€โ™€๏ธ', + 'mermaid_light_skin_tone': '๐Ÿงœ๐Ÿปโ€โ™€๏ธ', + 'mermaid_medium_dark_skin_tone': '๐Ÿงœ๐Ÿพโ€โ™€๏ธ', + 'mermaid_medium_light_skin_tone': '๐Ÿงœ๐Ÿผโ€โ™€๏ธ', + 'mermaid_medium_skin_tone': '๐Ÿงœ๐Ÿฝโ€โ™€๏ธ', + 'merman_dark_skin_tone': '๐Ÿงœ๐Ÿฟโ€โ™‚๏ธ', + 'merman_light_skin_tone': '๐Ÿงœ๐Ÿปโ€โ™‚๏ธ', + 'merman_medium_dark_skin_tone': '๐Ÿงœ๐Ÿพโ€โ™‚๏ธ', + 'merman_medium_light_skin_tone': '๐Ÿงœ๐Ÿผโ€โ™‚๏ธ', + 'merman_medium_skin_tone': '๐Ÿงœ๐Ÿฝโ€โ™‚๏ธ', + 'merperson_dark_skin_tone': '๐Ÿงœ๐Ÿฟ', + 'merperson_light_skin_tone': '๐Ÿงœ๐Ÿป', + 'merperson_medium_dark_skin_tone': '๐Ÿงœ๐Ÿพ', + 'merperson_medium_light_skin_tone': '๐Ÿงœ๐Ÿผ', + 'merperson_medium_skin_tone': '๐Ÿงœ๐Ÿฝ', + 'mf': '๐Ÿ‡ฒ๐Ÿ‡ซ', + 'mg': '๐Ÿ‡ฒ๐Ÿ‡ฌ', + 'mh': '๐Ÿ‡ฒ๐Ÿ‡ญ', + 'microphone2': '๐ŸŽ™', + 'mk': '๐Ÿ‡ฒ๐Ÿ‡ฐ', + 'ml': '๐Ÿ‡ฒ๐Ÿ‡ฑ', + 'mm': '๐Ÿ‡ฒ๐Ÿ‡ฒ', + 'mn': '๐Ÿ‡ฒ๐Ÿ‡ณ', + 'mo': '๐Ÿ‡ฒ๐Ÿ‡ด', + 'money_mouth': '๐Ÿค‘', + 'mother_christmas_tone1': '๐Ÿคถ๐Ÿป', + 'mother_christmas_tone2': '๐Ÿคถ๐Ÿผ', + 'mother_christmas_tone3': '๐Ÿคถ๐Ÿฝ', + 'mother_christmas_tone4': '๐Ÿคถ๐Ÿพ', + 'mother_christmas_tone5': '๐Ÿคถ๐Ÿฟ', + 'mother_christmas': '๐Ÿคถ', + 'motorbike': '๐Ÿ›ต', + 'mouse_three_button': '๐Ÿ–ฑ', + 'mouse2': '๐Ÿ', + 'mp': '๐Ÿ‡ฒ๐Ÿ‡ต', + 'mq': '๐Ÿ‡ฒ๐Ÿ‡ถ', + 'mr': '๐Ÿ‡ฒ๐Ÿ‡ท', + 'ms': '๐Ÿ‡ฒ๐Ÿ‡ธ', + 'mt': '๐Ÿ‡ฒ๐Ÿ‡น', + 'mu': '๐Ÿ‡ฒ๐Ÿ‡บ', + 'mv': '๐Ÿ‡ฒ๐Ÿ‡ป', + 'mw': '๐Ÿ‡ฒ๐Ÿ‡ผ', + 'mx': '๐Ÿ‡ฒ๐Ÿ‡ฝ', + 'my': '๐Ÿ‡ฒ๐Ÿ‡พ', + 'mz': '๐Ÿ‡ฒ๐Ÿ‡ฟ', + 'na': '๐Ÿ‡ณ๐Ÿ‡ฆ', + 'nc': '๐Ÿ‡ณ๐Ÿ‡จ', + 'ne': '๐Ÿ‡ณ๐Ÿ‡ช', + 'newspaper2': '๐Ÿ—ž', + 'nf': '๐Ÿ‡ณ๐Ÿ‡ซ', + 'ni': '๐Ÿ‡ณ๐Ÿ‡ฎ', + 'nl': '๐Ÿ‡ณ๐Ÿ‡ฑ', + 'np': '๐Ÿ‡ณ๐Ÿ‡ต', + 'nr': '๐Ÿ‡ณ๐Ÿ‡ท', + 'nu': '๐Ÿ‡ณ๐Ÿ‡บ', + 'nz': '๐Ÿ‡ณ๐Ÿ‡ฟ', + 'o2': '๐Ÿ…พ', + 'oil': '๐Ÿ›ข', + 'ok_woman_tone1': '๐Ÿ™†๐Ÿป', + 'ok_woman_tone2': '๐Ÿ™†๐Ÿผ', + 'ok_woman_tone3': '๐Ÿ™†๐Ÿฝ', + 'ok_woman_tone4': '๐Ÿ™†๐Ÿพ', + 'ok_woman_tone5': '๐Ÿ™†๐Ÿฟ', + 'ok_woman': '๐Ÿ™†', + 'older_adult_dark_skin_tone': '๐Ÿง“๐Ÿฟ', + 'older_adult_light_skin_tone': '๐Ÿง“๐Ÿป', + 'older_adult_medium_dark_skin_tone': '๐Ÿง“๐Ÿพ', + 'older_adult_medium_light_skin_tone': '๐Ÿง“๐Ÿผ', + 'older_adult_medium_skin_tone': '๐Ÿง“๐Ÿฝ', + 'om_symbol': '๐Ÿ•‰', + 'pa': '๐Ÿ‡ต๐Ÿ‡ฆ', + 'paella': '๐Ÿฅ˜', + 'palms_up_together_dark_skin_tone': '๐Ÿคฒ๐Ÿฟ', + 'palms_up_together_light_skin_tone': '๐Ÿคฒ๐Ÿป', + 'palms_up_together_medium_dark_skin_tone': '๐Ÿคฒ๐Ÿพ', + 'palms_up_together_medium_light_skin_tone': '๐Ÿคฒ๐Ÿผ', + 'palms_up_together_medium_skin_tone': '๐Ÿคฒ๐Ÿฝ', + 'park': '๐Ÿž', + 'pause_button': 'โธ', + 'pe': '๐Ÿ‡ต๐Ÿ‡ช', + 'pen_ballpoint': '๐Ÿ–Š', + 'pen_fountain': '๐Ÿ–‹', + 'pencil2': 'โœ', + 'person_climbing_dark_skin_tone': '๐Ÿง—๐Ÿฟ', + 'person_climbing_light_skin_tone': '๐Ÿง—๐Ÿป', + 'person_climbing_medium_dark_skin_tone': '๐Ÿง—๐Ÿพ', + 'person_climbing_medium_light_skin_tone': '๐Ÿง—๐Ÿผ', + 'person_climbing_medium_skin_tone': '๐Ÿง—๐Ÿฝ', + 'person_doing_cartwheel_tone1': '๐Ÿคธ๐Ÿป', + 'person_doing_cartwheel_tone2': '๐Ÿคธ๐Ÿผ', + 'person_doing_cartwheel_tone3': '๐Ÿคธ๐Ÿฝ', + 'person_doing_cartwheel_tone4': '๐Ÿคธ๐Ÿพ', + 'person_doing_cartwheel_tone5': '๐Ÿคธ๐Ÿฟ', + 'person_doing_cartwheel': '๐Ÿคธ', + 'person_golfing_dark_skin_tone': '๐ŸŒ๐Ÿฟ', + 'person_golfing_light_skin_tone': '๐ŸŒ๐Ÿป', + 'person_golfing_medium_dark_skin_tone': '๐ŸŒ๐Ÿพ', + 'person_golfing_medium_light_skin_tone': '๐ŸŒ๐Ÿผ', + 'person_golfing_medium_skin_tone': '๐ŸŒ๐Ÿฝ', + 'person_in_bed_dark_skin_tone': '๐Ÿ›Œ๐Ÿฟ', + 'person_in_bed_light_skin_tone': '๐Ÿ›Œ๐Ÿป', + 'person_in_bed_medium_dark_skin_tone': '๐Ÿ›Œ๐Ÿพ', + 'person_in_bed_medium_light_skin_tone': '๐Ÿ›Œ๐Ÿผ', + 'person_in_bed_medium_skin_tone': '๐Ÿ›Œ๐Ÿฝ', + 'person_in_lotus_position_dark_skin_tone': '๐Ÿง˜๐Ÿฟ', + 'person_in_lotus_position_light_skin_tone': '๐Ÿง˜๐Ÿป', + 'person_in_lotus_position_medium_dark_skin_tone': '๐Ÿง˜๐Ÿพ', + 'person_in_lotus_position_medium_light_skin_tone': '๐Ÿง˜๐Ÿผ', + 'person_in_lotus_position_medium_skin_tone': '๐Ÿง˜๐Ÿฝ', + 'person_in_steamy_room_dark_skin_tone': '๐Ÿง–๐Ÿฟ', + 'person_in_steamy_room_light_skin_tone': '๐Ÿง–๐Ÿป', + 'person_in_steamy_room_medium_dark_skin_tone': '๐Ÿง–๐Ÿพ', + 'person_in_steamy_room_medium_light_skin_tone': '๐Ÿง–๐Ÿผ', + 'person_in_steamy_room_medium_skin_tone': '๐Ÿง–๐Ÿฝ', + 'person_with_ball_tone1': 'โ›น๐Ÿป', + 'person_with_ball_tone2': 'โ›น๐Ÿผ', + 'person_with_ball_tone3': 'โ›น๐Ÿฝ', + 'person_with_ball_tone4': 'โ›น๐Ÿพ', + 'person_with_ball_tone5': 'โ›น๐Ÿฟ', + 'person_with_ball': 'โ›น', + 'person_with_blond_hair_tone1': '๐Ÿ‘ฑ๐Ÿป', + 'person_with_blond_hair_tone2': '๐Ÿ‘ฑ๐Ÿผ', + 'person_with_blond_hair_tone3': '๐Ÿ‘ฑ๐Ÿฝ', + 'person_with_blond_hair_tone4': '๐Ÿ‘ฑ๐Ÿพ', + 'person_with_blond_hair_tone5': '๐Ÿ‘ฑ๐Ÿฟ', + 'person_with_blond_hair': '๐Ÿ‘ฑ', + 'person_with_pouting_face_tone1': '๐Ÿ™Ž๐Ÿป', + 'person_with_pouting_face_tone2': '๐Ÿ™Ž๐Ÿผ', + 'person_with_pouting_face_tone3': '๐Ÿ™Ž๐Ÿฝ', + 'person_with_pouting_face_tone4': '๐Ÿ™Ž๐Ÿพ', + 'person_with_pouting_face_tone5': '๐Ÿ™Ž๐Ÿฟ', + 'person_with_pouting_face': '๐Ÿ™Ž', + 'pf': '๐Ÿ‡ต๐Ÿ‡ซ', + 'pg': '๐Ÿ‡ต๐Ÿ‡ฌ', + 'ph': '๐Ÿ‡ต๐Ÿ‡ญ', + 'pig2': '๐Ÿ–', + 'pk': '๐Ÿ‡ต๐Ÿ‡ฐ', + 'pl': '๐Ÿ‡ต๐Ÿ‡ฑ', + 'pm': '๐Ÿ‡ต๐Ÿ‡ฒ', + 'pn': '๐Ÿ‡ต๐Ÿ‡ณ', + 'poo': '๐Ÿ’ฉ', + 'pr': '๐Ÿ‡ต๐Ÿ‡ท', + 'projector': '๐Ÿ“ฝ', + 'ps': '๐Ÿ‡ต๐Ÿ‡ธ', + 'pt': '๐Ÿ‡ต๐Ÿ‡น', + 'pudding': '๐Ÿฎ', + 'pw': '๐Ÿ‡ต๐Ÿ‡ผ', + 'py': '๐Ÿ‡ต๐Ÿ‡พ', + 'qa': '๐Ÿ‡ถ๐Ÿ‡ฆ', + 'rabbit2': '๐Ÿ‡', + 'race_car': '๐ŸŽ', + 'racing_motorcycle': '๐Ÿ', + 'radioactive_sign': 'โ˜ข', + 'railroad_track': '๐Ÿ›ค', + 'raised_hand_with_part_between_middle_and_ring_fingers_tone1': '๐Ÿ––๐Ÿป', + 'raised_hand_with_part_between_middle_and_ring_fingers_tone2': '๐Ÿ––๐Ÿผ', + 'raised_hand_with_part_between_middle_and_ring_fingers_tone3': '๐Ÿ––๐Ÿฝ', + 'raised_hand_with_part_between_middle_and_ring_fingers_tone4': '๐Ÿ––๐Ÿพ', + 'raised_hand_with_part_between_middle_and_ring_fingers_tone5': '๐Ÿ––๐Ÿฟ', + 'raised_hand_with_part_between_middle_and_ring_fingers': '๐Ÿ––', + 'raising_hand_tone1': '๐Ÿ™‹๐Ÿป', + 'raising_hand_tone2': '๐Ÿ™‹๐Ÿผ', + 'raising_hand_tone3': '๐Ÿ™‹๐Ÿฝ', + 'raising_hand_tone4': '๐Ÿ™‹๐Ÿพ', + 'raising_hand_tone5': '๐Ÿ™‹๐Ÿฟ', + 'raising_hand': '๐Ÿ™‹', + 're': '๐Ÿ‡ท๐Ÿ‡ช', + 'record_button': 'โบ', + 'reversed_hand_with_middle_finger_extended_tone1': '๐Ÿ–•๐Ÿป', + 'reversed_hand_with_middle_finger_extended_tone2': '๐Ÿ–•๐Ÿผ', + 'reversed_hand_with_middle_finger_extended_tone3': '๐Ÿ–•๐Ÿฝ', + 'reversed_hand_with_middle_finger_extended_tone4': '๐Ÿ–•๐Ÿพ', + 'reversed_hand_with_middle_finger_extended_tone5': '๐Ÿ–•๐Ÿฟ', + 'reversed_hand_with_middle_finger_extended': '๐Ÿ–•', + 'right_fist_tone1': '๐Ÿคœ๐Ÿป', + 'right_fist_tone2': '๐Ÿคœ๐Ÿผ', + 'right_fist_tone3': '๐Ÿคœ๐Ÿฝ', + 'right_fist_tone4': '๐Ÿคœ๐Ÿพ', + 'right_fist_tone5': '๐Ÿคœ๐Ÿฟ', + 'right_fist': '๐Ÿคœ', + 'ro': '๐Ÿ‡ท๐Ÿ‡ด', + 'rolling_on_the_floor_laughing': '๐Ÿคฃ', + 'rs': '๐Ÿ‡ท๐Ÿ‡ธ', + 'ru': '๐Ÿ‡ท๐Ÿ‡บ', + 'runner_tone1': '๐Ÿƒ๐Ÿป', + 'runner_tone2': '๐Ÿƒ๐Ÿผ', + 'runner_tone3': '๐Ÿƒ๐Ÿฝ', + 'runner_tone4': '๐Ÿƒ๐Ÿพ', + 'runner_tone5': '๐Ÿƒ๐Ÿฟ', + 'runner': '๐Ÿƒ', + 'rw': '๐Ÿ‡ท๐Ÿ‡ผ', + 'sa': '๐Ÿˆ‚', + 'satellite_orbital': '๐Ÿ›ฐ', + 'saudi': '๐Ÿ‡ธ๐Ÿ‡ฆ', + 'saudiarabia': '๐Ÿ‡ธ๐Ÿ‡ฆ', + 'sb': '๐Ÿ‡ธ๐Ÿ‡ง', + 'sc': '๐Ÿ‡ธ๐Ÿ‡จ', + 'sd': '๐Ÿ‡ธ๐Ÿ‡ฉ', + 'se': '๐Ÿ‡ธ๐Ÿ‡ช', + 'second_place': '๐Ÿฅˆ', + 'sg': '๐Ÿ‡ธ๐Ÿ‡ฌ', + 'sh': '๐Ÿ‡ธ๐Ÿ‡ญ', + 'shaking_hands': '๐Ÿค', + 'shelled_peanut': '๐Ÿฅœ', + 'shopping_trolley': '๐Ÿ›’', + 'si': '๐Ÿ‡ธ๐Ÿ‡ฎ', + 'sick': '๐Ÿคข', + 'sj': '๐Ÿ‡ธ๐Ÿ‡ฏ', + 'sk': '๐Ÿ‡ธ๐Ÿ‡ฐ', + 'skeleton': '๐Ÿ’€', + 'skull_crossbones': 'โ˜ ', + 'sl': '๐Ÿ‡ธ๐Ÿ‡ฑ', + 'sleuth_or_spy_tone1': '๐Ÿ•ต๐Ÿป', + 'sleuth_or_spy_tone2': '๐Ÿ•ต๐Ÿผ', + 'sleuth_or_spy_tone3': '๐Ÿ•ต๐Ÿฝ', + 'sleuth_or_spy_tone4': '๐Ÿ•ต๐Ÿพ', + 'sleuth_or_spy_tone5': '๐Ÿ•ต๐Ÿฟ', + 'sleuth_or_spy': '๐Ÿ•ต', + 'slight_frown': '๐Ÿ™', + 'slight_smile': '๐Ÿ™‚', + 'sm': '๐Ÿ‡ธ๐Ÿ‡ฒ', + 'sn': '๐Ÿ‡ธ๐Ÿ‡ณ', + 'sneeze': '๐Ÿคง', + 'snow_capped_mountain': '๐Ÿ”', + 'snowboarder_dark_skin_tone': '๐Ÿ‚๐Ÿฟ', + 'snowboarder_light_skin_tone': '๐Ÿ‚๐Ÿป', + 'snowboarder_medium_dark_skin_tone': '๐Ÿ‚๐Ÿพ', + 'snowboarder_medium_light_skin_tone': '๐Ÿ‚๐Ÿผ', + 'snowboarder_medium_skin_tone': '๐Ÿ‚๐Ÿฝ', + 'so': '๐Ÿ‡ธ๐Ÿ‡ด', + 'speaking_head_in_silhouette': '๐Ÿ—ฃ', + 'speech_left': '๐Ÿ—จ', + 'spiral_calendar_pad': '๐Ÿ—“', + 'spiral_note_pad': '๐Ÿ—’', + 'spy_tone1': '๐Ÿ•ต๐Ÿป', + 'spy_tone2': '๐Ÿ•ต๐Ÿผ', + 'spy_tone3': '๐Ÿ•ต๐Ÿฝ', + 'spy_tone4': '๐Ÿ•ต๐Ÿพ', + 'spy_tone5': '๐Ÿ•ต๐Ÿฟ', + 'spy': '๐Ÿ•ต', + 'squeeze_bottle': '๐Ÿงด', + 'sr': '๐Ÿ‡ธ๐Ÿ‡ท', + 'ss': '๐Ÿ‡ธ๐Ÿ‡ธ', + 'st': '๐Ÿ‡ธ๐Ÿ‡น', + 'stop_button': 'โน', + 'stuffed_pita': '๐Ÿฅ™', + 'superhero_dark_skin_tone': '๐Ÿฆธ๐Ÿฟ', + 'superhero_light_skin_tone': '๐Ÿฆธ๐Ÿป', + 'superhero_medium_dark_skin_tone': '๐Ÿฆธ๐Ÿพ', + 'superhero_medium_light_skin_tone': '๐Ÿฆธ๐Ÿผ', + 'superhero_medium_skin_tone': '๐Ÿฆธ๐Ÿฝ', + 'supervillain_dark_skin_tone': '๐Ÿฆน๐Ÿฟ', + 'supervillain_light_skin_tone': '๐Ÿฆน๐Ÿป', + 'supervillain_medium_dark_skin_tone': '๐Ÿฆน๐Ÿพ', + 'supervillain_medium_light_skin_tone': '๐Ÿฆน๐Ÿผ', + 'supervillain_medium_skin_tone': '๐Ÿฆน๐Ÿฝ', + 'sv': '๐Ÿ‡ธ๐Ÿ‡ป', + 'sx': '๐Ÿ‡ธ๐Ÿ‡ฝ', + 'sy': '๐Ÿ‡ธ๐Ÿ‡พ', + 'sz': '๐Ÿ‡ธ๐Ÿ‡ฟ', + 't_rex': '๐Ÿฆ–', + 'ta': '๐Ÿ‡น๐Ÿ‡ฆ', + 'table_tennis': '๐Ÿ“', + 'tc': '๐Ÿ‡น๐Ÿ‡จ', + 'td': '๐Ÿ‡น๐Ÿ‡ฉ', + 'tf': '๐Ÿ‡น๐Ÿ‡ซ', + 'tg': '๐Ÿ‡น๐Ÿ‡ฌ', + 'th': '๐Ÿ‡น๐Ÿ‡ญ', + 'thermometer_face': '๐Ÿค’', + 'third_place': '๐Ÿฅ‰', + 'three_button_mouse': '๐Ÿ–ฑ', + 'thumbdown_tone1': '๐Ÿ‘Ž๐Ÿป', + 'thumbdown_tone2': '๐Ÿ‘Ž๐Ÿผ', + 'thumbdown_tone3': '๐Ÿ‘Ž๐Ÿฝ', + 'thumbdown_tone4': '๐Ÿ‘Ž๐Ÿพ', + 'thumbdown_tone5': '๐Ÿ‘Ž๐Ÿฟ', + 'thumbdown': '๐Ÿ‘Ž', + 'thumbup_tone1': '๐Ÿ‘๐Ÿป', + 'thumbup_tone2': '๐Ÿ‘๐Ÿผ', + 'thumbup_tone3': '๐Ÿ‘๐Ÿฝ', + 'thumbup_tone4': '๐Ÿ‘๐Ÿพ', + 'thumbup_tone5': '๐Ÿ‘๐Ÿฟ', + 'thumbup': '๐Ÿ‘', + 'thunder_cloud_rain': 'โ›ˆ', + 'tiger2': '๐Ÿ…', + 'timer': 'โฒ', + 'tj': '๐Ÿ‡น๐Ÿ‡ฏ', + 'tk': '๐Ÿ‡น๐Ÿ‡ฐ', + 'tl': '๐Ÿ‡น๐Ÿ‡ฑ', + 'tn': '๐Ÿ‡น๐Ÿ‡ณ', + 'to': '๐Ÿ‡น๐Ÿ‡ด', + 'tools': '๐Ÿ› ', + 'tr': '๐Ÿ‡น๐Ÿ‡ท', + 'track_next': 'โญ', + 'track_previous': 'โฎ', + 'train2': '๐Ÿš†', + 'tt': '๐Ÿ‡น๐Ÿ‡น', + 'tuxedo_tone1': '๐Ÿคต๐Ÿป', + 'tuxedo_tone2': '๐Ÿคต๐Ÿผ', + 'tuxedo_tone3': '๐Ÿคต๐Ÿฝ', + 'tuxedo_tone4': '๐Ÿคต๐Ÿพ', + 'tuxedo_tone5': '๐Ÿคต๐Ÿฟ', + 'tw': '๐Ÿ‡น๐Ÿ‡ผ', + 'tz': '๐Ÿ‡น๐Ÿ‡ฟ', + 'u5272': '๐Ÿˆน', + 'u5408': '๐Ÿˆด', + 'u55b6': '๐Ÿˆบ', + 'u6307': '๐Ÿˆฏ', + 'u6708': '๐Ÿˆท', + 'u6709': '๐Ÿˆถ', + 'u6e80': '๐Ÿˆต', + 'u7121': '๐Ÿˆš', + 'u7533': '๐Ÿˆธ', + 'u7981': '๐Ÿˆฒ', + 'u7a7a': '๐Ÿˆณ', + 'ua': '๐Ÿ‡บ๐Ÿ‡ฆ', + 'ug': '๐Ÿ‡บ๐Ÿ‡ฌ', + 'um': '๐Ÿ‡บ๐Ÿ‡ฒ', + 'umbrella2': 'โ˜‚', + 'upside_down': '๐Ÿ™ƒ', + 'urn': 'โšฑ', + 'us': '๐Ÿ‡บ๐Ÿ‡ธ', + 'uy': '๐Ÿ‡บ๐Ÿ‡พ', + 'uz': '๐Ÿ‡บ๐Ÿ‡ฟ', + 'va': '๐Ÿ‡ป๐Ÿ‡ฆ', + 'vampire_dark_skin_tone': '๐Ÿง›๐Ÿฟ', + 'vampire_light_skin_tone': '๐Ÿง›๐Ÿป', + 'vampire_medium_dark_skin_tone': '๐Ÿง›๐Ÿพ', + 'vampire_medium_light_skin_tone': '๐Ÿง›๐Ÿผ', + 'vampire_medium_skin_tone': '๐Ÿง›๐Ÿฝ', + 'vc': '๐Ÿ‡ป๐Ÿ‡จ', + 've': '๐Ÿ‡ป๐Ÿ‡ช', + 'vg': '๐Ÿ‡ป๐Ÿ‡ฌ', + 'vi': '๐Ÿ‡ป๐Ÿ‡ฎ', + 'vn': '๐Ÿ‡ป๐Ÿ‡ณ', + 'vu': '๐Ÿ‡ป๐Ÿ‡บ', + 'waving_black_flag': '๐Ÿด', + 'waving_white_flag': '๐Ÿณ', + 'wf': '๐Ÿ‡ผ๐Ÿ‡ซ', + 'whale2': '๐Ÿ‹', + 'white_sun_behind_cloud_with_rain': '๐ŸŒฆ', + 'white_sun_behind_cloud': '๐ŸŒฅ', + 'white_sun_cloud': '๐ŸŒฅ', + 'white_sun_rain_cloud': '๐ŸŒฆ', + 'white_sun_small_cloud': '๐ŸŒค', + 'white_sun_with_small_cloud': '๐ŸŒค', + 'wilted_rose': '๐Ÿฅ€', + 'woman_artist_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€๐ŸŽจ', + 'woman_artist_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€๐ŸŽจ', + 'woman_artist_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€๐ŸŽจ', + 'woman_artist_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€๐ŸŽจ', + 'woman_artist_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€๐ŸŽจ', + 'woman_astronaut_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿš€', + 'woman_astronaut_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€๐Ÿš€', + 'woman_astronaut_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€๐Ÿš€', + 'woman_astronaut_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€๐Ÿš€', + 'woman_astronaut_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿš€', + 'woman_bald_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆฒ', + 'woman_bald_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆฒ', + 'woman_bald_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆฒ', + 'woman_bald_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆฒ', + 'woman_bald_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆฒ', + 'woman_biking_dark_skin_tone': '๐Ÿšด๐Ÿฟโ€โ™€๏ธ', + 'woman_biking_light_skin_tone': '๐Ÿšด๐Ÿปโ€โ™€๏ธ', + 'woman_biking_medium_dark_skin_tone': '๐Ÿšด๐Ÿพโ€โ™€๏ธ', + 'woman_biking_medium_light_skin_tone': '๐Ÿšด๐Ÿผโ€โ™€๏ธ', + 'woman_biking_medium_skin_tone': '๐Ÿšด๐Ÿฝโ€โ™€๏ธ', + 'woman_bouncing_ball_dark_skin_tone': 'โ›น๐Ÿฟโ€โ™€๏ธ', + 'woman_bouncing_ball_light_skin_tone': 'โ›น๐Ÿปโ€โ™€๏ธ', + 'woman_bouncing_ball_medium_dark_skin_tone': 'โ›น๐Ÿพโ€โ™€๏ธ', + 'woman_bouncing_ball_medium_light_skin_tone': 'โ›น๐Ÿผโ€โ™€๏ธ', + 'woman_bouncing_ball_medium_skin_tone': 'โ›น๐Ÿฝโ€โ™€๏ธ', + 'woman_bowing_dark_skin_tone': '๐Ÿ™‡๐Ÿฟโ€โ™€๏ธ', + 'woman_bowing_light_skin_tone': '๐Ÿ™‡๐Ÿปโ€โ™€๏ธ', + 'woman_bowing_medium_dark_skin_tone': '๐Ÿ™‡๐Ÿพโ€โ™€๏ธ', + 'woman_bowing_medium_light_skin_tone': '๐Ÿ™‡๐Ÿผโ€โ™€๏ธ', + 'woman_bowing_medium_skin_tone': '๐Ÿ™‡๐Ÿฝโ€โ™€๏ธ', + 'woman_cartwheeling_dark_skin_tone': '๐Ÿคธ๐Ÿฟโ€โ™€๏ธ', + 'woman_cartwheeling_light_skin_tone': '๐Ÿคธ๐Ÿปโ€โ™€๏ธ', + 'woman_cartwheeling_medium_dark_skin_tone': '๐Ÿคธ๐Ÿพโ€โ™€๏ธ', + 'woman_cartwheeling_medium_light_skin_tone': '๐Ÿคธ๐Ÿผโ€โ™€๏ธ', + 'woman_cartwheeling_medium_skin_tone': '๐Ÿคธ๐Ÿฝโ€โ™€๏ธ', + 'woman_climbing_dark_skin_tone': '๐Ÿง—๐Ÿฟโ€โ™€๏ธ', + 'woman_climbing_light_skin_tone': '๐Ÿง—๐Ÿปโ€โ™€๏ธ', + 'woman_climbing_medium_dark_skin_tone': '๐Ÿง—๐Ÿพโ€โ™€๏ธ', + 'woman_climbing_medium_light_skin_tone': '๐Ÿง—๐Ÿผโ€โ™€๏ธ', + 'woman_climbing_medium_skin_tone': '๐Ÿง—๐Ÿฝโ€โ™€๏ธ', + 'woman_construction_worker_dark_skin_tone': '๐Ÿ‘ท๐Ÿฟโ€โ™€๏ธ', + 'woman_construction_worker_light_skin_tone': '๐Ÿ‘ท๐Ÿปโ€โ™€๏ธ', + 'woman_construction_worker_medium_dark_skin_tone': '๐Ÿ‘ท๐Ÿพโ€โ™€๏ธ', + 'woman_construction_worker_medium_light_skin_tone': '๐Ÿ‘ท๐Ÿผโ€โ™€๏ธ', + 'woman_construction_worker_medium_skin_tone': '๐Ÿ‘ท๐Ÿฝโ€โ™€๏ธ', + 'woman_cook_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿณ', + 'woman_cook_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€๐Ÿณ', + 'woman_cook_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€๐Ÿณ', + 'woman_cook_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€๐Ÿณ', + 'woman_cook_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿณ', + 'woman_curly_haired_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆฑ', + 'woman_curly_haired_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆฑ', + 'woman_curly_haired_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆฑ', + 'woman_curly_haired_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆฑ', + 'woman_curly_haired_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆฑ', + 'woman_detective_dark_skin_tone': '๐Ÿ•ต๐Ÿฟโ€โ™€๏ธ', + 'woman_detective_light_skin_tone': '๐Ÿ•ต๐Ÿปโ€โ™€๏ธ', + 'woman_detective_medium_dark_skin_tone': '๐Ÿ•ต๐Ÿพโ€โ™€๏ธ', + 'woman_detective_medium_light_skin_tone': '๐Ÿ•ต๐Ÿผโ€โ™€๏ธ', + 'woman_detective_medium_skin_tone': '๐Ÿ•ต๐Ÿฝโ€โ™€๏ธ', + 'woman_elf_dark_skin_tone': '๐Ÿง๐Ÿฟโ€โ™€๏ธ', + 'woman_elf_light_skin_tone': '๐Ÿง๐Ÿปโ€โ™€๏ธ', + 'woman_elf_medium_dark_skin_tone': '๐Ÿง๐Ÿพโ€โ™€๏ธ', + 'woman_elf_medium_light_skin_tone': '๐Ÿง๐Ÿผโ€โ™€๏ธ', + 'woman_elf_medium_skin_tone': '๐Ÿง๐Ÿฝโ€โ™€๏ธ', + 'woman_facepalming_dark_skin_tone': '๐Ÿคฆ๐Ÿฟโ€โ™€๏ธ', + 'woman_facepalming_light_skin_tone': '๐Ÿคฆ๐Ÿปโ€โ™€๏ธ', + 'woman_facepalming_medium_dark_skin_tone': '๐Ÿคฆ๐Ÿพโ€โ™€๏ธ', + 'woman_facepalming_medium_light_skin_tone': '๐Ÿคฆ๐Ÿผโ€โ™€๏ธ', + 'woman_facepalming_medium_skin_tone': '๐Ÿคฆ๐Ÿฝโ€โ™€๏ธ', + 'woman_factory_worker_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿญ', + 'woman_factory_worker_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€๐Ÿญ', + 'woman_factory_worker_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€๐Ÿญ', + 'woman_factory_worker_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€๐Ÿญ', + 'woman_factory_worker_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿญ', + 'woman_fairy_dark_skin_tone': '๐Ÿงš๐Ÿฟโ€โ™€๏ธ', + 'woman_fairy_light_skin_tone': '๐Ÿงš๐Ÿปโ€โ™€๏ธ', + 'woman_fairy_medium_dark_skin_tone': '๐Ÿงš๐Ÿพโ€โ™€๏ธ', + 'woman_fairy_medium_light_skin_tone': '๐Ÿงš๐Ÿผโ€โ™€๏ธ', + 'woman_fairy_medium_skin_tone': '๐Ÿงš๐Ÿฝโ€โ™€๏ธ', + 'woman_farmer_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€๐ŸŒพ', + 'woman_farmer_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ', + 'woman_farmer_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€๐ŸŒพ', + 'woman_farmer_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€๐ŸŒพ', + 'woman_farmer_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€๐ŸŒพ', + 'woman_firefighter_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿš’', + 'woman_firefighter_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€๐Ÿš’', + 'woman_firefighter_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€๐Ÿš’', + 'woman_firefighter_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€๐Ÿš’', + 'woman_firefighter_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿš’', + 'woman_frowning_dark_skin_tone': '๐Ÿ™๐Ÿฟโ€โ™€๏ธ', + 'woman_frowning_light_skin_tone': '๐Ÿ™๐Ÿปโ€โ™€๏ธ', + 'woman_frowning_medium_dark_skin_tone': '๐Ÿ™๐Ÿพโ€โ™€๏ธ', + 'woman_frowning_medium_light_skin_tone': '๐Ÿ™๐Ÿผโ€โ™€๏ธ', + 'woman_frowning_medium_skin_tone': '๐Ÿ™๐Ÿฝโ€โ™€๏ธ', + 'woman_gesturing_no_dark_skin_tone': '๐Ÿ™…๐Ÿฟโ€โ™€๏ธ', + 'woman_gesturing_no_light_skin_tone': '๐Ÿ™…๐Ÿปโ€โ™€๏ธ', + 'woman_gesturing_no_medium_dark_skin_tone': '๐Ÿ™…๐Ÿพโ€โ™€๏ธ', + 'woman_gesturing_no_medium_light_skin_tone': '๐Ÿ™…๐Ÿผโ€โ™€๏ธ', + 'woman_gesturing_no_medium_skin_tone': '๐Ÿ™…๐Ÿฝโ€โ™€๏ธ', + 'woman_gesturing_ok_dark_skin_tone': '๐Ÿ™†๐Ÿฟโ€โ™€๏ธ', + 'woman_gesturing_ok_light_skin_tone': '๐Ÿ™†๐Ÿปโ€โ™€๏ธ', + 'woman_gesturing_ok_medium_dark_skin_tone': '๐Ÿ™†๐Ÿพโ€โ™€๏ธ', + 'woman_gesturing_ok_medium_light_skin_tone': '๐Ÿ™†๐Ÿผโ€โ™€๏ธ', + 'woman_gesturing_ok_medium_skin_tone': '๐Ÿ™†๐Ÿฝโ€โ™€๏ธ', + 'woman_getting_face_massage_dark_skin_tone': '๐Ÿ’†๐Ÿฟโ€โ™€๏ธ', + 'woman_getting_face_massage_light_skin_tone': '๐Ÿ’†๐Ÿปโ€โ™€๏ธ', + 'woman_getting_face_massage_medium_dark_skin_tone': '๐Ÿ’†๐Ÿพโ€โ™€๏ธ', + 'woman_getting_face_massage_medium_light_skin_tone': '๐Ÿ’†๐Ÿผโ€โ™€๏ธ', + 'woman_getting_face_massage_medium_skin_tone': '๐Ÿ’†๐Ÿฝโ€โ™€๏ธ', + 'woman_getting_face_massage_tone1': '๐Ÿ’†๐Ÿปโ€โ™€๏ธ', + 'woman_getting_face_massage_tone2': '๐Ÿ’†๐Ÿผโ€โ™€๏ธ', + 'woman_getting_face_massage_tone3': '๐Ÿ’†๐Ÿฝโ€โ™€๏ธ', + 'woman_getting_face_massage_tone4': '๐Ÿ’†๐Ÿพโ€โ™€๏ธ', + 'woman_getting_face_massage_tone5': '๐Ÿ’†๐Ÿฟโ€โ™€๏ธ', + 'woman_getting_face_massage': '๐Ÿ’†โ€โ™€๏ธ', + 'woman_getting_haircut_dark_skin_tone': '๐Ÿ’‡๐Ÿฟโ€โ™€๏ธ', + 'woman_getting_haircut_light_skin_tone': '๐Ÿ’‡๐Ÿปโ€โ™€๏ธ', + 'woman_getting_haircut_medium_dark_skin_tone': '๐Ÿ’‡๐Ÿพโ€โ™€๏ธ', + 'woman_getting_haircut_medium_light_skin_tone': '๐Ÿ’‡๐Ÿผโ€โ™€๏ธ', + 'woman_getting_haircut_medium_skin_tone': '๐Ÿ’‡๐Ÿฝโ€โ™€๏ธ', + 'woman_golfing_dark_skin_tone': '๐ŸŒ๐Ÿฟโ€โ™€๏ธ', + 'woman_golfing_light_skin_tone': '๐ŸŒ๐Ÿปโ€โ™€๏ธ', + 'woman_golfing_medium_dark_skin_tone': '๐ŸŒ๐Ÿพโ€โ™€๏ธ', + 'woman_golfing_medium_light_skin_tone': '๐ŸŒ๐Ÿผโ€โ™€๏ธ', + 'woman_golfing_medium_skin_tone': '๐ŸŒ๐Ÿฝโ€โ™€๏ธ', + 'woman_guard_dark_skin_tone': '๐Ÿ’‚๐Ÿฟโ€โ™€๏ธ', + 'woman_guard_light_skin_tone': '๐Ÿ’‚๐Ÿปโ€โ™€๏ธ', + 'woman_guard_medium_dark_skin_tone': '๐Ÿ’‚๐Ÿพโ€โ™€๏ธ', + 'woman_guard_medium_light_skin_tone': '๐Ÿ’‚๐Ÿผโ€โ™€๏ธ', + 'woman_guard_medium_skin_tone': '๐Ÿ’‚๐Ÿฝโ€โ™€๏ธ', + 'woman_health_worker_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€โš•๏ธ', + 'woman_health_worker_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€โš•๏ธ', + 'woman_health_worker_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€โš•๏ธ', + 'woman_health_worker_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€โš•๏ธ', + 'woman_health_worker_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€โš•๏ธ', + 'woman_in_lotus_position_dark_skin_tone': '๐Ÿง˜๐Ÿฟโ€โ™€๏ธ', + 'woman_in_lotus_position_light_skin_tone': '๐Ÿง˜๐Ÿปโ€โ™€๏ธ', + 'woman_in_lotus_position_medium_dark_skin_tone': '๐Ÿง˜๐Ÿพโ€โ™€๏ธ', + 'woman_in_lotus_position_medium_light_skin_tone': '๐Ÿง˜๐Ÿผโ€โ™€๏ธ', + 'woman_in_lotus_position_medium_skin_tone': '๐Ÿง˜๐Ÿฝโ€โ™€๏ธ', + 'woman_in_steamy_room_dark_skin_tone': '๐Ÿง–๐Ÿฟโ€โ™€๏ธ', + 'woman_in_steamy_room_light_skin_tone': '๐Ÿง–๐Ÿปโ€โ™€๏ธ', + 'woman_in_steamy_room_medium_dark_skin_tone': '๐Ÿง–๐Ÿพโ€โ™€๏ธ', + 'woman_in_steamy_room_medium_light_skin_tone': '๐Ÿง–๐Ÿผโ€โ™€๏ธ', + 'woman_in_steamy_room_medium_skin_tone': '๐Ÿง–๐Ÿฝโ€โ™€๏ธ', + 'woman_judge_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€โš–๏ธ', + 'woman_judge_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€โš–๏ธ', + 'woman_judge_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€โš–๏ธ', + 'woman_judge_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€โš–๏ธ', + 'woman_judge_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€โš–๏ธ', + 'woman_juggling_dark_skin_tone': '๐Ÿคน๐Ÿฟโ€โ™€๏ธ', + 'woman_juggling_light_skin_tone': '๐Ÿคน๐Ÿปโ€โ™€๏ธ', + 'woman_juggling_medium_dark_skin_tone': '๐Ÿคน๐Ÿพโ€โ™€๏ธ', + 'woman_juggling_medium_light_skin_tone': '๐Ÿคน๐Ÿผโ€โ™€๏ธ', + 'woman_juggling_medium_skin_tone': '๐Ÿคน๐Ÿฝโ€โ™€๏ธ', + 'woman_lifting_weights_dark_skin_tone': '๐Ÿ‹๐Ÿฟโ€โ™€๏ธ', + 'woman_lifting_weights_light_skin_tone': '๐Ÿ‹๐Ÿปโ€โ™€๏ธ', + 'woman_lifting_weights_medium_dark_skin_tone': '๐Ÿ‹๐Ÿพโ€โ™€๏ธ', + 'woman_lifting_weights_medium_light_skin_tone': '๐Ÿ‹๐Ÿผโ€โ™€๏ธ', + 'woman_lifting_weights_medium_skin_tone': '๐Ÿ‹๐Ÿฝโ€โ™€๏ธ', + 'woman_mage_dark_skin_tone': '๐Ÿง™๐Ÿฟโ€โ™€๏ธ', + 'woman_mage_light_skin_tone': '๐Ÿง™๐Ÿปโ€โ™€๏ธ', + 'woman_mage_medium_dark_skin_tone': '๐Ÿง™๐Ÿพโ€โ™€๏ธ', + 'woman_mage_medium_light_skin_tone': '๐Ÿง™๐Ÿผโ€โ™€๏ธ', + 'woman_mage_medium_skin_tone': '๐Ÿง™๐Ÿฝโ€โ™€๏ธ', + 'woman_mechanic_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿ”ง', + 'woman_mechanic_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ”ง', + 'woman_mechanic_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€๐Ÿ”ง', + 'woman_mechanic_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€๐Ÿ”ง', + 'woman_mechanic_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ”ง', + 'woman_mountain_biking_dark_skin_tone': '๐Ÿšต๐Ÿฟโ€โ™€๏ธ', + 'woman_mountain_biking_light_skin_tone': '๐Ÿšต๐Ÿปโ€โ™€๏ธ', + 'woman_mountain_biking_medium_dark_skin_tone': '๐Ÿšต๐Ÿพโ€โ™€๏ธ', + 'woman_mountain_biking_medium_light_skin_tone': '๐Ÿšต๐Ÿผโ€โ™€๏ธ', + 'woman_mountain_biking_medium_skin_tone': '๐Ÿšต๐Ÿฝโ€โ™€๏ธ', + 'woman_office_worker_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿ’ผ', + 'woman_office_worker_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ’ผ', + 'woman_office_worker_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€๐Ÿ’ผ', + 'woman_office_worker_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€๐Ÿ’ผ', + 'woman_office_worker_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ผ', + 'woman_pilot_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€โœˆ๏ธ', + 'woman_pilot_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€โœˆ๏ธ', + 'woman_pilot_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€โœˆ๏ธ', + 'woman_pilot_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€โœˆ๏ธ', + 'woman_pilot_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€โœˆ๏ธ', + 'woman_playing_handball_dark_skin_tone': '๐Ÿคพ๐Ÿฟโ€โ™€๏ธ', + 'woman_playing_handball_light_skin_tone': '๐Ÿคพ๐Ÿปโ€โ™€๏ธ', + 'woman_playing_handball_medium_dark_skin_tone': '๐Ÿคพ๐Ÿพโ€โ™€๏ธ', + 'woman_playing_handball_medium_light_skin_tone': '๐Ÿคพ๐Ÿผโ€โ™€๏ธ', + 'woman_playing_handball_medium_skin_tone': '๐Ÿคพ๐Ÿฝโ€โ™€๏ธ', + 'woman_playing_water_polo_dark_skin_tone': '๐Ÿคฝ๐Ÿฟโ€โ™€๏ธ', + 'woman_playing_water_polo_light_skin_tone': '๐Ÿคฝ๐Ÿปโ€โ™€๏ธ', + 'woman_playing_water_polo_medium_dark_skin_tone': '๐Ÿคฝ๐Ÿพโ€โ™€๏ธ', + 'woman_playing_water_polo_medium_light_skin_tone': '๐Ÿคฝ๐Ÿผโ€โ™€๏ธ', + 'woman_playing_water_polo_medium_skin_tone': '๐Ÿคฝ๐Ÿฝโ€โ™€๏ธ', + 'woman_police_officer_dark_skin_tone': '๐Ÿ‘ฎ๐Ÿฟโ€โ™€๏ธ', + 'woman_police_officer_light_skin_tone': '๐Ÿ‘ฎ๐Ÿปโ€โ™€๏ธ', + 'woman_police_officer_medium_dark_skin_tone': '๐Ÿ‘ฎ๐Ÿพโ€โ™€๏ธ', + 'woman_police_officer_medium_light_skin_tone': '๐Ÿ‘ฎ๐Ÿผโ€โ™€๏ธ', + 'woman_police_officer_medium_skin_tone': '๐Ÿ‘ฎ๐Ÿฝโ€โ™€๏ธ', + 'woman_pouting_dark_skin_tone': '๐Ÿ™Ž๐Ÿฟโ€โ™€๏ธ', + 'woman_pouting_light_skin_tone': '๐Ÿ™Ž๐Ÿปโ€โ™€๏ธ', + 'woman_pouting_medium_dark_skin_tone': '๐Ÿ™Ž๐Ÿพโ€โ™€๏ธ', + 'woman_pouting_medium_light_skin_tone': '๐Ÿ™Ž๐Ÿผโ€โ™€๏ธ', + 'woman_pouting_medium_skin_tone': '๐Ÿ™Ž๐Ÿฝโ€โ™€๏ธ', + 'woman_raising_hand_dark_skin_tone': '๐Ÿ™‹๐Ÿฟโ€โ™€๏ธ', + 'woman_raising_hand_light_skin_tone': '๐Ÿ™‹๐Ÿปโ€โ™€๏ธ', + 'woman_raising_hand_medium_dark_skin_tone': '๐Ÿ™‹๐Ÿพโ€โ™€๏ธ', + 'woman_raising_hand_medium_light_skin_tone': '๐Ÿ™‹๐Ÿผโ€โ™€๏ธ', + 'woman_raising_hand_medium_skin_tone': '๐Ÿ™‹๐Ÿฝโ€โ™€๏ธ', + 'woman_red_haired_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆฐ', + 'woman_red_haired_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆฐ', + 'woman_red_haired_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆฐ', + 'woman_red_haired_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆฐ', + 'woman_red_haired_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆฐ', + 'woman_rowing_boat_dark_skin_tone': '๐Ÿšฃ๐Ÿฟโ€โ™€๏ธ', + 'woman_rowing_boat_light_skin_tone': '๐Ÿšฃ๐Ÿปโ€โ™€๏ธ', + 'woman_rowing_boat_medium_dark_skin_tone': '๐Ÿšฃ๐Ÿพโ€โ™€๏ธ', + 'woman_rowing_boat_medium_light_skin_tone': '๐Ÿšฃ๐Ÿผโ€โ™€๏ธ', + 'woman_rowing_boat_medium_skin_tone': '๐Ÿšฃ๐Ÿฝโ€โ™€๏ธ', + 'woman_running_dark_skin_tone': '๐Ÿƒ๐Ÿฟโ€โ™€๏ธ', + 'woman_running_light_skin_tone': '๐Ÿƒ๐Ÿปโ€โ™€๏ธ', + 'woman_running_medium_dark_skin_tone': '๐Ÿƒ๐Ÿพโ€โ™€๏ธ', + 'woman_running_medium_light_skin_tone': '๐Ÿƒ๐Ÿผโ€โ™€๏ธ', + 'woman_running_medium_skin_tone': '๐Ÿƒ๐Ÿฝโ€โ™€๏ธ', + 'woman_scientist_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿ”ฌ', + 'woman_scientist_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ”ฌ', + 'woman_scientist_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€๐Ÿ”ฌ', + 'woman_scientist_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€๐Ÿ”ฌ', + 'woman_scientist_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ”ฌ', + 'woman_shrugging_dark_skin_tone': '๐Ÿคท๐Ÿฟโ€โ™€๏ธ', + 'woman_shrugging_light_skin_tone': '๐Ÿคท๐Ÿปโ€โ™€๏ธ', + 'woman_shrugging_medium_dark_skin_tone': '๐Ÿคท๐Ÿพโ€โ™€๏ธ', + 'woman_shrugging_medium_light_skin_tone': '๐Ÿคท๐Ÿผโ€โ™€๏ธ', + 'woman_shrugging_medium_skin_tone': '๐Ÿคท๐Ÿฝโ€โ™€๏ธ', + 'woman_singer_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€๐ŸŽค', + 'woman_singer_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€๐ŸŽค', + 'woman_singer_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€๐ŸŽค', + 'woman_singer_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€๐ŸŽค', + 'woman_singer_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€๐ŸŽค', + 'woman_student_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€๐ŸŽ“', + 'woman_student_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€๐ŸŽ“', + 'woman_student_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€๐ŸŽ“', + 'woman_student_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€๐ŸŽ“', + 'woman_student_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€๐ŸŽ“', + 'woman_superhero_dark_skin_tone': '๐Ÿฆธ๐Ÿฟโ€โ™€๏ธ', + 'woman_superhero_light_skin_tone': '๐Ÿฆธ๐Ÿปโ€โ™€๏ธ', + 'woman_superhero_medium_dark_skin_tone': '๐Ÿฆธ๐Ÿพโ€โ™€๏ธ', + 'woman_superhero_medium_light_skin_tone': '๐Ÿฆธ๐Ÿผโ€โ™€๏ธ', + 'woman_superhero_medium_skin_tone': '๐Ÿฆธ๐Ÿฝโ€โ™€๏ธ', + 'woman_supervillain_dark_skin_tone': '๐Ÿฆน๐Ÿฟโ€โ™€๏ธ', + 'woman_supervillain_light_skin_tone': '๐Ÿฆน๐Ÿปโ€โ™€๏ธ', + 'woman_supervillain_medium_dark_skin_tone': '๐Ÿฆน๐Ÿพโ€โ™€๏ธ', + 'woman_supervillain_medium_light_skin_tone': '๐Ÿฆน๐Ÿผโ€โ™€๏ธ', + 'woman_supervillain_medium_skin_tone': '๐Ÿฆน๐Ÿฝโ€โ™€๏ธ', + 'woman_surfing_dark_skin_tone': '๐Ÿ„๐Ÿฟโ€โ™€๏ธ', + 'woman_surfing_light_skin_tone': '๐Ÿ„๐Ÿปโ€โ™€๏ธ', + 'woman_surfing_medium_dark_skin_tone': '๐Ÿ„๐Ÿพโ€โ™€๏ธ', + 'woman_surfing_medium_light_skin_tone': '๐Ÿ„๐Ÿผโ€โ™€๏ธ', + 'woman_surfing_medium_skin_tone': '๐Ÿ„๐Ÿฝโ€โ™€๏ธ', + 'woman_swimming_dark_skin_tone': '๐ŸŠ๐Ÿฟโ€โ™€๏ธ', + 'woman_swimming_light_skin_tone': '๐ŸŠ๐Ÿปโ€โ™€๏ธ', + 'woman_swimming_medium_dark_skin_tone': '๐ŸŠ๐Ÿพโ€โ™€๏ธ', + 'woman_swimming_medium_light_skin_tone': '๐ŸŠ๐Ÿผโ€โ™€๏ธ', + 'woman_swimming_medium_skin_tone': '๐ŸŠ๐Ÿฝโ€โ™€๏ธ', + 'woman_teacher_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿซ', + 'woman_teacher_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€๐Ÿซ', + 'woman_teacher_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€๐Ÿซ', + 'woman_teacher_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€๐Ÿซ', + 'woman_teacher_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿซ', + 'woman_technologist_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿ’ป', + 'woman_technologist_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ’ป', + 'woman_technologist_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€๐Ÿ’ป', + 'woman_technologist_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€๐Ÿ’ป', + 'woman_technologist_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ป', + 'woman_tipping_hand_dark_skin_tone': '๐Ÿ’๐Ÿฟโ€โ™€๏ธ', + 'woman_tipping_hand_light_skin_tone': '๐Ÿ’๐Ÿปโ€โ™€๏ธ', + 'woman_tipping_hand_medium_dark_skin_tone': '๐Ÿ’๐Ÿพโ€โ™€๏ธ', + 'woman_tipping_hand_medium_light_skin_tone': '๐Ÿ’๐Ÿผโ€โ™€๏ธ', + 'woman_tipping_hand_medium_skin_tone': '๐Ÿ’๐Ÿฝโ€โ™€๏ธ', + 'woman_vampire_dark_skin_tone': '๐Ÿง›๐Ÿฟโ€โ™€๏ธ', + 'woman_vampire_light_skin_tone': '๐Ÿง›๐Ÿปโ€โ™€๏ธ', + 'woman_vampire_medium_dark_skin_tone': '๐Ÿง›๐Ÿพโ€โ™€๏ธ', + 'woman_vampire_medium_light_skin_tone': '๐Ÿง›๐Ÿผโ€โ™€๏ธ', + 'woman_vampire_medium_skin_tone': '๐Ÿง›๐Ÿฝโ€โ™€๏ธ', + 'woman_walking_dark_skin_tone': '๐Ÿšถ๐Ÿฟโ€โ™€๏ธ', + 'woman_walking_light_skin_tone': '๐Ÿšถ๐Ÿปโ€โ™€๏ธ', + 'woman_walking_medium_dark_skin_tone': '๐Ÿšถ๐Ÿพโ€โ™€๏ธ', + 'woman_walking_medium_light_skin_tone': '๐Ÿšถ๐Ÿผโ€โ™€๏ธ', + 'woman_walking_medium_skin_tone': '๐Ÿšถ๐Ÿฝโ€โ™€๏ธ', + 'woman_wearing_turban_dark_skin_tone': '๐Ÿ‘ณ๐Ÿฟโ€โ™€๏ธ', + 'woman_wearing_turban_light_skin_tone': '๐Ÿ‘ณ๐Ÿปโ€โ™€๏ธ', + 'woman_wearing_turban_medium_dark_skin_tone': '๐Ÿ‘ณ๐Ÿพโ€โ™€๏ธ', + 'woman_wearing_turban_medium_light_skin_tone': '๐Ÿ‘ณ๐Ÿผโ€โ™€๏ธ', + 'woman_wearing_turban_medium_skin_tone': '๐Ÿ‘ณ๐Ÿฝโ€โ™€๏ธ', + 'woman_white_haired_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆณ', + 'woman_white_haired_light_skin_tone': '๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆณ', + 'woman_white_haired_medium_dark_skin_tone': '๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆณ', + 'woman_white_haired_medium_light_skin_tone': '๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆณ', + 'woman_white_haired_medium_skin_tone': '๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆณ', + 'woman_with_headscarf_dark_skin_tone': '๐Ÿง•๐Ÿฟ', + 'woman_with_headscarf_light_skin_tone': '๐Ÿง•๐Ÿป', + 'woman_with_headscarf_medium_dark_skin_tone': '๐Ÿง•๐Ÿพ', + 'woman_with_headscarf_medium_light_skin_tone': '๐Ÿง•๐Ÿผ', + 'woman_with_headscarf_medium_skin_tone': '๐Ÿง•๐Ÿฝ', + 'worship_symbol': '๐Ÿ›', + 'ws': '๐Ÿ‡ผ๐Ÿ‡ธ', + 'xk': '๐Ÿ‡ฝ๐Ÿ‡ฐ', + 'ye': '๐Ÿ‡พ๐Ÿ‡ช', + 'yt': '๐Ÿ‡พ๐Ÿ‡น', + 'za': '๐Ÿ‡ฟ๐Ÿ‡ฆ', + 'zm': '๐Ÿ‡ฟ๐Ÿ‡ฒ', + 'zw': '๐Ÿ‡ฟ๐Ÿ‡ผ', + 'digit_zero': '0๏ธโƒฃ', + 'digit_one': '1๏ธโƒฃ', + 'digit_two': '2๏ธโƒฃ', + 'digit_three': '3๏ธโƒฃ', + 'digit_four': '4๏ธโƒฃ', + 'digit_five': '5๏ธโƒฃ', + 'digit_six': '6๏ธโƒฃ', + 'digit_seven': '7๏ธโƒฃ', + 'digit_eight': '8๏ธโƒฃ', + 'digit_nine': '9๏ธโƒฃ', + 'pound_symbol': '#๏ธโƒฃ', + 'asterisk_symbol': '*๏ธโƒฃ', + 'tone1': '๐Ÿป', + 'tone2': '๐Ÿผ', + 'tone3': '๐Ÿฝ', + 'tone4': '๐Ÿพ', + 'tone5': '๐Ÿฟ', +}; diff --git a/apps/meteor/app/emoji-native/lib/shortnameToUnicode.ts b/apps/meteor/app/emoji-native/lib/shortnameToUnicode.ts new file mode 100644 index 0000000000000..a244a24025ab6 --- /dev/null +++ b/apps/meteor/app/emoji-native/lib/shortnameToUnicode.ts @@ -0,0 +1,19 @@ +import type { EmojiEntry } from './generateEmojiData'; +import { getEmojiData } from './generateEmojiData'; +import { legacyEmojioneMap } from './legacyEmojioneMap'; + +export function shortnameToUnicode(text: string): string { + const { emojiList } = getEmojiData(); + + return text.replace(/:([a-zA-Z0-9_+-]+):/g, (match, shortcode) => { + const key = `:${shortcode}:`; + const emoji = emojiList[key] as EmojiEntry | undefined; + if (emoji?.unicode) return emoji.unicode; + + // Fallback to legacy emojione shortcodes for backward compatibility + const legacy = Object.hasOwn(legacyEmojioneMap, shortcode) ? legacyEmojioneMap[shortcode] : undefined; + if (legacy) return legacy; + + return match; + }); +} diff --git a/apps/meteor/app/emoji/client/emojiParser.ts b/apps/meteor/app/emoji/client/emojiParser.ts index c919642d28345..7319cd8d4135b 100644 --- a/apps/meteor/app/emoji/client/emojiParser.ts +++ b/apps/meteor/app/emoji/client/emojiParser.ts @@ -1,5 +1,4 @@ import { emoji } from './lib'; -import { isIE11 } from '../../../client/lib/utils/isIE11'; /** * emojiParser is a function that will replace emojis @@ -21,7 +20,7 @@ export const emojiParser = (html: string) => { checkEmojiOnly.innerHTML = html; - const emojis = Array.from(checkEmojiOnly.querySelectorAll('.emoji:not(:empty), .emojione:not(:empty)')); + const emojis = Array.from(checkEmojiOnly.querySelectorAll('.emoji:not(:empty)')); emojis.forEach((emojiElement) => { const htmlElement = emojiElement.parentElement; @@ -33,33 +32,31 @@ export const emojiParser = (html: string) => { let hasText = false; - if (!isIE11) { - const isElement = (node: Node): node is Element => node.nodeType === Node.ELEMENT_NODE; + const isElement = (node: Node): node is Element => node.nodeType === Node.ELEMENT_NODE; - const isTextNode = (node: Node): node is Text => node.nodeType === Node.TEXT_NODE; + const isTextNode = (node: Node): node is Text => node.nodeType === Node.TEXT_NODE; - const filter = (node: Node) => { - if (isElement(node) && (node.classList.contains('emojione') || node.classList.contains('emoji'))) { - return NodeFilter.FILTER_REJECT; - } - return NodeFilter.FILTER_ACCEPT; - }; + const filter = (node: Node) => { + if (isElement(node) && node.classList.contains('emoji')) { + return NodeFilter.FILTER_REJECT; + } + return NodeFilter.FILTER_ACCEPT; + }; - const walker = document.createTreeWalker(checkEmojiOnly, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, filter); + const walker = document.createTreeWalker(checkEmojiOnly, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, filter); - while (walker.nextNode()) { - if (isTextNode(walker.currentNode) && walker.currentNode.nodeValue.trim() !== '') { - hasText = true; - break; - } + while (walker.nextNode()) { + if (isTextNode(walker.currentNode) && walker.currentNode.nodeValue.trim() !== '') { + hasText = true; + break; } - const emojiOnly = emojis.length && !hasText; + } + const emojiOnly = emojis.length && !hasText; - if (emojiOnly) { - for (let i = 0, len = emojis.length; i < len; i++) { - const { classList } = emojis[i]; - classList.add('big'); - } + if (emojiOnly) { + for (let i = 0, len = emojis.length; i < len; i++) { + const { classList } = emojis[i]; + classList.add('big'); } } diff --git a/apps/meteor/app/emoji/client/helpers.ts b/apps/meteor/app/emoji/client/helpers.ts index d36dc958cf20e..15f46499f8ace 100644 --- a/apps/meteor/app/emoji/client/helpers.ts +++ b/apps/meteor/app/emoji/client/helpers.ts @@ -5,6 +5,9 @@ import type { EmojiCategory, EmojiItem } from '.'; import { emoji, emojiEmitter } from './lib'; export const CUSTOM_CATEGORY = 'rocket'; +export const CUSTOM_CATEGORY_CLASSNAME = 'emojipicker--custom'; + +const MIXED_TONE_SUFFIX = /_tone([1-5])-[1-5]$/; type RowItem = Array; type RowDivider = { category: string; i18n: TranslationKey }; @@ -66,11 +69,11 @@ export const createEmojiList = ( setRecentEmojis: (emojis: string[]) => void, ): (RowItem | LoadMoreItem)[] => { const items: RowItem = []; - const emojiPackages = Object.values(emoji.packages); + const emojiPackages = Object.entries(emoji.packages); let count = 0; let limited = false; - emojiPackages.forEach((emojiPackage) => { + emojiPackages.forEach(([packageName, emojiPackage]) => { if (!emojiPackage.emojisByCategory?.[category]) { return; } @@ -83,12 +86,19 @@ export const createEmojiList = ( const emojiToRender = `:${current}${tone}:`; - if (!emoji.list[emojiToRender]) { + const actualEmoji = emoji.list[emojiToRender]; + if (!actualEmoji) { removeFromRecent(emojiToRender, recentEmojis, setRecentEmojis); return; } - const image = emojiPackage.renderPicker(emojiToRender); + // A custom emoji with the same name overrides the native one (including its tones), so skip the native duplicate + if (packageName === 'native' && emoji.list[`:${current}:`]?.emojiPackage === 'emojiCustom') { + continue; + } + + const actualPackage = actualEmoji.emojiPackage; + const image = emoji.packages[actualPackage].renderPicker(emojiToRender); if (!image) { continue; } @@ -165,13 +175,19 @@ export const getEmojisBySearchTerm = ( tone = `_tone${actualTone}`; } + const mixedTones = current.match(MIXED_TONE_SUFFIX); + const categoryName = current.replace(MIXED_TONE_SUFFIX, ''); + if (mixedTones && !(actualTone > 0 && Number(mixedTones[1]) === actualTone)) { + continue; + } + let emojiFound = false; for (const key in emoji.packages[emojiPackage].emojisByCategory) { if (emoji.packages[emojiPackage].emojisByCategory.hasOwnProperty(key)) { const contents = emoji.packages[emojiPackage].emojisByCategory[key]; const searchValArray = alias !== undefined ? alias.replace(/:/g, '').split('_') : alias; - if (contents.indexOf(current) !== -1 || searchValArray?.includes(searchTerm)) { + if (contents.indexOf(categoryName) !== -1 || searchValArray?.includes(searchTerm)) { emojiFound = true; break; } @@ -181,12 +197,15 @@ export const getEmojisBySearchTerm = ( if (emojiFound) { const emojiToRender = `:${current}${tone}:`; - if (!emoji.list[emojiToRender]) { + const actualEmoji = emoji.list[emojiToRender]; + if (!actualEmoji) { removeFromRecent(emojiToRender, recentEmojis, setRecentEmojis); break; } - emojis.push({ emoji: current, image: emoji.packages[emojiPackage].renderPicker(emojiToRender) }); + const actualPackage = actualEmoji.emojiPackage; + + emojis.push({ emoji: current, image: emoji.packages[actualPackage].renderPicker(emojiToRender) }); } } } diff --git a/apps/meteor/app/emoji/client/lib.ts b/apps/meteor/app/emoji/client/lib.ts index 462e86cc34a91..688ef40d8b652 100644 --- a/apps/meteor/app/emoji/client/lib.ts +++ b/apps/meteor/app/emoji/client/lib.ts @@ -1,5 +1,4 @@ import { Emitter } from '@rocket.chat/emitter'; -import emojione from 'emojione'; import type { EmojiPackages } from '../lib/rocketchat'; @@ -14,7 +13,7 @@ export const emoji: EmojiPackages & { dispatchUpdate: () => void } = { recent: [], }, toneList: {}, - render: emojione.toImage, + render: (message: string) => message, renderPicker(emojiToRender) { const correctPackage = emoji.list[emojiToRender].emojiPackage; if (!correctPackage) { diff --git a/apps/meteor/app/emoji/lib/rocketchat.ts b/apps/meteor/app/emoji/lib/rocketchat.ts index 1c5b515407ed8..6e96cceeaa81d 100644 --- a/apps/meteor/app/emoji/lib/rocketchat.ts +++ b/apps/meteor/app/emoji/lib/rocketchat.ts @@ -7,7 +7,6 @@ export type EmojiPackage = { toneList: Record; render: (message: string) => string; renderPicker: (emojiToRender: string) => string | undefined; - ascii?: boolean; sprites?: unknown; list?: string[]; _regexpSignature?: string | null; @@ -32,6 +31,7 @@ export type EmojiPackages = { aliasOf?: undefined; extension?: string; etag?: string; + unicode?: string; } | { emojiPackage: string; diff --git a/apps/meteor/app/emoji/server/index.ts b/apps/meteor/app/emoji/server/index.ts deleted file mode 100644 index 525aac9589067..0000000000000 --- a/apps/meteor/app/emoji/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { emoji } from './lib'; diff --git a/apps/meteor/app/emoji/server/lib.ts b/apps/meteor/app/emoji/server/lib.ts deleted file mode 100644 index ecd109579cfe7..0000000000000 --- a/apps/meteor/app/emoji/server/lib.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { EmojiPackages } from '../lib/rocketchat'; - -export const emoji: EmojiPackages = { - packages: { - base: { - emojiCategories: [{ key: 'recent', i18n: 'Frequently_Used' }], - categoryIndex: 0, - emojisByCategory: { - recent: [], - }, - toneList: {}, - render: (message: string) => message, - renderPicker(emojiToRender) { - if (!emoji.list[emojiToRender]) { - return; - } - const correctPackage = emoji.list[emojiToRender].emojiPackage; - if (!correctPackage) { - return; - } - - return emoji.packages[correctPackage]?.renderPicker(emojiToRender); - }, - }, - }, - /** @type {Record} */ - list: {}, -}; diff --git a/apps/meteor/app/error-handler/server/index.ts b/apps/meteor/app/error-handler/server/index.ts deleted file mode 100644 index a321a704bc2cb..0000000000000 --- a/apps/meteor/app/error-handler/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './lib/RocketChat.ErrorHandler'; diff --git a/apps/meteor/app/file-upload/server/config/Webdav.ts b/apps/meteor/app/file-upload/server/config/Webdav.ts deleted file mode 100644 index 901c74e9c149e..0000000000000 --- a/apps/meteor/app/file-upload/server/config/Webdav.ts +++ /dev/null @@ -1,74 +0,0 @@ -import _ from 'underscore'; - -import { SystemLogger } from '../../../../server/lib/logger/system'; -import { settings } from '../../../settings/server'; -import { FileUploadClass, FileUpload } from '../lib/FileUpload'; -import '../../ufs/Webdav/server'; - -const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, _req, res) { - (await this.store.getReadStream(file._id, file)) - .on('error', () => { - SystemLogger.error('An error ocurred when fetching the file'); - res.writeHead(503); - res.end(); - }) - .on('data', (chunk) => { - res.write(chunk); - }) - .on('end', res.end.bind(res)); -}; - -const copy: FileUploadClass['copy'] = async function (this: FileUploadClass, file, out) { - return new Promise(async (resolve) => { - (await this.store.getReadStream(file._id, file)).pipe(out).on('finish', () => resolve()); - }); -}; - -const WebdavUploads = new FileUploadClass({ - name: 'Webdav:Uploads', - get, - copy, - // store setted bellow -}); - -const WebdavAvatars = new FileUploadClass({ - name: 'Webdav:Avatars', - get, - copy, - // store setted bellow -}); - -const WebdavUserDataFiles = new FileUploadClass({ - name: 'Webdav:UserDataFiles', - get, - copy, - // store setted bellow -}); - -const configure = _.debounce(() => { - const uploadFolderPath = settings.get('FileUpload_Webdav_Upload_Folder_Path'); - const server = settings.get('FileUpload_Webdav_Server_URL'); - const username = settings.get('FileUpload_Webdav_Username'); - const password = settings.get('FileUpload_Webdav_Password'); - - if (!server || !username || !password) { - return; - } - - const config = { - connection: { - credentials: { - server, - username, - password, - }, - }, - uploadFolderPath, - }; - - WebdavUploads.store = FileUpload.configureUploadsStore('Webdav', WebdavUploads.name, config); - WebdavAvatars.store = FileUpload.configureUploadsStore('Webdav', WebdavAvatars.name, config); - WebdavUserDataFiles.store = FileUpload.configureUploadsStore('Webdav', WebdavUserDataFiles.name, config); -}, 500); - -settings.watchByRegex(/^FileUpload_Webdav_/, configure); diff --git a/apps/meteor/app/file-upload/server/index.ts b/apps/meteor/app/file-upload/server/index.ts deleted file mode 100644 index ff2fc4fabf369..0000000000000 --- a/apps/meteor/app/file-upload/server/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import '../lib/FileUploadBase'; -import { FileUpload } from './lib/FileUpload'; -import './lib/requests'; -import './config/_configUploadStorage'; -import './methods/sendFileMessage'; -import './methods/getS3FileUrl'; - -export { FileUpload }; diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.spec.ts b/apps/meteor/app/file-upload/server/lib/FileUpload.spec.ts deleted file mode 100644 index ddec9231b3bb2..0000000000000 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.spec.ts +++ /dev/null @@ -1,471 +0,0 @@ -import { expect } from 'chai'; -import { before, beforeEach, describe, it } from 'mocha'; -import proxyquire from 'proxyquire'; -import sinon from 'sinon'; - -import { createFakeMessageWithAttachment } from '../../../../tests/mocks/data'; - -const fakeStorageModel = { findOneById: sinon.stub(), deleteFile: sinon.stub() }; -const settingsStub = { watch: sinon.stub(), get: sinon.stub() }; -const settingsGetMap = new Map(); -const messagesModelStub = { - find: sinon.stub(), -}; -const usersModelStub = { - findOneByIdAndLoginToken: sinon.stub(), -}; -const subscriptionsModelStub = { - findOneByRoomIdAndUserId: sinon.stub(), -}; -const validateAndDecodeJWTStub = sinon.stub(); -const systemLoggerStub = { - error: sinon.stub(), -}; -const roomCoordinatorStub = { - getRoomDirectives: sinon.stub(), -}; - -const { FileUpload, FileUploadClass } = proxyquire.noCallThru().load('./FileUpload', { - '@rocket.chat/models': { - Messages: messagesModelStub, - Users: usersModelStub, - Subscriptions: subscriptionsModelStub, - }, - 'meteor/check': sinon.stub(), - 'meteor/meteor': sinon.stub(), - 'meteor/ostrio:cookies': { Cookies: sinon.stub() }, - 'sharp': sinon.stub(), - 'stream-buffers': sinon.stub(), - '@rocket.chat/tools': sinon.stub(), - '../../../../server/lib/i18n': sinon.stub(), - '../../../../server/lib/logger/system': { SystemLogger: systemLoggerStub }, - '../../../../server/lib/rooms/roomCoordinator': { roomCoordinator: roomCoordinatorStub }, - '../../../../server/ufs': sinon.stub(), - '../../../../server/ufs/ufs-methods': sinon.stub(), - '../../../settings/server': { settings: settingsStub }, - '../../../utils/lib/mimeTypes': sinon.stub(), - '../../../utils/server/lib/JWTHelper': { - validateAndDecodeJWT: validateAndDecodeJWTStub, - generateJWT: sinon.stub(), - }, - '../../../utils/server/restrictions': sinon.stub(), - '../../../api/server/lib/MultipartUploadHandler': sinon.stub(), - '@rocket.chat/account-utils': { hashLoginToken: sinon.stub().callsFake((token) => `hashed_${token}`) }, -}); - -describe('FileUpload', () => { - before(() => { - new FileUploadClass({ name: 'fakeStorage:Uploads', model: fakeStorageModel, store: {} }); - settingsGetMap.set('FileUpload_Storage_Type', 'fakeStorage'); - settingsStub.get.callsFake((settingName) => settingsGetMap.get(settingName)); - }); - - beforeEach(() => { - messagesModelStub.find.reset(); - fakeStorageModel.findOneById.reset(); - fakeStorageModel.deleteFile.reset(); - usersModelStub.findOneByIdAndLoginToken.reset(); - subscriptionsModelStub.findOneByRoomIdAndUserId.reset(); - validateAndDecodeJWTStub.reset(); - systemLoggerStub.error.reset(); - roomCoordinatorStub.getRoomDirectives.reset(); - settingsGetMap.clear(); - settingsGetMap.set('FileUpload_Storage_Type', 'fakeStorage'); - }); - - it('should not remove any file if no room id is provided', async () => { - expect(await FileUpload.removeFilesByRoomId()).to.be.undefined; - - expect(messagesModelStub.find.called).to.be.false; - expect(fakeStorageModel.findOneById.called).to.be.false; - }); - - it('should not remove any file if an empty room id is provided', async () => { - expect(await FileUpload.removeFilesByRoomId('')).to.be.undefined; - - expect(messagesModelStub.find.called).to.be.false; - expect(fakeStorageModel.findOneById.called).to.be.false; - }); - - it('should not remove any file if an invalid room id is provided', async () => { - messagesModelStub.find.returns([]); - expect(await FileUpload.removeFilesByRoomId('invalid')).to.be.undefined; - - expect(messagesModelStub.find.called).to.be.true; - expect(fakeStorageModel.findOneById.called).to.be.false; - }); - - it('should delete file from storage if message contains a single file', async () => { - fakeStorageModel.findOneById.resolves({ _id: 'file-id', store: 'fakeStorage:Uploads' }); - - const fakeMessage = createFakeMessageWithAttachment(); - messagesModelStub.find.returns([fakeMessage]); - expect(await FileUpload.removeFilesByRoomId('invalid')).to.be.undefined; - - expect(messagesModelStub.find.called).to.be.true; - expect(fakeStorageModel.findOneById.calledOnceWith(fakeMessage.files?.[0]._id)).to.be.true; - expect(fakeStorageModel.deleteFile.calledOnceWith('file-id')).to.be.true; - }); - - it('should delete multiple files from storage if message contains many files (e.g. image and thumbnail)', async () => { - fakeStorageModel.findOneById.callsFake((_id) => ({ _id, store: 'fakeStorage:Uploads' })); - - const fakeMessage = createFakeMessageWithAttachment({ - files: [ - { _id: 'file-id', name: 'image', size: 100, type: 'image/png', format: 'png' }, - { _id: 'thumbnail-id', name: 'thumbnail-image', size: 25, type: 'image/png', format: 'png' }, - ], - }); - messagesModelStub.find.returns([fakeMessage]); - expect(await FileUpload.removeFilesByRoomId('invalid')).to.be.undefined; - - expect(messagesModelStub.find.called).to.be.true; - expect(fakeStorageModel.findOneById.calledTwice).to.be.true; - expect(fakeStorageModel.findOneById.calledWith('file-id')).to.be.true; - expect(fakeStorageModel.findOneById.calledWith('thumbnail-id')).to.be.true; - expect(fakeStorageModel.deleteFile.calledTwice).to.be.true; - expect(fakeStorageModel.deleteFile.calledWith('file-id')).to.be.true; - expect(fakeStorageModel.deleteFile.calledWith('thumbnail-id')).to.be.true; - }); - - describe('requestCanAccessFiles', () => { - it('should allow access if FileUpload_ProtectFiles is false', async () => { - settingsGetMap.set('FileUpload_ProtectFiles', false); - - const request = { - headers: {}, - url: '/file-upload/test-file-id/test-file.png', - } as any; - - const result = await FileUpload.requestCanAccessFiles(request); - expect(result).to.be.true; - }); - - it('should allow access if no url is provided', async () => { - settingsGetMap.set('FileUpload_ProtectFiles', true); - - const request = { - headers: {}, - url: undefined, - } as any; - - const result = await FileUpload.requestCanAccessFiles(request); - expect(result).to.be.true; - }); - - it('should deny access if FileUpload_Enable_json_web_token_for_files is true but no token is provided', async () => { - settingsGetMap.set('FileUpload_ProtectFiles', true); - settingsGetMap.set('FileUpload_Enable_json_web_token_for_files', true); - - const request = { - headers: {}, - url: '/file-upload/test-file-id/test-file.png', - } as any; - - const file = { - _id: 'test-file-id', - rid: 'test-room-id', - } as any; - - const result = await FileUpload.requestCanAccessFiles(request, file); - expect(result).to.be.false; - }); - - it('should deny access if FileUpload_json_web_token_secret_for_files is not configured', async () => { - settingsGetMap.set('FileUpload_ProtectFiles', true); - settingsGetMap.set('FileUpload_Enable_json_web_token_for_files', true); - settingsGetMap.set('FileUpload_json_web_token_secret_for_files', ''); - - const request = { - headers: {}, - url: '/file-upload/test-file-id/test-file.png?token=some-token', - } as any; - - const file = { - _id: 'test-file-id', - rid: 'test-room-id', - } as any; - - const result = await FileUpload.requestCanAccessFiles(request, file); - expect(result).to.be.false; - expect(systemLoggerStub.error.calledOnce).to.be.true; - }); - - it('should deny access if an invalid token is provided', async () => { - settingsGetMap.set('FileUpload_ProtectFiles', true); - settingsGetMap.set('FileUpload_Enable_json_web_token_for_files', true); - settingsGetMap.set('FileUpload_json_web_token_secret_for_files', 'test-secret'); - validateAndDecodeJWTStub.returns(null); - - const request = { - headers: {}, - url: '/file-upload/test-file-id/test-file.png?token=invalid-token', - } as any; - - const file = { - _id: 'test-file-id', - rid: 'test-room-id', - } as any; - - const result = await FileUpload.requestCanAccessFiles(request, file); - expect(result).to.be.false; - expect(validateAndDecodeJWTStub.calledOnce).to.be.true; - }); - - it('should deny access if token is invalid or payload cannot be decoded', async () => { - settingsGetMap.set('FileUpload_ProtectFiles', true); - settingsGetMap.set('FileUpload_Enable_json_web_token_for_files', true); - settingsGetMap.set('FileUpload_json_web_token_secret_for_files', 'test-secret'); - validateAndDecodeJWTStub.returns(null); - - const request = { - headers: {}, - url: '/file-upload/test-file-id/test-file.png?token=valid-token', - } as any; - - const file = { - _id: 'test-file-id', - rid: 'test-room-id', - } as any; - - const result = await FileUpload.requestCanAccessFiles(request, file); - expect(result).to.be.false; - }); - - it('should deny access if the fileId and rid in the token do not match the requested file', async () => { - settingsGetMap.set('FileUpload_ProtectFiles', true); - settingsGetMap.set('FileUpload_Enable_json_web_token_for_files', true); - settingsGetMap.set('FileUpload_json_web_token_secret_for_files', 'test-secret'); - validateAndDecodeJWTStub.returns({ fileId: 'different-file-id', rid: 'different-room-id', userId: 'test-user-id' }); - - const request = { - headers: {}, - url: '/file-upload/test-file-id/test-file.png?token=valid-token', - } as any; - - const file = { - _id: 'test-file-id', - rid: 'test-room-id', - } as any; - - const result = await FileUpload.requestCanAccessFiles(request, file); - expect(result).to.be.false; - }); - - it('should deny access if file object is not provided when using JWT', async () => { - settingsGetMap.set('FileUpload_ProtectFiles', true); - settingsGetMap.set('FileUpload_Enable_json_web_token_for_files', true); - settingsGetMap.set('FileUpload_json_web_token_secret_for_files', 'test-secret'); - validateAndDecodeJWTStub.returns({ fileId: 'test-file-id', rid: 'test-room-id', userId: 'test-user-id' }); - - const request = { - headers: {}, - url: '/file-upload/test-file-id/test-file.png?token=valid-token', - } as any; - - const result = await FileUpload.requestCanAccessFiles(request, undefined); - expect(result).to.be.false; - }); - - it('should allow access when everything is valid: token is valid, secret configured, and file/room match', async () => { - settingsGetMap.set('FileUpload_ProtectFiles', true); - settingsGetMap.set('FileUpload_Enable_json_web_token_for_files', true); - settingsGetMap.set('FileUpload_json_web_token_secret_for_files', 'test-secret'); - validateAndDecodeJWTStub.returns({ fileId: 'test-file-id', rid: 'test-room-id', userId: 'test-user-id' }); - - const request = { - headers: {}, - url: '/file-upload/test-file-id/test-file.png?token=valid-token', - } as any; - - const file = { - _id: 'test-file-id', - rid: 'test-room-id', - } as any; - - const result = await FileUpload.requestCanAccessFiles(request, file); - expect(result).to.be.true; - expect(validateAndDecodeJWTStub.calledOnceWith('valid-token', 'test-secret')).to.be.true; - }); - - describe('livechat room-based authorization (rc_room_type=l)', () => { - it('should allow access when livechat credentials are valid and file belongs to the same room', async () => { - settingsGetMap.set('FileUpload_ProtectFiles', true); - const canAccessUploadedFileStub = sinon.stub().resolves(true); - roomCoordinatorStub.getRoomDirectives.returns({ canAccessUploadedFile: canAccessUploadedFileStub }); - - const request = { - headers: {}, - url: '/file-upload/test-file-id/test-file.png?rc_room_type=l&rc_rid=room-1&rc_token=visitor-token', - } as any; - - const file = { _id: 'test-file-id', rid: 'room-1' } as any; - - const result = await FileUpload.requestCanAccessFiles(request, file); - expect(result).to.be.true; - expect(canAccessUploadedFileStub.calledOnce).to.be.true; - }); - - it('should deny access when livechat credentials are valid but file belongs to a different room', async () => { - settingsGetMap.set('FileUpload_ProtectFiles', true); - const canAccessUploadedFileStub = sinon.stub().resolves(false); - roomCoordinatorStub.getRoomDirectives.returns({ canAccessUploadedFile: canAccessUploadedFileStub }); - - const request = { - headers: {}, - url: '/file-upload/victim-file-id/secret.txt?rc_room_type=l&rc_rid=room-attacker&rc_token=attacker-token', - } as any; - - // File belongs to victim's room, not the attacker's room - const file = { _id: 'victim-file-id', rid: 'room-victim' } as any; - - const result = await FileUpload.requestCanAccessFiles(request, file); - expect(result).to.be.false; - }); - - it('should pass the file object to canAccessUploadedFile', async () => { - settingsGetMap.set('FileUpload_ProtectFiles', true); - const canAccessUploadedFileStub = sinon.stub().resolves(true); - roomCoordinatorStub.getRoomDirectives.returns({ canAccessUploadedFile: canAccessUploadedFileStub }); - - const request = { - headers: {}, - url: '/file-upload/test-file-id/test-file.png?rc_room_type=l&rc_rid=room-1&rc_token=visitor-token', - } as any; - - const file = { _id: 'test-file-id', rid: 'room-1' } as any; - - await FileUpload.requestCanAccessFiles(request, file); - - const callArgs = canAccessUploadedFileStub.firstCall.args; - expect(callArgs[1]).to.deep.equal(file); - }); - - it('should deny access when rc_room_type is provided but canAccessUploadedFile returns false', async () => { - settingsGetMap.set('FileUpload_ProtectFiles', true); - const canAccessUploadedFileStub = sinon.stub().resolves(false); - roomCoordinatorStub.getRoomDirectives.returns({ canAccessUploadedFile: canAccessUploadedFileStub }); - - const request = { - headers: {}, - url: '/file-upload/test-file-id/test-file.png?rc_room_type=l&rc_rid=room-1&rc_token=invalid-token', - } as any; - - const file = { _id: 'test-file-id', rid: 'room-1' } as any; - - const result = await FileUpload.requestCanAccessFiles(request, file); - expect(result).to.be.false; - }); - }); - }); - - describe('getRequestUserId', () => { - it('should return undefined when no url is provided', async () => { - const request = { headers: {}, url: undefined } as any; - - const result = await FileUpload.getRequestUserId(request); - expect(result).to.be.undefined; - expect(usersModelStub.findOneByIdAndLoginToken.called).to.be.false; - }); - - it('should return undefined when no credentials are provided', async () => { - const request = { headers: {}, url: '/ufs/UserDataFiles/file-id' } as any; - - const result = await FileUpload.getRequestUserId(request); - expect(result).to.be.undefined; - expect(usersModelStub.findOneByIdAndLoginToken.called).to.be.false; - }); - - it('should return undefined when a uid is provided without a token', async () => { - const request = { headers: { 'x-user-id': 'user-1' }, url: '/ufs/UserDataFiles/file-id' } as any; - - const result = await FileUpload.getRequestUserId(request); - expect(result).to.be.undefined; - expect(usersModelStub.findOneByIdAndLoginToken.called).to.be.false; - }); - - it('should return undefined when the login token is invalid', async () => { - usersModelStub.findOneByIdAndLoginToken.resolves(null); - - const request = { headers: { 'x-user-id': 'user-1', 'x-auth-token': 'bad-token' }, url: '/ufs/UserDataFiles/file-id' } as any; - - const result = await FileUpload.getRequestUserId(request); - expect(result).to.be.undefined; - expect(usersModelStub.findOneByIdAndLoginToken.calledOnceWith('user-1', 'hashed_bad-token')).to.be.true; - }); - - it('should return the user id when credentials are valid via headers', async () => { - usersModelStub.findOneByIdAndLoginToken.resolves({ _id: 'user-1' }); - - const request = { headers: { 'x-user-id': 'user-1', 'x-auth-token': 'good-token' }, url: '/ufs/UserDataFiles/file-id' } as any; - - const result = await FileUpload.getRequestUserId(request); - expect(result).to.equal('user-1'); - expect(usersModelStub.findOneByIdAndLoginToken.calledOnceWith('user-1', 'hashed_good-token')).to.be.true; - }); - - it('should return the user id when credentials are valid via query string', async () => { - usersModelStub.findOneByIdAndLoginToken.resolves({ _id: 'user-1' }); - - const request = { headers: {}, url: '/ufs/UserDataFiles/file-id?rc_uid=user-1&rc_token=good-token' } as any; - - const result = await FileUpload.getRequestUserId(request); - expect(result).to.equal('user-1'); - expect(usersModelStub.findOneByIdAndLoginToken.calledOnceWith('user-1', 'hashed_good-token')).to.be.true; - }); - }); - - describe('UserDataFiles.onRead', () => { - // eslint-disable-next-line new-cap - const getOnRead = () => FileUpload.defaults.UserDataFiles().onRead; - - const createResponse = () => { - const res = { writeHead: sinon.stub(), setHeader: sinon.stub() }; - res.writeHead.returns(res); - return res as any; - }; - - it('should deny access to an unauthenticated request', async () => { - const res = createResponse(); - const file = { _id: 'file-id', userId: 'owner-1', name: 'export.zip' } as any; - const request = { headers: {}, url: '/ufs/UserDataFiles/file-id' } as any; - - const result = await getOnRead()('file-id', file, request, res); - expect(result).to.be.false; - expect(res.writeHead.calledOnceWith(403)).to.be.true; - expect(res.setHeader.called).to.be.false; - }); - - it('should deny access to an authenticated user who is not the owner', async () => { - usersModelStub.findOneByIdAndLoginToken.resolves({ _id: 'attacker-1' }); - - const res = createResponse(); - const file = { _id: 'file-id', userId: 'owner-1', name: 'export.zip' } as any; - const request = { - headers: { 'x-user-id': 'attacker-1', 'x-auth-token': 'attacker-token' }, - url: '/ufs/UserDataFiles/file-id', - } as any; - - const result = await getOnRead()('file-id', file, request, res); - expect(result).to.be.false; - expect(res.writeHead.calledOnceWith(403)).to.be.true; - expect(res.setHeader.called).to.be.false; - }); - - it('should allow access to the owner of the export', async () => { - usersModelStub.findOneByIdAndLoginToken.resolves({ _id: 'owner-1' }); - - const res = createResponse(); - const file = { _id: 'file-id', userId: 'owner-1', name: 'export.zip' } as any; - const request = { - headers: { 'x-user-id': 'owner-1', 'x-auth-token': 'owner-token' }, - url: '/ufs/UserDataFiles/file-id', - } as any; - - const result = await getOnRead()('file-id', file, request, res); - expect(result).to.be.true; - expect(res.writeHead.called).to.be.false; - expect(res.setHeader.calledOnceWith('content-disposition', 'attachment; filename="export.zip"')).to.be.true; - }); - }); -}); diff --git a/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts b/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts deleted file mode 100644 index 2ab3312b31f30..0000000000000 --- a/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts +++ /dev/null @@ -1,212 +0,0 @@ -import stream from 'node:stream'; - -import { - DeleteObjectCommand, - GetObjectCommand, - S3Client, - type GetObjectCommandInput, - type PutObjectCommandInput, - type S3ClientConfig, -} from '@aws-sdk/client-s3'; -import { Upload } from '@aws-sdk/lib-storage'; -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import type { IUpload } from '@rocket.chat/core-typings'; -import { Random } from '@rocket.chat/random'; -import { check } from 'meteor/check'; -import _ from 'underscore'; - -import { SystemLogger } from '../../../../server/lib/logger/system'; -import { UploadFS } from '../../../../server/ufs'; -import type { StoreOptions } from '../../../../server/ufs/ufs-store'; -import { getUrlExpiryTimeSpanWithFallback } from '../../server/lib/urlExpiry'; - -export type S3Options = StoreOptions & { - connection: S3ClientConfig; - params: { - Bucket: string; - ACL: string; - }; - URLExpiryTimeSpan: number; - getPath: (file: Omit) => string; -}; - -class AmazonS3Store extends UploadFS.Store { - protected getPath: (file: Omit) => string; - - constructor(options: S3Options) { - // 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 customUserAgent = process.env.FILE_STORAGE_CUSTOM_USER_AGENT?.trim(); - - const s3 = new S3Client({ - ...options.connection, - ...(customUserAgent && { customUserAgent }), - }); - - 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; - } - - return file._id; - }; - - this.getRedirectURL = async (file, forceDownload = false) => { - const expiresIn = getUrlExpiryTimeSpanWithFallback(classOptions.URLExpiryTimeSpan); - return getSignedUrl( - s3, - new GetObjectCommand({ - Key: this.getPath(file), - ResponseContentDisposition: `${forceDownload ? 'attachment' : 'inline'}; filename="${encodeURI(file.name || '')}"`, - Bucket: classOptions.params.Bucket, - }), - { expiresIn }, - ); - }; - - /** - * Creates the file in the collection - */ - this.create = async (file) => { - check(file, Object); - - if (file._id == null) { - file._id = Random.id(); - } - - file.AmazonS3 = { - path: classOptions.getPath(file as Omit), - }; - - file.store = this.options.name; // assign store to file - - return (await this.getCollection().insertOne(file)).insertedId; - }; - - /** - * Removes the file - */ - this.delete = async function (fileId) { - const file = await this.getCollection().findOne({ _id: fileId }); - if (!file) { - throw new Error('File not found'); - } - - try { - return await s3.send( - new DeleteObjectCommand({ - Key: this.getPath(file), - Bucket: classOptions.params.Bucket, - }), - ); - } catch (error) { - SystemLogger.error({ error, key: this.getPath(file), bucket: classOptions.params.Bucket }); - throw error; - } - }; - - /** - * Returns the file read stream - */ - this.getReadStream = async function (_fileId, file, options = {}) { - const params: GetObjectCommandInput = { - Key: this.getPath(file), - Bucket: classOptions.params.Bucket, - }; - - if (options.start != null && options.end != null) { - params.Range = `bytes=${options.start}-${options.end}`; - } - - const response = await s3.send(new GetObjectCommand(params)); - - if (!response.Body) { - throw new Error('File not found'); - } - - if (!('readable' in response.Body)) { - throw new Error('Response body is not a readable stream'); - } - - return response.Body; - }; - - /** - * Returns the file write stream - */ - this.getWriteStream = async function (_fileId, file /* , options*/) { - const writeStream = new stream.PassThrough(); - - writeStream.on('newListener', (event, listener) => { - if (event === 'finish') { - process.nextTick(() => { - writeStream.removeListener(event, listener); - writeStream.on('real_finish', listener); - }); - } - }); - - const uploadParams: PutObjectCommandInput = { - Key: this.getPath(file), - Body: writeStream, - Bucket: classOptions.params.Bucket, - ...(file.type && { ContentType: file.type }), - ...(file.size != null && { ContentLength: file.size }), - ...(classOptions.params.ACL && { ACL: classOptions.params.ACL as PutObjectCommandInput['ACL'] }), - }; - - const upload = new Upload({ - client: s3, - params: uploadParams, - }); - - upload - .done() - .then(() => { - writeStream.emit('real_finish'); - }) - .catch((error) => { - SystemLogger.error({ err: error }); - writeStream.emit('error', error); - }); - - return writeStream; - }; - - this.getUrlExpiryTimeSpan = async () => { - return getUrlExpiryTimeSpanWithFallback(classOptions.URLExpiryTimeSpan); - }; - } -} - -// Add store to UFS namespace -UploadFS.store.AmazonS3 = AmazonS3Store; diff --git a/apps/meteor/app/file-upload/ufs/GoogleStorage/server.ts b/apps/meteor/app/file-upload/ufs/GoogleStorage/server.ts deleted file mode 100644 index 03b74df609b01..0000000000000 --- a/apps/meteor/app/file-upload/ufs/GoogleStorage/server.ts +++ /dev/null @@ -1,163 +0,0 @@ -import type { GetSignedUrlConfig } from '@google-cloud/storage'; -import { Storage } from '@google-cloud/storage'; -import type { IUpload } from '@rocket.chat/core-typings'; -import { Random } from '@rocket.chat/random'; -import { check } from 'meteor/check'; - -import { SystemLogger } from '../../../../server/lib/logger/system'; -import { UploadFS } from '../../../../server/ufs'; -import type { StoreOptions } from '../../../../server/ufs/ufs-store'; -import { getUrlExpiryTimeSpanWithFallback } from '../../server/lib/urlExpiry'; - -type GStoreOptions = StoreOptions & { - connection: { - credentials: { - client_email: string; - private_key: string; - projectId: string; - }; - }; - bucket: string; - URLExpiryTimeSpan: number; - getPath: (file: Omit) => string; -}; - -class GoogleStorageStore extends UploadFS.Store { - protected getPath: (file: Omit) => string; - - constructor(options: GStoreOptions) { - super(options); - - const userAgent = process.env.FILE_STORAGE_CUSTOM_USER_AGENT?.trim(); - - const gcs = new Storage({ - ...(userAgent && { userAgent }), - ...options.connection, - }); - const 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; - } - - return file._id; - }; - - this.getRedirectURL = async (file, forceDownload = false) => { - const expirySeconds = getUrlExpiryTimeSpanWithFallback(options.URLExpiryTimeSpan); - const params: GetSignedUrlConfig = { - action: 'read', - responseDisposition: forceDownload ? 'attachment' : 'inline', - version: 'v2', - expires: Date.now() + expirySeconds * 1000, - }; - - const res = await bucket.file(this.getPath(file)).getSignedUrl(params); - return res[0]; - }; - - this.getUrlExpiryTimeSpan = async () => { - return getUrlExpiryTimeSpanWithFallback(options.URLExpiryTimeSpan); - }; - - /** - * Creates the file in the collection - * @param file - * @param callback - * @return {string} - */ - this.create = async function (file) { - check(file, Object); - - if (file._id == null) { - file._id = Random.id(); - } - - file.GoogleStorage = { - path: options.getPath(file as Omit), - }; - - file.store = this.options.name; // assign store to file - - return (await this.getCollection().insertOne(file)).insertedId; - }; - - /** - * Removes the file - * @param fileId - * @param callback - */ - this.delete = async function (fileId) { - // TODO - const file = await this.getCollection().findOne({ _id: fileId }); - if (!file) { - throw new Error('File not found'); - } - - try { - return bucket.file(this.getPath(file)).delete(); - } catch (err: any) { - SystemLogger.error({ err }); - } - }; - - /** - * Returns the file read stream - * @param fileId - * @param file - * @param options - * @return {*} - */ - this.getReadStream = async function (_fileId, file, options = {}) { - const config: { - start?: number; - end?: number; - } = {}; - - if (options.start != null) { - config.start = options.start; - } - - if (options.end != null) { - config.end = options.end; - } - - return bucket.file(this.getPath(file)).createReadStream(config); - }; - - /** - * Returns the file write stream - * @param fileId - * @param file - * @param options - * @return {*} - */ - this.getWriteStream = async function (_fileId, file /* , options*/) { - return 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/apps/meteor/app/file-upload/ufs/Webdav/server.ts b/apps/meteor/app/file-upload/ufs/Webdav/server.ts deleted file mode 100644 index 7eccb59d609cf..0000000000000 --- a/apps/meteor/app/file-upload/ufs/Webdav/server.ts +++ /dev/null @@ -1,154 +0,0 @@ -import stream from 'node:stream'; - -import type { IUpload } from '@rocket.chat/core-typings'; -import { Random } from '@rocket.chat/random'; -import { check } from 'meteor/check'; - -import { SystemLogger } from '../../../../server/lib/logger/system'; -import { UploadFS } from '../../../../server/ufs'; -import type { StoreOptions } from '../../../../server/ufs/ufs-store'; -import { WebdavClientAdapter } from '../../../webdav/server/lib/webdavClientAdapter'; - -type WebdavOptions = StoreOptions & { - connection: { - credentials: { - server: string; - username: string; - password: string; - }; - }; - uploadFolderPath: string; - getPath: (file: Omit) => string; -}; - -class WebdavStore extends UploadFS.Store { - protected getPath: (file: Omit) => string; - - constructor(options: WebdavOptions) { - super(options); - const { server, username, password } = options.connection.credentials; - const client = new WebdavClientAdapter(server, { username, password }); - - options.getPath = function (file) { - if (options.uploadFolderPath[options.uploadFolderPath.length - 1] !== '/') { - options.uploadFolderPath += '/'; - } - return options.uploadFolderPath + file._id; - }; - - client.stat(options.uploadFolderPath).catch((err) => { - if (err.message.toLowerCase() === 'not found') { - void client.createDirectory(options.uploadFolderPath); - } else if (err.message.toLowerCase() === 'unauthorized') { - console.warn('File upload is unauthorized to connect on Webdav, please verify your credentials'); - } - }); - - /** - * Returns the file path - * @param file - * @return {string} - */ - this.getPath = function (file) { - if (file.Webdav) { - return file.Webdav.path; - } - - return file._id; - }; - - /** - * Creates the file in the col lection - * @param file - * @param callback - * @return {string} - */ - this.create = async function (file) { - check(file, Object); - - if (file._id == null) { - file._id = Random.id(); - } - - file.Webdav = { - path: options.getPath(file as Omit), - }; - - file.store = this.options.name; - - return (await this.getCollection().insertOne(file)).insertedId; - }; - - /** - * Removes the file - * @param fileId - * @param callback - */ - this.delete = async function (fileId) { - const file = await this.getCollection().findOne({ _id: fileId }); - if (!file) { - throw new Error('File no found'); - } - - try { - return client.deleteFile(this.getPath(file)); - } catch (err: any) { - SystemLogger.error({ err }); - } - }; - - /** - * Returns the file read stream - * @param fileId - * @param file - * @param options - * @return {*} - */ - this.getReadStream = async function (_fileId, file, options = {}) { - const range: { - start?: number; - end?: number; - } = {}; - - if (options.start != null) { - range.start = options.start; - } - - if (options.end != null) { - range.end = options.end; - } - return client.createReadStream(this.getPath(file), options); - }; - - /** - * Returns the file write stream - * @param fileId - * @param file - * @return {*} - */ - this.getWriteStream = async function (_fileId, file) { - const writeStream = new stream.PassThrough(); - const webdavStream = client.createWriteStream(this.getPath(file), file.size || 0); - - // TODO remove timeout when UploadFS bug resolved - const newListenerCallback = (event: string, listener: () => any) => { - if (event === 'finish') { - process.nextTick(() => { - writeStream.removeListener(event, listener); - writeStream.removeListener('newListener', newListenerCallback); - writeStream.on(event, () => { - setTimeout(listener, 500); - }); - }); - } - }; - writeStream.on('newListener', newListenerCallback); - - writeStream.pipe(webdavStream); - return writeStream; - }; - } -} - -// Add store to UFS namespace -UploadFS.store.Webdav = WebdavStore; diff --git a/apps/meteor/app/github-enterprise/server/index.ts b/apps/meteor/app/github-enterprise/server/index.ts deleted file mode 100644 index cf327e4971bb2..0000000000000 --- a/apps/meteor/app/github-enterprise/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './lib'; diff --git a/apps/meteor/app/github-enterprise/server/lib.ts b/apps/meteor/app/github-enterprise/server/lib.ts deleted file mode 100644 index b4f67a1c3a50f..0000000000000 --- a/apps/meteor/app/github-enterprise/server/lib.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { OauthConfig } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; - -import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; -import { settings } from '../../settings/server'; - -// GitHub Enterprise Server CallBack URL needs to be http(s)://{rocketchat.server}[:port]/_oauth/github_enterprise -// In RocketChat -> Administration the URL needs to be http(s)://{github.enterprise.server}/ - -const config: OauthConfig = { - serverURL: '', - identityPath: '/api/v3/user', - authorizePath: '/login/oauth/authorize', - tokenPath: '/login/oauth/access_token', - addAutopublishFields: { - forLoggedInUser: ['services.github-enterprise'], - forOtherUsers: ['services.github-enterprise.username'], - }, -}; - -const GitHubEnterprise = new CustomOAuth('github_enterprise', config); - -Meteor.startup(() => { - settings.watch('API_GitHub_Enterprise_URL', (value) => { - config.serverURL = value; - GitHubEnterprise.configure(config); - }); -}); diff --git a/apps/meteor/app/github/server/index.ts b/apps/meteor/app/github/server/index.ts deleted file mode 100644 index cf327e4971bb2..0000000000000 --- a/apps/meteor/app/github/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './lib'; diff --git a/apps/meteor/app/github/server/lib.ts b/apps/meteor/app/github/server/lib.ts deleted file mode 100644 index 5409f7a809803..0000000000000 --- a/apps/meteor/app/github/server/lib.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { OauthConfig } from '@rocket.chat/core-typings'; - -import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; - -const config: OauthConfig = { - serverURL: 'https://github.com', - identityPath: 'https://api.github.com/user', - tokenPath: 'https://github.com/login/oauth/access_token', - emailPath: 'https://api.github.com/user/emails', - scope: 'user:email', - mergeUsers: false, - addAutopublishFields: { - forLoggedInUser: ['services.github'], - forOtherUsers: ['services.github.username'], - }, - accessTokenParam: 'access_token', - identityTokenSentVia: 'header', -}; - -export const Github = new CustomOAuth('github', config); diff --git a/apps/meteor/app/gitlab/server/index.ts b/apps/meteor/app/gitlab/server/index.ts deleted file mode 100644 index cf327e4971bb2..0000000000000 --- a/apps/meteor/app/gitlab/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './lib'; diff --git a/apps/meteor/app/gitlab/server/lib.ts b/apps/meteor/app/gitlab/server/lib.ts deleted file mode 100644 index 0f4d330cf5970..0000000000000 --- a/apps/meteor/app/gitlab/server/lib.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { OauthConfig } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; - -import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; -import { settings } from '../../settings/server'; - -const config: OauthConfig = { - serverURL: 'https://gitlab.com', - identityPath: '/api/v4/user', - scope: 'read_user', - mergeUsers: false, - addAutopublishFields: { - forLoggedInUser: ['services.gitlab'], - forOtherUsers: ['services.gitlab.username'], - }, - accessTokenParam: 'access_token', -}; - -const Gitlab = new CustomOAuth('gitlab', config); - -Meteor.startup(() => { - const updateConfig = _.debounce(() => { - config.serverURL = settings.get('API_Gitlab_URL').trim().replace(/\/*$/, '') || config.serverURL; - config.identityPath = settings.get('Accounts_OAuth_Gitlab_identity_path') || config.identityPath; - config.mergeUsers = Boolean(settings.get('Accounts_OAuth_Gitlab_merge_users')); - Gitlab.configure(config); - }, 300); - - settings.watchMultiple(['API_Gitlab_URL', 'Accounts_OAuth_Gitlab_identity_path', 'Accounts_OAuth_Gitlab_merge_users'], updateConfig); -}); diff --git a/apps/meteor/app/iframe-login/server/index.ts b/apps/meteor/app/iframe-login/server/index.ts deleted file mode 100644 index 6dbf8cb373253..0000000000000 --- a/apps/meteor/app/iframe-login/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './iframe_server'; diff --git a/apps/meteor/app/importer-csv/server/index.ts b/apps/meteor/app/importer-csv/server/index.ts deleted file mode 100644 index f20c375ae9435..0000000000000 --- a/apps/meteor/app/importer-csv/server/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { CsvImporter } from './CsvImporter'; -import { Importers } from '../../importer/server'; - -Importers.add({ - key: 'csv', - name: 'CSV', - importer: CsvImporter, -}); diff --git a/apps/meteor/app/importer-omnichannel-contacts/server/index.ts b/apps/meteor/app/importer-omnichannel-contacts/server/index.ts deleted file mode 100644 index 0ba882650bf07..0000000000000 --- a/apps/meteor/app/importer-omnichannel-contacts/server/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { License } from '@rocket.chat/license'; - -import { ContactImporter } from './ContactImporter'; -import { Importers } from '../../importer/server'; - -License.onValidFeature('contact-id-verification', () => { - Importers.add({ - key: 'omnichannel_contact', - name: 'omnichannel_contacts_importer', - importer: ContactImporter, - }); -}); diff --git a/apps/meteor/app/importer-pending-avatars/server/index.ts b/apps/meteor/app/importer-pending-avatars/server/index.ts deleted file mode 100644 index 2710c06b9d15b..0000000000000 --- a/apps/meteor/app/importer-pending-avatars/server/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { PendingAvatarImporter } from './PendingAvatarImporter'; -import { Importers } from '../../importer/server'; - -Importers.add({ - key: 'pending-avatars', - name: 'Pending Avatars', - visible: false, - importer: PendingAvatarImporter, -}); diff --git a/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts b/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts deleted file mode 100644 index 35b88c4267138..0000000000000 --- a/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts +++ /dev/null @@ -1,214 +0,0 @@ -import http from 'node:http'; -import https from 'node:https'; - -import { api } from '@rocket.chat/core-services'; -import type { IImport, MessageAttachment, IUpload, IImporterShortSelection } from '@rocket.chat/core-typings'; -import { Messages } from '@rocket.chat/models'; -import { Random } from '@rocket.chat/random'; - -import { FileUpload } from '../../file-upload/server'; -import { Importer, ProgressStep } from '../../importer/server'; -import type { ConverterOptions } from '../../importer/server/classes/ImportDataConverter'; -import type { ImporterProgress } from '../../importer/server/classes/ImporterProgress'; -import type { ImporterInfo } from '../../importer/server/definitions/ImporterInfo'; - -export class PendingFileImporter extends Importer { - constructor(info: ImporterInfo, importRecord: IImport, converterOptions: ConverterOptions = {}) { - super(info, importRecord, converterOptions); - } - - async prepareFileCount() { - this.logger.debug('start preparing import operation'); - await super.updateProgress(ProgressStep.PREPARING_STARTED); - - const fileCount = await Messages.countAllImportedMessagesWithFilesToDownload(); - if (fileCount === 0) { - await super.updateProgress(ProgressStep.DONE); - return 0; - } - - this.progress.count.total += fileCount; - await this.updateRecord({ - 'count.messages': fileCount, - 'count.total': fileCount, - 'messagesstatus': null, - 'status': ProgressStep.IMPORTING_FILES, - }); - this.reportProgress(); - - setImmediate(() => { - void this.startImport({}); - }); - - return fileCount; - } - - override async startImport(importSelection: IImporterShortSelection): Promise { - const downloadedFileIds: string[] = []; - const maxFileCount = 10; - const maxFileSize = 1024 * 1024 * 500; - - let count = 0; - let currentSize = 0; - let nextSize = 0; - - const waitForFiles = async () => { - if (count + 1 < maxFileCount && currentSize + nextSize < maxFileSize) { - return; - } - - return new Promise((resolve) => { - const handler = setInterval(() => { - if (count + 1 >= maxFileCount) { - return; - } - - if (currentSize + nextSize >= maxFileSize && count > 0) { - return; - } - - clearInterval(handler); - resolve(); - }, 1000); - }); - }; - - const completeFile = async (details: { size: number }) => { - await this.addCountCompleted(1); - count--; - currentSize -= details.size; - }; - - const logError = this.logger.error.bind(this.logger); - - try { - const pendingFileMessageList = Messages.findAllImportedMessagesWithFilesToDownload(); - const importedRoomIds = new Set(); - for await (const message of pendingFileMessageList) { - try { - const { _importFile } = message; - - if (!_importFile || _importFile.downloaded || downloadedFileIds.includes(_importFile.id)) { - await this.addCountCompleted(1); - continue; - } - - const url = _importFile.downloadUrl; - if (!url?.startsWith('http')) { - await this.addCountCompleted(1); - continue; - } - - const details: { message_id: string; name: string; size: number; userId: string; rid: string; type?: string } = { - message_id: `${message._id}-file-${_importFile.id}`, - name: _importFile.name || Random.id(), - size: _importFile.size || 0, - userId: message.u._id, - rid: message.rid, - }; - - const requestModule = /https/i.test(url) ? https : http; - const fileStore = FileUpload.getStore('Uploads'); - - nextSize = details.size; - await waitForFiles(); - count++; - currentSize += nextSize; - downloadedFileIds.push(_importFile.id); - - requestModule.get(url, (res) => { - const contentType = res.headers['content-type']; - if (!details.type && contentType) { - details.type = contentType; - } - - const rawData: Uint8Array[] = []; - res.on('data', (chunk) => { - rawData.push(chunk); - - // Update progress more often on large files - this.reportProgress(); - }); - res.on('error', async (err) => { - await completeFile(details); - logError({ err }); - }); - - res.on('end', async () => { - try { - // Bypass the fileStore filters - const file = await fileStore._doInsert(details, Buffer.concat(rawData)); - - const url = FileUpload.getPath(`${file._id}/${encodeURI(file.name || '')}`); - const attachment = this.getMessageAttachment(file, url); - - await Messages.setImportFileRocketChatAttachment(_importFile.id, url, attachment); - await completeFile(details); - importedRoomIds.add(message.rid); - } catch (err) { - await completeFile(details); - logError({ err }); - } - }); - }); - } catch (err) { - this.logger.error({ err }); - } - } - - void api.broadcast('notify.importedMessages', { roomIds: Array.from(importedRoomIds) }); - } catch (error) { - // If the cursor expired, restart the method - if (this.isCursorNotFoundError(error)) { - this.logger.info('CursorNotFound'); - return this.startImport(importSelection); - } - - await super.updateProgress(ProgressStep.ERROR); - throw error; - } - - await super.updateProgress(ProgressStep.DONE); - return this.getProgress(); - } - - getMessageAttachment(file: IUpload, url: string): MessageAttachment { - if (file.type) { - if (/^image\/.+/.test(file.type)) { - return { - title: file.name, - title_link: url, - image_url: url, - image_type: file.type, - image_size: file.size, - image_dimensions: file.identify ? file.identify.size : undefined, - }; - } - - if (/^audio\/.+/.test(file.type)) { - return { - title: file.name, - title_link: url, - audio_url: url, - audio_type: file.type, - audio_size: file.size, - }; - } - - if (/^video\/.+/.test(file.type)) { - return { - title: file.name, - title_link: url, - video_url: url, - video_type: file.type, - video_size: file.size, - }; - } - } - - return { - title: file.name, - title_link: url, - }; - } -} diff --git a/apps/meteor/app/importer-pending-files/server/index.ts b/apps/meteor/app/importer-pending-files/server/index.ts deleted file mode 100644 index e18b0de0a29e4..0000000000000 --- a/apps/meteor/app/importer-pending-files/server/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { PendingFileImporter } from './PendingFileImporter'; -import { Importers } from '../../importer/server'; - -Importers.add({ - key: 'pending-files', - name: 'Pending Files', - visible: false, - importer: PendingFileImporter, -}); diff --git a/apps/meteor/app/importer-slack-users/server/index.ts b/apps/meteor/app/importer-slack-users/server/index.ts deleted file mode 100644 index 67b2dbdb319a2..0000000000000 --- a/apps/meteor/app/importer-slack-users/server/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { SlackUsersImporter } from './SlackUsersImporter'; -import { Importers } from '../../importer/server'; - -Importers.add({ - key: 'slack-users', - name: 'Slack_Users', - importer: SlackUsersImporter, -}); diff --git a/apps/meteor/app/importer-slack/server/index.ts b/apps/meteor/app/importer-slack/server/index.ts deleted file mode 100644 index d32edde7271fb..0000000000000 --- a/apps/meteor/app/importer-slack/server/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { SlackImporter } from './SlackImporter'; -import { Importers } from '../../importer/server'; - -Importers.add({ - key: 'slack', - name: 'Slack', - importer: SlackImporter, -}); diff --git a/apps/meteor/app/importer/server/index.ts b/apps/meteor/app/importer/server/index.ts deleted file mode 100644 index 02949733174c5..0000000000000 --- a/apps/meteor/app/importer/server/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ProgressStep } from '../lib/ImporterProgressStep'; -import { Importer } from './classes/Importer'; -import { ImporterSelection } from './classes/ImporterSelection'; -import { SelectionChannel } from './classes/ImporterSelectionChannel'; -import { SelectionUser } from './classes/ImporterSelectionUser'; -import { ImporterWebsocket } from './classes/ImporterWebsocket'; -import { ImportersContainer } from './classes/ImportersContainer'; -import './methods'; -import './startup/setImportsToInvalid'; -import './startup/store'; - -export { Importer, ImporterWebsocket, ProgressStep, ImporterSelection as Selection, SelectionChannel, SelectionUser }; - -export const Importers = new ImportersContainer(); - -Importers.add({ - key: 'api', - name: 'API', - visible: false, - importer: Importer, -}); diff --git a/apps/meteor/app/importer/server/methods/index.ts b/apps/meteor/app/importer/server/methods/index.ts deleted file mode 100644 index 0136565a1185f..0000000000000 --- a/apps/meteor/app/importer/server/methods/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './getImportProgress'; -export * from './startImport'; -export * from './uploadImportFile'; -export * from './getImportFileData'; -export * from './downloadPublicImportFile'; -export * from './getLatestImportOperations'; diff --git a/apps/meteor/app/integrations/server/api/api.ts b/apps/meteor/app/integrations/server/api/api.ts deleted file mode 100644 index de016532e92e2..0000000000000 --- a/apps/meteor/app/integrations/server/api/api.ts +++ /dev/null @@ -1,554 +0,0 @@ -import type { IIncomingIntegration, IIntegration, IOutgoingIntegration, IUser, RequiredField } from '@rocket.chat/core-typings'; -import { Integrations, Users } from '@rocket.chat/models'; -import { Random } from '@rocket.chat/random'; -import { isIntegrationsHooksAddSchema, isIntegrationsHooksRemoveSchema } from '@rocket.chat/rest-typings'; -import type express from 'express'; -import { Meteor } from 'meteor/meteor'; -import type { RateLimiterOptionsToCheck } from 'meteor/rate-limit'; -import { WebApp } from 'meteor/webapp'; -import _ from 'underscore'; - -import { isPlainObject } from '../../../../lib/utils/isPlainObject'; -import { APIClass } from '../../../api/server/ApiClass'; -import type { RateLimiterOptions } from '../../../api/server/api'; -import { API, defaultRateLimiterOptions } from '../../../api/server/api'; -import type { FailureResult, GenericRouteExecutionContext, SuccessResult, UnavailableResult } from '../../../api/server/definition'; -import { loggerMiddleware } from '../../../api/server/middlewares/logger'; -import { metricsMiddleware } from '../../../api/server/middlewares/metrics'; -import { tracerSpanMiddleware } from '../../../api/server/middlewares/tracer'; -import type { APIActionContext } from '../../../api/server/router'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import type { WebhookResponseItem } from '../../../lib/server/functions/processWebhookMessage'; -import { processWebhookMessage } from '../../../lib/server/functions/processWebhookMessage'; -import { metrics } from '../../../metrics/server'; -import { settings } from '../../../settings/server'; -import { IsolatedVMScriptEngine } from '../lib/isolated-vm/isolated-vm'; -import { incomingLogger, integrationLogger } from '../logger'; -import { addOutgoingIntegration } from '../methods/outgoing/addOutgoingIntegration'; -import { deleteOutgoingIntegration } from '../methods/outgoing/deleteOutgoingIntegration'; - -const ivmEngine = new IsolatedVMScriptEngine(true); - -function getEngine(_integration: IIntegration): IsolatedVMScriptEngine { - return ivmEngine; -} - -type IntegrationOptions = { - event: string; - name: string; - target_url: string; - data?: { - channel_name?: string; - trigger_words?: string[]; - username?: string; - }; -}; - -type IntegrationThis = GenericRouteExecutionContext & { - request: Request & { - integration: IIncomingIntegration; - }; - user: RequiredField; -}; - -async function createIntegration(options: IntegrationOptions, user: IUser): Promise { - incomingLogger.info({ msg: 'Add integration', integration: options.name }); - incomingLogger.debug({ options }); - - switch (options.event) { - case 'newMessageOnChannel': - if (options.data == null) { - options.data = {}; - } - if (options.data.channel_name?.indexOf('#') === -1) { - options.data.channel_name = `#${options.data.channel_name}`; - } - return addOutgoingIntegration(user._id, { - username: 'rocket.cat', - urls: [options.target_url], - name: options.name, - channel: options.data.channel_name, - triggerWords: options.data.trigger_words, - type: 'webhook-outgoing', - event: 'sendMessage', - token: Random.id(24), - scriptEnabled: false, - script: '', - enabled: true, - _id: Random.id(), - _updatedAt: new Date(), - }); - case 'newMessageToUser': - if (options.data?.username?.indexOf('@') === -1) { - options.data.username = `@${options.data.username}`; - } - if (!options.data?.username) { - throw new Error('username-required'); - } - - return addOutgoingIntegration(user._id, { - username: 'rocket.cat', - urls: [options.target_url], - name: options.name, - channel: options.data.username, - triggerWords: options.data.trigger_words, - _id: '', - type: 'webhook-outgoing', - token: '', - scriptEnabled: false, - script: '', - enabled: false, - _updatedAt: new Date(), - event: 'sendMessage', - }); - } -} - -async function removeIntegration(options: { target_url: string }, user: IUser): Promise | FailureResult> { - incomingLogger.info('Remove integration'); - incomingLogger.debug({ options }); - - const integrationToRemove = await Integrations.findOneByUrl(options.target_url); - if (!integrationToRemove) { - return API.v1.failure('integration-not-found'); - } - - await deleteOutgoingIntegration(integrationToRemove._id, user._id); - - return API.v1.success(); -} - -/** - * Slack/GitHub-style webhooks send JSON wrapped in a `payload` field - * with Content-Type: application/x-www-form-urlencoded (e.g. `payload={"text":"hello"}`). - * This function unwraps it so integrations receive the parsed JSON directly. - */ -function getBodyParams(bodyParams: unknown, request: Request): Record { - if (!isPlainObject(bodyParams)) { - return {}; - } - - if ( - request.headers.get('content-type')?.startsWith('application/x-www-form-urlencoded') && - Object.keys(bodyParams).length === 1 && - typeof bodyParams.payload === 'string' - ) { - try { - const parsed = JSON.parse(bodyParams.payload); - - // Valid JSON must be an object, not an array or primitive - if (!isPlainObject(parsed)) { - throw new Error('Integration payload must be a JSON object, not an array or primitive'); - } - - return parsed; - } catch (err) { - // Invalid JSON -> return original bodyParams (backward compatibility) - if (err instanceof SyntaxError) { - return bodyParams; - } - throw err; - } - } - - return bodyParams; -} - -async function executeIntegrationRest( - this: IntegrationThis, -): Promise< - | SuccessResult | { responses: WebhookResponseItem[] } | undefined | void> - | FailureResult - | FailureResult<{ responses: WebhookResponseItem[] }> - | UnavailableResult -> { - incomingLogger.info({ msg: 'Post integration:', integration: this.request.integration.name }); - incomingLogger.debug({ urlParams: this.urlParams, bodyParams: this.bodyParams }); - - if (this.request.integration.enabled !== true) { - return API.v1.unavailable('Service Unavailable'); - } - - if (!(await hasPermissionAsync(this.user._id, 'message-impersonate'))) { - incomingLogger.error({ - msg: 'Error trying to execute integration', - integration: this.request.integration.name, - error: 'The assigned user from the integration must have the message-impersonate permission.', - }); - return API.v1.failure('error-user-lacks-message-impersonate-permission'); - } - - const defaultValues = { - channel: this.request.integration.channel, - alias: this.request.integration.alias || '', - avatar: this.request.integration.avatar || '', - emoji: this.request.integration.emoji || '', - }; - - const scriptEngine = getEngine(this.request.integration); - - let bodyParams: Record; - try { - bodyParams = getBodyParams(this.bodyParams, this.request); - } catch (err) { - return API.v1.failure(err instanceof Error ? err.message : String(err)); - } - - const separateResponse = bodyParams.separateResponse === true; - let scriptResponse: Record | undefined; - - if (scriptEngine.integrationHasValidScript(this.request.integration) && this.request.body) { - const buffers = []; - const reader = this.request.body.getReader(); - for (let result = await reader.read(); !result.done; result = await reader.read()) { - buffers.push(result.value); - } - const contentRaw = Buffer.concat(buffers).toString('utf8'); - const protocol = `${this.request.headers.get('x-forwarded-proto')}:` || 'http:'; - const url = new URL(this.request.url, `${protocol}//${this.request.headers.get('host')}`); - const query = isPlainObject(this.queryParams) ? this.queryParams : {}; - - const request = { - url: { - query, - hash: url.hash, - search: url.search, - pathname: url.pathname, - path: this.request.url, - }, - url_raw: this.request.url, - url_params: this.urlParams, - content: bodyParams, - content_raw: contentRaw, - headers: Object.fromEntries(this.request.headers.entries()), - body: bodyParams, - user: { - _id: this.user._id, - name: this.user.name || '', - username: this.user.username, - }, - }; - - const result = await scriptEngine.processIncomingRequest({ - integration: this.request.integration, - request, - }); - - try { - if (!result) { - incomingLogger.debug({ - msg: 'Process Incoming Request result of Trigger has no data', - integration: this.request.integration.name, - }); - return API.v1.success(); - } - if (result?.error) { - return API.v1.failure(result.error); - } - - bodyParams = result?.content; - - if (!('separateResponse' in bodyParams)) { - bodyParams.separateResponse = separateResponse; - } - - scriptResponse = result.response; - if (result.user) { - this.user = result.user; - } - - incomingLogger.debug({ - msg: 'Process Incoming Request result of Trigger', - integration: this.request.integration.name, - result: bodyParams, - }); - } catch (err) { - incomingLogger.error({ - msg: 'Error running Script in Trigger', - integration: this.request.integration.name, - script: this.request.integration.scriptCompiled, - err, - }); - return API.v1.failure('error-running-script'); - } - } - - if (!bodyParams || (_.isEmpty(bodyParams) && !this.request.integration.scriptEnabled)) { - return API.v1.success(); - } - - if ((bodyParams.channel || bodyParams.roomId) && !this.request.integration.overrideDestinationChannelEnabled) { - return API.v1.failure('overriding destination channel is disabled for this integration'); - } - - bodyParams.bot = { i: this.request.integration._id }; - - try { - const messageResponse = await processWebhookMessage(bodyParams, this.user, defaultValues); - if (_.isEmpty(messageResponse)) { - return API.v1.failure('unknown-error'); - } - - if (scriptResponse) { - incomingLogger.debug({ msg: 'response', response: scriptResponse }); - return API.v1.success(scriptResponse); - } - if (bodyParams.separateResponse) { - const allFailed = messageResponse.every((response) => 'error' in response && response.error); - if (allFailed) { - return API.v1.failure({ responses: messageResponse }); - } - return API.v1.success({ responses: messageResponse }); - } - return API.v1.success(); - } catch (err: any) { - incomingLogger.error({ msg: 'Error processing webhook message', err }); - return API.v1.failure(err?.error || err?.message || 'Unknown error'); - } -} - -type IntegrationSampleBody = { - token: string; - channel_id: string; - channel_name: string; - timestamp: Date; - user_id: string; - user_name: string; - text: string; - trigger_word: string; -}; - -function integrationSampleRest(): { statusCode: number; body: IntegrationSampleBody[] } { - incomingLogger.info('Sample Integration'); - return { - statusCode: 200, - body: [ - { - token: Random.id(24), - channel_id: Random.id(), - channel_name: 'general', - timestamp: new Date(), - user_id: Random.id(), - user_name: 'rocket.cat', - text: 'Sample text 1', - trigger_word: 'Sample', - }, - { - token: Random.id(24), - channel_id: Random.id(), - channel_name: 'general', - timestamp: new Date(), - user_id: Random.id(), - user_name: 'rocket.cat', - text: 'Sample text 2', - trigger_word: 'Sample', - }, - { - token: Random.id(24), - channel_id: Random.id(), - channel_name: 'general', - timestamp: new Date(), - user_id: Random.id(), - user_name: 'rocket.cat', - text: 'Sample text 3', - trigger_word: 'Sample', - }, - ], - }; -} - -function integrationInfoRest(): { statusCode: number; body: { success: boolean } } { - incomingLogger.info('Info integration'); - return { - statusCode: 200, - body: { - success: true, - }, - }; -} - -class WebHookAPI extends APIClass<'/hooks'> { - override async authenticatedRoute(routeContext: APIActionContext): Promise { - const { integrationId, token } = routeContext.urlParams; - const integration = await Integrations.findOneByIdAndToken(integrationId, decodeURIComponent(token)); - - if (!integration) { - incomingLogger.info({ msg: 'Invalid integration id or token', integrationId, token }); - - throw new Error('Invalid integration id or token provided.'); - } - - routeContext.request.headers.set('x-auth-token', token); - - const req = routeContext.request as Request & { integration?: IIncomingIntegration }; - req.integration = integration; - - return Users.findOneById(req.integration.userId); - } - - override shouldAddRateLimitToRoute(options: { rateLimiterOptions?: RateLimiterOptions | boolean }): boolean { - const { rateLimiterOptions } = options; - return ( - (typeof rateLimiterOptions === 'object' || rateLimiterOptions === undefined) && - !process.env.TEST_MODE && - Boolean(defaultRateLimiterOptions.numRequestsAllowed && defaultRateLimiterOptions.intervalTimeInMS) - ); - } - - override async shouldVerifyRateLimit(): Promise { - return ( - settings.get('API_Enable_Rate_Limiter') === true && - (process.env.NODE_ENV !== 'development' || settings.get('API_Enable_Rate_Limiter_Dev') === true) - ); - } - - override async enforceRateLimit( - objectForRateLimitMatch: RateLimiterOptionsToCheck, - request: Request, - response: Response, - userId: string, - ): Promise { - const { method, url } = request; - const route = url.replace(`/${this.apiPath}`, ''); - const nameRoute = this.getFullRouteName(route, method.toLowerCase()); - if (!this.getRateLimiter(nameRoute)) { - this.addRateLimiterRuleForRoutes({ - routes: [route], - rateLimiterOptions: defaultRateLimiterOptions, - endpoints: { - post: 'executeIntegrationRest', - get: 'executeIntegrationRest', - }, - }); - } - - const integrationForRateLimitMatch = objectForRateLimitMatch; - integrationForRateLimitMatch.route = nameRoute; - - await super.enforceRateLimit(integrationForRateLimitMatch, request, response, userId); - } -} - -const Api = new WebHookAPI({ - enableCors: true, - apiPath: 'hooks/', - useDefaultAuth: false, - prettyJson: process.env.NODE_ENV === 'development', -}); - -Api.router - .use( - metricsMiddleware({ - basePathRegex: new RegExp(/^\/hooks\//), - api: Api, - settings, - endpointTimeSummary: metrics.rocketchatRestApi, - endpointTimeHistogram: metrics.rocketchatRestApiSeconds, - responseSizeHistogram: metrics.rocketchatRestApiResponseSizeBytes, - activeRequestsGauge: metrics.rocketchatRestApiActiveRequests, - }), - ) - .use(tracerSpanMiddleware) - .use(loggerMiddleware(integrationLogger)); - -Api.addRoute( - ':integrationId/:userId/:token', - { authRequired: true }, - { - post: executeIntegrationRest, - get: executeIntegrationRest, - }, -); - -Api.addRoute( - ':integrationId/:token', - { authRequired: true }, - { - post: executeIntegrationRest, - get: executeIntegrationRest, - }, -); - -Api.addRoute( - 'sample/:integrationId/:userId/:token', - { authRequired: true }, - { - get: integrationSampleRest, - }, -); - -Api.addRoute( - 'sample/:integrationId/:token', - { authRequired: true }, - { - get: integrationSampleRest, - }, -); - -Api.addRoute( - 'info/:integrationId/:userId/:token', - { authRequired: true }, - { - get: integrationInfoRest, - }, -); - -Api.addRoute( - 'info/:integrationId/:token', - { authRequired: true }, - { - get: integrationInfoRest, - }, -); - -Api.addRoute( - 'add/:integrationId/:userId/:token', - { authRequired: true, validateParams: isIntegrationsHooksAddSchema }, - { - async post() { - const result = await createIntegration(this.bodyParams, this.user); - - return API.v1.success(result || {}); - }, - }, -); - -Api.addRoute( - 'add/:integrationId/:token', - { authRequired: true, validateParams: isIntegrationsHooksAddSchema }, - { - async post() { - const result = await createIntegration(this.bodyParams, this.user); - - return API.v1.success(result || {}); - }, - }, -); - -Api.addRoute( - 'remove/:integrationId/:userId/:token', - { authRequired: true, validateParams: isIntegrationsHooksRemoveSchema }, - { - async post() { - const result = await removeIntegration(this.bodyParams, this.user); - - return API.v1.success(result || {}); - }, - }, -); - -Api.addRoute( - 'remove/:integrationId/:token', - { authRequired: true, validateParams: isIntegrationsHooksRemoveSchema }, - { - async post() { - const result = await removeIntegration(this.bodyParams, this.user); - - return API.v1.success(result || {}); - }, - }, -); - -Meteor.startup(() => { - (WebApp.rawConnectHandlers as unknown as ReturnType).use(Api.router.router); -}); diff --git a/apps/meteor/app/integrations/server/index.ts b/apps/meteor/app/integrations/server/index.ts deleted file mode 100644 index ea3dbd71dd6ee..0000000000000 --- a/apps/meteor/app/integrations/server/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import './logger'; -import './lib/validateOutgoingIntegration'; -import './methods/incoming/addIncomingIntegration'; -import './methods/incoming/updateIncomingIntegration'; -import './methods/incoming/deleteIncomingIntegration'; -import './methods/outgoing/addOutgoingIntegration'; -import './methods/outgoing/updateOutgoingIntegration'; -import './methods/outgoing/replayOutgoingIntegration'; -import './methods/outgoing/deleteOutgoingIntegration'; -import './methods/clearIntegrationHistory'; -import './api/api'; -import './lib/triggerHandler'; -import './triggers'; -import './startup'; diff --git a/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts b/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts deleted file mode 100644 index a30bb9b5ee9a3..0000000000000 --- a/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Integrations, IntegrationHistory } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import notifications from '../../../notifications/server/lib/Notifications'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - clearIntegrationHistory(integrationId: string): Promise; - } -} - -Meteor.methods({ - async clearIntegrationHistory(integrationId) { - let integration; - - if (!this.userId) { - throw new Meteor.Error('not_authorized', 'Unauthorized', { - method: 'clearIntegrationHistory', - }); - } - - if (await hasPermissionAsync(this.userId, 'manage-outgoing-integrations')) { - integration = await Integrations.findOneById(integrationId); - } else if (await hasPermissionAsync(this.userId, 'manage-own-outgoing-integrations')) { - integration = await Integrations.findOne({ - '_id': integrationId, - '_createdBy._id': this.userId, - }); - } else { - throw new Meteor.Error('not_authorized', 'Unauthorized', { - method: 'clearIntegrationHistory', - }); - } - - if (!integration) { - throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { - method: 'clearIntegrationHistory', - }); - } - - // Don't sending to IntegrationHistory listener since it don't waits for 'removed' events. - await IntegrationHistory.removeByIntegrationId(integrationId); - - notifications.streamIntegrationHistory.emit(integrationId, { type: 'removed', id: integrationId }); - - return true; - }, -}); diff --git a/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts deleted file mode 100644 index 3258aeed8aebc..0000000000000 --- a/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Integrations } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; -import { notifyOnIntegrationChangedById } from '../../../../lib/server/lib/notifyListener'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - deleteIncomingIntegration(integrationId: string): Promise; - } -} - -export const deleteIncomingIntegration = async (integrationId: string, userId: string): Promise => { - let integration; - - if (userId && (await hasPermissionAsync(userId, 'manage-incoming-integrations'))) { - integration = Integrations.findOneById(integrationId); - } else if (userId && (await hasPermissionAsync(userId, 'manage-own-incoming-integrations'))) { - integration = Integrations.findOne({ - '_id': integrationId, - '_createdBy._id': userId, - }); - } else { - throw new Meteor.Error('not_authorized', 'Unauthorized', { - method: 'deleteIncomingIntegration', - }); - } - - if (!(await integration)) { - throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { - method: 'deleteIncomingIntegration', - }); - } - - await Integrations.removeById(integrationId); - void notifyOnIntegrationChangedById(integrationId, 'removed'); -}; - -Meteor.methods({ - async deleteIncomingIntegration(integrationId) { - methodDeprecationLogger.method('deleteIncomingIntegration', '9.0.0', '/v1/integrations.remove'); - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error('not_authorized', 'Unauthorized', { - method: 'deleteIncomingIntegration', - }); - } - - await deleteIncomingIntegration(integrationId, userId); - - return true; - }, -}); diff --git a/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts deleted file mode 100644 index b9e44d7d5adbd..0000000000000 --- a/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Integrations, IntegrationHistory } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; -import { notifyOnIntegrationChangedById } from '../../../../lib/server/lib/notifyListener'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - deleteOutgoingIntegration(integrationId: string): Promise; - } -} - -export const deleteOutgoingIntegration = async (integrationId: string, userId: string): Promise => { - let integration; - - if (!userId) { - throw new Meteor.Error('not_authorized', 'Unauthorized', { - method: 'deleteOutgoingIntegration', - }); - } - - if (await hasPermissionAsync(userId, 'manage-outgoing-integrations')) { - integration = Integrations.findOneById(integrationId); - } else if (await hasPermissionAsync(userId, 'manage-own-outgoing-integrations')) { - integration = Integrations.findOne({ - '_id': integrationId, - '_createdBy._id': userId, - }); - } else { - throw new Meteor.Error('not_authorized', 'Unauthorized', { - method: 'deleteOutgoingIntegration', - }); - } - - if (!(await integration)) { - throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { - method: 'deleteOutgoingIntegration', - }); - } - - await Integrations.removeById(integrationId); - // Don't sending to IntegrationHistory listener since it don't waits for 'removed' events. - await IntegrationHistory.removeByIntegrationId(integrationId); - void notifyOnIntegrationChangedById(integrationId, 'removed'); -}; - -Meteor.methods({ - async deleteOutgoingIntegration(integrationId) { - methodDeprecationLogger.method('deleteOutgoingIntegration', '9.0.0', '/v1/integrations.remove'); - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error('not_authorized', 'Unauthorized', { - method: 'deleteOutgoingIntegration', - }); - } - - await deleteOutgoingIntegration(integrationId, userId); - - return true; - }, -}); diff --git a/apps/meteor/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts deleted file mode 100644 index c8108bfc2acda..0000000000000 --- a/apps/meteor/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { IOutgoingIntegration } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Integrations, IntegrationHistory } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { triggerHandler } from '../../lib/triggerHandler'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - replayOutgoingIntegration(params: { integrationId: string; historyId: string }): Promise; - } -} - -Meteor.methods({ - async replayOutgoingIntegration({ integrationId, historyId }) { - let integration: IOutgoingIntegration | null = null; - - if (!this.userId) { - throw new Meteor.Error('not_authorized', 'Unauthorized', { - method: 'replayOutgoingIntegration', - }); - } - - if (await hasPermissionAsync(this.userId, 'manage-outgoing-integrations')) { - integration = await Integrations.findOneById(integrationId); - } else if (await hasPermissionAsync(this.userId, 'manage-own-outgoing-integrations')) { - const foundIntegration = await Integrations.findOne({ - '_id': integrationId, - '_createdBy._id': this.userId, - }); - - if (foundIntegration && 'event' in foundIntegration) { - integration = foundIntegration; - } - } - - if (!integration) { - throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { - method: 'replayOutgoingIntegration', - }); - } - - const history = await IntegrationHistory.findOneByIntegrationIdAndHistoryId(integration._id, historyId); - - if (!history) { - throw new Meteor.Error('error-invalid-integration-history', 'Invalid Integration History', { - method: 'replayOutgoingIntegration', - }); - } - - await triggerHandler.replay(integration, history); - - return true; - }, -}); diff --git a/apps/meteor/app/integrations/server/triggers.ts b/apps/meteor/app/integrations/server/triggers.ts deleted file mode 100644 index adf3d5b8bdf4d..0000000000000 --- a/apps/meteor/app/integrations/server/triggers.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { triggerHandler } from './lib/triggerHandler'; -import { callbacks } from '../../../server/lib/callbacks'; -import { afterLeaveRoomCallback } from '../../../server/lib/callbacks/afterLeaveRoomCallback'; - -const callbackHandler = function _callbackHandler(eventType: string) { - return function _wrapperFunction(...args: any[]) { - return triggerHandler.executeTriggers(eventType, ...args); - }; -}; - -callbacks.add( - 'afterSaveMessage', - (message, { room }) => callbackHandler('sendMessage')(message, room), - callbacks.priority.LOW, - 'integrations-sendMessage', -); -callbacks.add('afterCreateChannel', callbackHandler('roomCreated'), callbacks.priority.LOW, 'integrations-roomCreated'); -callbacks.add('afterCreatePrivateGroup', callbackHandler('roomCreated'), callbacks.priority.LOW, 'integrations-roomCreated'); -callbacks.add('afterCreateUser', callbackHandler('userCreated'), callbacks.priority.LOW, 'integrations-userCreated'); -callbacks.add('afterJoinRoom', callbackHandler('roomJoined'), callbacks.priority.LOW, 'integrations-roomJoined'); -afterLeaveRoomCallback.add(callbackHandler('roomLeft'), callbacks.priority.LOW, 'integrations-roomLeft'); -callbacks.add('afterRoomArchived', callbackHandler('roomArchived'), callbacks.priority.LOW, 'integrations-roomArchived'); -callbacks.add('afterFileUpload', callbackHandler('fileUploaded'), callbacks.priority.LOW, 'integrations-fileUploaded'); diff --git a/apps/meteor/app/invites/server/functions/removeInvite.ts b/apps/meteor/app/invites/server/functions/removeInvite.ts deleted file mode 100644 index c754d82d071cc..0000000000000 --- a/apps/meteor/app/invites/server/functions/removeInvite.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { IInvite } from '@rocket.chat/core-typings'; -import { Invites } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; - -export const removeInvite = async (userId: string, invite: Pick) => { - if (!userId || !invite) { - return false; - } - - if (!(await hasPermissionAsync(userId, 'create-invite-links'))) { - throw new Meteor.Error('not_authorized'); - } - - if (!invite._id) { - throw new Meteor.Error('error-the-field-is-required', 'The field _id is required', { - method: 'removeInvite', - field: '_id', - }); - } - - // Before anything, let's check if there's an existing invite - const existing = await Invites.findOneById(invite._id); - - if (!existing) { - throw new Meteor.Error('invalid-invitation-id', 'Invalid Invitation _id', { - method: 'removeInvite', - }); - } - - await Invites.removeById(invite._id); - - return true; -}; diff --git a/apps/meteor/app/irc/server/irc-bridge/index.js b/apps/meteor/app/irc/server/irc-bridge/index.js deleted file mode 100644 index 25f6b5b9f0a29..0000000000000 --- a/apps/meteor/app/irc/server/irc-bridge/index.js +++ /dev/null @@ -1,239 +0,0 @@ -import { Logger } from '@rocket.chat/logger'; -import { Settings } from '@rocket.chat/models'; -import moment from 'moment'; -import Queue from 'queue-fifo'; - -import { withThrottling } from '../../../../lib/utils/highOrderFunctions'; -import { callbacks } from '../../../../server/lib/callbacks'; -import { afterLeaveRoomCallback } from '../../../../server/lib/callbacks/afterLeaveRoomCallback'; -import { afterLogoutCleanUpCallback } from '../../../../server/lib/callbacks/afterLogoutCleanUpCallback'; -import { updateAuditedBySystem } from '../../../../server/settings/lib/auditedSettingUpdates'; -import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener'; -import * as servers from '../servers'; -import * as localCommandHandlers from './localHandlers'; -import * as peerCommandHandlers from './peerHandlers'; - -const logger = new Logger('IRC Bridge'); -const queueLogger = logger.section('Queue'); - -let removed = false; -const updateLastPing = withThrottling({ wait: 10_000 })(() => { - if (removed) { - return; - } - - void (async () => { - const updatedValue = await updateAuditedBySystem({ - reason: 'updateLastPing', - })(Settings.updateValueById, 'IRC_Bridge_Last_Ping', new Date(), { upsert: true }); - if (updatedValue.modifiedCount || updatedValue.upsertedCount) { - void notifyOnSettingChangedById('IRC_Bridge_Last_Ping'); - } - })(); -}); - -class Bridge { - constructor(config) { - // General - this.config = config; - - // Workaround for Rocket.Chat callbacks being called multiple times - this.loggedInUsers = []; - - // Server - const Server = servers[this.config.server.protocol]; - - this.server = new Server(this.config); - - this.setupPeerHandlers(); - this.setupLocalHandlers(); - - // Command queue - this.queue = new Queue(); - this.queueTimeout = 5; - } - - async init() { - this.initTime = new Date(); - removed = false; - this.loggedInUsers = []; - - const lastPing = await Settings.findOneById('IRC_Bridge_Last_Ping'); - if (lastPing) { - if (Math.abs(moment(lastPing.value).diff()) < 1000 * 30) { - this.log('Not trying to connect.'); - this.remove(); - return; - } - } - - this.log('Connecting.'); - updateLastPing(); - this.server.register(); - - this.server.on('registered', () => { - this.logQueue('Starting...'); - - this.runQueue(); - }); - } - - stop() { - this.server.disconnect(); - } - - remove() { - this.log('Removing current connection.'); - removed = true; - this.server = null; - this.removeLocalHandlers(); - } - - /** - * Log helper - */ - log(message) { - // TODO logger: debug? - logger.info(message); - } - - logQueue(message) { - // TODO logger: debug? - queueLogger.info(message); - } - - /** - * - * - * Queue - * - * - */ - onMessageReceived(from, command, ...parameters) { - this.queue.enqueue({ from, command, parameters }); - } - - async runQueue() { - if (!this.server) { - return; - } - - const lastResetTime = Settings.findOneById('IRC_Bridge_Reset_Time'); - if (lastResetTime && lastResetTime.value > this.initTime) { - this.stop(); - this.remove(); - return; - } - - updateLastPing(); - - // If it is empty, skip and keep the queue going - if (this.queue.isEmpty()) { - return setTimeout(this.runQueue.bind(this), this.queueTimeout); - } - - // Get the command - const item = this.queue.dequeue(); - - this.logQueue({ msg: 'Processing command from source', command: item.command, from: item.from }); - - // Handle the command accordingly - try { - // Handle the command accordingly - switch (item.from) { - case 'local': - if (!localCommandHandlers[item.command]) { - throw new Error(`Could not find handler for local:${item.command}`); - } - - await localCommandHandlers[item.command].apply(this, item.parameters); - break; - case 'peer': - if (!peerCommandHandlers[item.command]) { - throw new Error(`Could not find handler for peer:${item.command}`); - } - - await peerCommandHandlers[item.command].apply(this, item.parameters); - break; - } - } catch (e) { - this.logQueue(e); - } - - // Keep the queue going - setTimeout(this.runQueue.bind(this), this.queueTimeout); - } - - /** - * - * - * Peer - * - * - */ - setupPeerHandlers() { - this.server.on('peerCommand', (cmd) => { - this.onMessageReceived('peer', cmd.identifier, cmd.args); - }); - } - - /** - * - * - * Local - * - * - */ - setupLocalHandlers() { - // Auth - callbacks.add('afterValidateLogin', this.onMessageReceived.bind(this, 'local', 'onLogin'), callbacks.priority.LOW, 'irc-on-login'); - callbacks.add( - 'afterCreateUser', - this.onMessageReceived.bind(this, 'local', 'onCreateUser'), - callbacks.priority.LOW, - 'irc-on-create-user', - ); - // Joining rooms or channels - callbacks.add( - 'afterCreateChannel', - this.onMessageReceived.bind(this, 'local', 'onCreateRoom'), - callbacks.priority.LOW, - 'irc-on-create-channel', - ); - callbacks.add( - 'afterCreateRoom', - this.onMessageReceived.bind(this, 'local', 'onCreateRoom'), - callbacks.priority.LOW, - 'irc-on-create-room', - ); - callbacks.add('afterJoinRoom', this.onMessageReceived.bind(this, 'local', 'onJoinRoom'), callbacks.priority.LOW, 'irc-on-join-room'); - // Leaving rooms or channels - afterLeaveRoomCallback.add(this.onMessageReceived.bind(this, 'local', 'onLeaveRoom'), callbacks.priority.LOW, 'irc-on-leave-room'); - // Chatting - callbacks.add( - 'afterSaveMessage', - (message, { room }) => this.onMessageReceived('local', 'onSaveMessage', message, room), - callbacks.priority.LOW, - 'irc-on-save-message', - ); - // Leaving - afterLogoutCleanUpCallback.add(this.onMessageReceived.bind(this, 'local', 'onLogout'), callbacks.priority.LOW, 'irc-on-logout'); - } - - removeLocalHandlers() { - callbacks.remove('afterValidateLogin', 'irc-on-login'); - callbacks.remove('afterCreateUser', 'irc-on-create-user'); - callbacks.remove('afterCreateChannel', 'irc-on-create-channel'); - callbacks.remove('afterCreateRoom', 'irc-on-create-room'); - callbacks.remove('afterJoinRoom', 'irc-on-join-room'); - afterLeaveRoomCallback.remove('irc-on-leave-room'); - callbacks.remove('afterSaveMessage', 'irc-on-save-message'); - afterLogoutCleanUpCallback.remove('irc-on-logout'); - } - - sendCommand(command, parameters) { - this.server.emit('onReceiveFromLocal', command, parameters); - } -} - -export default Bridge; diff --git a/apps/meteor/app/lib/server/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts deleted file mode 100644 index 7b871e2d37a9d..0000000000000 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { Apps, AppEvents } from '@rocket.chat/apps'; -import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; -import { Message, Team, Room } from '@rocket.chat/core-services'; -import { isBannedSubscription, isRoomNativeFederated, type IUser } from '@rocket.chat/core-typings'; -import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; -import { callbacks } from '../../../../server/lib/callbacks'; -import { beforeAddUserToRoom } from '../../../../server/lib/callbacks/beforeAddUserToRoom'; -import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; -import { settings } from '../../../settings/server'; -import { beforeAddUserToRoom as beforeAddUserToRoomPatch } from '../lib/beforeAddUserToRoom'; -import { notifyOnRoomChangedById, notifyOnSubscriptionChanged } from '../lib/notifyListener'; - -/** - * This function adds user to the given room. - * Caution - It does not validates if the user has permission to join room - */ - -export const addUserToRoom = async ( - rid: string, - user: Pick, - inviter?: Pick, - { - skipSystemMessage, - skipAlertSound, - createAsHidden = false, - }: { - skipSystemMessage?: boolean; - skipAlertSound?: boolean; - createAsHidden?: boolean; - } = {}, -): Promise => { - const now = new Date(); - const room = await Rooms.findOneById(rid); - - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { - method: 'addUserToRoom', - }); - } - - const userToBeAdded = await Users.findOneById(user._id); - const roomDirectives = roomCoordinator.getRoomDirectives(room.t); - - if (!userToBeAdded) { - throw new Meteor.Error('user-not-found'); - } - - if ( - !(await roomDirectives.allowMemberAction(room, RoomMemberActions.JOIN, userToBeAdded._id)) && - !(await roomDirectives.allowMemberAction(room, RoomMemberActions.INVITE, userToBeAdded._id)) - ) { - return; - } - - try { - const inviterUser = inviter && ((await Users.findOneById(inviter._id)) || undefined); - // Not "duplicated": we're moving away from callbacks so this is a patch function. We should migrate the next one to be a patch or use this same patch, instead of calling both - await beforeAddUserToRoomPatch([userToBeAdded.username!], room, inviterUser); - await beforeAddUserToRoom.run({ user: userToBeAdded, inviter: inviterUser }, room); - } catch (error) { - throw new Meteor.Error((error as any)?.message); - } - - // TODO: are we calling this twice? - - await callbacks.run('beforeAddedToRoom', { user: userToBeAdded, inviter }); - - try { - await Apps.self?.triggerEvent(AppEvents.IPreRoomUserJoined, room, userToBeAdded, inviter); - } catch (error: any) { - if (error.name === AppsEngineException.name) { - throw new Meteor.Error('error-app-prevented', error.message); - } - - throw error; - } - - // for federation rooms we stop here since everything else will be handled by the federation invite flow - if (isRoomNativeFederated(room)) { - return; - } - - // TODO: are we calling this twice? - if (room.t === 'c' || room.t === 'p' || room.t === 'l') { - // Add a new event, with an optional inviter - await callbacks.run('beforeAddedToRoom', { user: userToBeAdded, inviter }, room); - - // Keep the current event - await callbacks.run('beforeJoinRoom', userToBeAdded, room); - } - - const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userToBeAdded._id); - if (subscription) { - if (!isBannedSubscription(subscription)) { - return true; - } - - const deletedSub = await Subscriptions.removeByRoomIdAndUserId(rid, userToBeAdded._id); - if (!deletedSub) { - return true; - } - void notifyOnSubscriptionChanged(deletedSub, 'removed'); - - if (!skipSystemMessage && inviter) { - await Message.saveSystemMessage('user-unbanned', rid, userToBeAdded.username!, inviter, { ts: now }); - } - } - - await Room.createUserSubscription({ - room, - ts: now, - inviter, - userToBeAdded, - createAsHidden, - skipAlertSound, - skipSystemMessage, - }); - - if (room.t === 'c' || room.t === 'p') { - process.nextTick(async () => { - // Add a new event, with an optional inviter - await callbacks.run('afterAddedToRoom', { user: userToBeAdded, inviter }, room); - - // Keep the current event - await callbacks.run('afterJoinRoom', userToBeAdded, room); - - void Apps.self?.triggerEvent(AppEvents.IPostRoomUserJoined, room, userToBeAdded, inviter); - }); - } - - if (room.teamMain && room.teamId) { - // if user is joining to main team channel, create a membership - await Team.addMember(inviter || userToBeAdded, userToBeAdded._id, room.teamId); - } - - if (room.encrypted && settings.get('E2E_Enable') && userToBeAdded.e2e?.public_key) { - await Rooms.addUserIdToE2EEQueueByRoomIds([room._id], userToBeAdded._id); - } - - void notifyOnRoomChangedById(rid); - return true; -}; diff --git a/apps/meteor/app/lib/server/functions/archiveRoom.ts b/apps/meteor/app/lib/server/functions/archiveRoom.ts deleted file mode 100644 index 9bc3582d6671d..0000000000000 --- a/apps/meteor/app/lib/server/functions/archiveRoom.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Message } from '@rocket.chat/core-services'; -import type { IMessage } from '@rocket.chat/core-typings'; -import { Rooms, Subscriptions } from '@rocket.chat/models'; - -import { callbacks } from '../../../../server/lib/callbacks'; -import { notifyOnRoomChanged, notifyOnSubscriptionChangedByRoomId } from '../lib/notifyListener'; - -export const archiveRoom = async function (rid: string, user: IMessage['u']): Promise { - await Rooms.archiveById(rid); - - const archiveResponse = await Subscriptions.archiveByRoomId(rid); - if (archiveResponse.modifiedCount) { - void notifyOnSubscriptionChangedByRoomId(rid); - } - - await Message.saveSystemMessage('room-archived', rid, '', user); - - const room = await Rooms.findOneById(rid); - - await callbacks.run('afterRoomArchived', room, user); - - if (room) { - void notifyOnRoomChanged(room); - } -}; diff --git a/apps/meteor/app/lib/server/functions/blockUser.ts b/apps/meteor/app/lib/server/functions/blockUser.ts deleted file mode 100644 index c748ade354a34..0000000000000 --- a/apps/meteor/app/lib/server/functions/blockUser.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Subscriptions, Rooms } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; -import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; -import { notifyOnSubscriptionChangedByRoomIdAndUserIds } from '../lib/notifyListener'; - -export const blockUserMethod = async (userId: string, { rid, blocked }: { rid: string; blocked: string }): Promise => { - const room = await Rooms.findOne({ _id: rid }); - - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' }); - } - - if (!(await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BLOCK, userId))) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' }); - } - - const [blockedUser, blockerUser] = await Promise.all([ - Subscriptions.findOneByRoomIdAndUserId(rid, blocked, { projection: { _id: 1 } }), - Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection: { _id: 1 } }), - ]); - - if (!blockedUser || !blockerUser) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' }); - } - - const [blockedResponse, blockerResponse] = await Subscriptions.setBlockedByRoomId(rid, blocked, userId); - - const listenerUsers = [...(blockedResponse?.modifiedCount ? [blocked] : []), ...(blockerResponse?.modifiedCount ? [userId] : [])]; - - if (listenerUsers.length) { - void notifyOnSubscriptionChangedByRoomIdAndUserIds(rid, listenerUsers); - } -}; diff --git a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts deleted file mode 100644 index 1e3e3faeb15b5..0000000000000 --- a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { api } from '@rocket.chat/core-services'; -import type { IRoom } from '@rocket.chat/core-typings'; -import { Messages, Rooms, Subscriptions, ReadReceipts, ReadReceiptsArchive } from '@rocket.chat/models'; - -import { deleteRoom } from './deleteRoom'; -import { NOTIFICATION_ATTACHMENT_COLOR } from '../../../../lib/constants'; -import { i18n } from '../../../../server/lib/i18n'; -import { FileUpload } from '../../../file-upload/server'; -import { notifyOnRoomChangedById, notifyOnSubscriptionChangedById } from '../lib/notifyListener'; - -const FILE_CLEANUP_BATCH_SIZE = 1000; - -export async function cleanRoomHistory({ - rid = '', - latest = new Date(), - oldest = new Date('0001-01-01T00:00:00Z'), - inclusive = true, - limit = 0, - excludePinned = true, - ignoreDiscussion = true, - filesOnly = false, - fromUsers = [], - ignoreThreads = true, -}: { - rid?: IRoom['_id']; - latest?: Date; - oldest?: Date; - inclusive?: boolean; - limit?: number; - excludePinned?: boolean; - ignoreDiscussion?: boolean; - filesOnly?: boolean; - fromUsers?: string[]; - ignoreThreads?: boolean; -}): Promise { - const gt = inclusive ? '$gte' : '$gt'; - const lt = inclusive ? '$lte' : '$lt'; - - const ts = { [gt]: oldest, [lt]: latest }; - - const text = `_${i18n.t('File_removed_by_prune')}_`; - - let fileCount = 0; - - const cursor = Messages.findFilesByRoomIdPinnedTimestampAndUsers(rid, excludePinned, ignoreDiscussion, ts, fromUsers, ignoreThreads, { - projection: { pinned: 1, files: 1 }, - limit, - }); - - const targetMessageIdsForAttachmentRemoval = new Set(); - // Since we remove every file from the messages, we don't need to specify which fileId has been removed. - const pruneMessageAttachment = { type: 'removed-file', color: NOTIFICATION_ATTACHMENT_COLOR, text }; - - async function performFileAttachmentCleanupBatch() { - if (targetMessageIdsForAttachmentRemoval.size === 0) return; - - const ids = [...targetMessageIdsForAttachmentRemoval]; - await Messages.removeFileAttachmentsByMessageIds(ids, pruneMessageAttachment); - await Messages.clearFilesByMessageIds(ids); - void api.broadcast('notify.deleteMessageBulk', rid, { - rid, - excludePinned, - ignoreDiscussion, - ts, - users: fromUsers, - ids, - filesOnly: true, - replaceFileAttachmentsWith: pruneMessageAttachment, - }); - targetMessageIdsForAttachmentRemoval.clear(); - } - - for await (const document of cursor) { - const uploadsStore = FileUpload.getStore('Uploads'); - - document.files && (await Promise.all(document.files.map((file) => uploadsStore.deleteById(file._id)))); - - fileCount++; - if (filesOnly) { - targetMessageIdsForAttachmentRemoval.add(document._id); - } - - if (targetMessageIdsForAttachmentRemoval.size >= FILE_CLEANUP_BATCH_SIZE) { - await performFileAttachmentCleanupBatch(); - } - } - - if (targetMessageIdsForAttachmentRemoval.size > 0) { - await performFileAttachmentCleanupBatch(); - } - - if (filesOnly) { - return fileCount; - } - - if (!ignoreDiscussion) { - const discussionsCursor = Messages.findDiscussionByRoomIdPinnedTimestampAndUsers(rid, excludePinned, ts, fromUsers, { - projection: { drid: 1 }, - ...(limit && { limit }), - }); - - for await (const { drid } of discussionsCursor) { - if (!drid) { - continue; - } - await deleteRoom(drid); - } - } - - if (!ignoreThreads) { - const threads = new Set(); - - await Messages.findThreadsByRoomIdPinnedTimestampAndUsers( - { rid, pinned: excludePinned, ignoreDiscussion, ts, users: fromUsers }, - { projection: { _id: 1 } }, - ).forEach(({ _id }) => { - threads.add(_id); - }); - - if (threads.size > 0) { - const subscriptionIds: string[] = ( - await Subscriptions.findUnreadThreadsByRoomId(rid, [...threads], { projection: { _id: 1 } }).toArray() - ).map(({ _id }) => _id); - - const { modifiedCount } = await Subscriptions.removeUnreadThreadsByRoomId(rid, [...threads]); - if (modifiedCount) { - subscriptionIds.forEach((id) => notifyOnSubscriptionChangedById(id)); - } - } - } - - const selectedMessageIds = limit - ? await Messages.findByIdPinnedTimestampLimitAndUsers(rid, excludePinned, ignoreDiscussion, ts, limit, fromUsers, ignoreThreads) - : undefined; - const count = await Messages.removeByIdPinnedTimestampLimitAndUsers( - rid, - excludePinned, - ignoreDiscussion, - ts, - limit, - fromUsers, - ignoreThreads, - selectedMessageIds, - ); - - if (limit && selectedMessageIds) { - await ReadReceipts.removeByMessageIds(selectedMessageIds); - await ReadReceiptsArchive.removeByMessageIds(selectedMessageIds); - } - - if (count) { - const lastMessage = await Messages.getLastVisibleUserMessageSentByRoomId(rid); - - await Rooms.resetLastMessageById(rid, lastMessage, -count); - - void notifyOnRoomChangedById(rid); - - void api.broadcast('notify.deleteMessageBulk', rid, { - rid, - excludePinned, - ignoreDiscussion, - ts, - users: fromUsers, - ids: selectedMessageIds, - }); - } - - return count; -} diff --git a/apps/meteor/app/lib/server/functions/deleteUser.ts b/apps/meteor/app/lib/server/functions/deleteUser.ts deleted file mode 100644 index 6a09dbc7b0338..0000000000000 --- a/apps/meteor/app/lib/server/functions/deleteUser.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { Apps, AppEvents } from '@rocket.chat/apps'; -import { api } from '@rocket.chat/core-services'; -import { isUserFederated, type IUser } from '@rocket.chat/core-typings'; -import { - Integrations, - LivechatVisitors, - LivechatDepartmentAgents, - Messages, - Rooms, - Subscriptions, - Users, - ReadReceipts, - ReadReceiptsArchive, - LivechatUnitMonitors, - ModerationReports, -} from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { getSubscribedRoomsForUserWithDetails, shouldRemoveOrChangeOwner } from './getRoomsWithSingleOwner'; -import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; -import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; -import { updateGroupDMsName } from './updateGroupDMsName'; -import { callbacks } from '../../../../server/lib/callbacks'; -import { i18n } from '../../../../server/lib/i18n'; -import { FileUpload } from '../../../file-upload/server'; -import { settings } from '../../../settings/server'; -import { - notifyOnRoomChangedById, - notifyOnIntegrationChangedByUserId, - notifyOnLivechatDepartmentAgentChanged, - notifyOnUserChange, -} from '../lib/notifyListener'; - -export async function deleteUser(userId: string, confirmRelinquish = false, deletedBy?: IUser['_id']): Promise<{ deletedRooms: string[] }> { - if (userId === 'rocket.cat') { - throw new Meteor.Error('error-action-not-allowed', 'Deleting the rocket.cat user is not allowed', { - method: 'deleteUser', - action: 'Delete_user', - }); - } - - const user = await Users.findOneById(userId, { - projection: { username: 1, avatarOrigin: 1, roles: 1, federated: 1 }, - }); - - if (!user) { - return { deletedRooms: [] }; - } - - if (isUserFederated(user)) { - throw new Meteor.Error('error-not-allowed', 'User participated in federation, this user can only be deactivated permanently', { - method: 'deleteUser', - }); - } - - const subscribedRooms = await getSubscribedRoomsForUserWithDetails(userId); - - if (shouldRemoveOrChangeOwner(subscribedRooms) && !confirmRelinquish) { - const rooms = await getUserSingleOwnedRooms(subscribedRooms); - throw new Meteor.Error('user-last-owner', '', rooms); - } - - let deletedRooms: string[] = []; - // Users without username can't do anything, so there is nothing to remove - if (user.username != null) { - let userToReplaceWhenUnlinking: IUser | null = null; - const nameAlias = i18n.t('Removed_User'); - deletedRooms = await relinquishRoomOwnerships(userId, subscribedRooms, true); - - const messageErasureType = settings.get<'Delete' | 'Unlink' | 'Keep'>('Message_ErasureType'); - switch (messageErasureType) { - case 'Delete': - const store = FileUpload.getStore('Uploads'); - const cursor = Messages.findFilesByUserId(userId); - - for await (const { file, files } of cursor) { - const fileIds = files?.map(({ _id }) => _id) || []; - for await (const fileId of fileIds) { - if (fileId) { - await store.deleteById(fileId); - } - } - - if (file?._id && !fileIds.includes(file._id)) { - await store.deleteById(file._id); - } - } - - await Messages.removeByUserId(userId); - await ReadReceipts.removeByUserId(userId); - await ReadReceiptsArchive.removeByUserId(userId); - - await ModerationReports.hideMessageReportsByUserId( - userId, - deletedBy || userId, - deletedBy === userId ? 'user deleted own account' : 'user account deleted', - 'DELETE_USER', - ); - - break; - case 'Unlink': - userToReplaceWhenUnlinking = await Users.findOneById('rocket.cat'); - if (!userToReplaceWhenUnlinking?._id || !userToReplaceWhenUnlinking?.username) { - break; - } - await Messages.unlinkUserId(userId, userToReplaceWhenUnlinking?._id, userToReplaceWhenUnlinking?.username, nameAlias); - break; - } - - await Rooms.updateGroupDMsRemovingUsernamesByUsername(user.username, userId); // Remove direct rooms with the user - await Rooms.removeDirectRoomContainingUsername(user.username); // Remove direct rooms with the user - - const rids = subscribedRooms.map((room) => room.rid); - void notifyOnRoomChangedById(rids); - - await Subscriptions.removeByUserId(userId); - - // Remove user as livechat agent - if (user.roles.includes('livechat-agent')) { - const departmentAgents = await LivechatDepartmentAgents.findByAgentId(userId).toArray(); - - const { deletedCount } = await LivechatDepartmentAgents.removeByAgentId(userId); - - if (deletedCount > 0) { - departmentAgents.forEach((depAgent) => { - void notifyOnLivechatDepartmentAgentChanged( - { - _id: depAgent._id, - agentId: userId, - departmentId: depAgent.departmentId, - }, - 'removed', - ); - }); - } - } - - if (user.roles.includes('livechat-monitor')) { - // Remove user as Unit Monitor - await LivechatUnitMonitors.removeByMonitorId(userId); - } - - // This is for compatibility. Since we allowed any user to be contact manager b4, we need to have the same logic - // for deletion. - await LivechatVisitors.removeContactManagerByUsername(user.username); - - // removes user's avatar - if (user.avatarOrigin === 'upload' || user.avatarOrigin === 'url' || user.avatarOrigin === 'rest') { - await FileUpload.getStore('Avatars').deleteByName(user.username); - } - - // Disables all the integrations which rely on the user being deleted. - await Integrations.disableByUserId(userId); - void notifyOnIntegrationChangedByUserId(userId); - - // Don't broadcast user.deleted for Erasure Type of 'Keep' so that messages don't disappear from logged in sessions - if (messageErasureType === 'Delete') { - void api.broadcast('user.deleted', user, { - messageErasureType, - }); - } - if (messageErasureType === 'Unlink' && userToReplaceWhenUnlinking) { - void api.broadcast('user.deleted', user, { - messageErasureType, - replaceByUser: { _id: userToReplaceWhenUnlinking._id, username: userToReplaceWhenUnlinking?.username, alias: nameAlias }, - }); - } - } - - // Remove user from users database - await Users.removeById(userId); - - // App IPostUserDeleted event hook - if (deletedBy) { - await Apps.self?.triggerEvent(AppEvents.IPostUserDeleted, { user, performedBy: await Users.findOneById(deletedBy) }); - } - - // update name and fname of group direct messages - await updateGroupDMsName(user); - - void notifyOnUserChange({ clientAction: 'removed', id: user._id }); - - await callbacks.run('afterDeleteUser', user); - - return { deletedRooms }; -} diff --git a/apps/meteor/app/lib/server/functions/getUsernameSuggestion.ts b/apps/meteor/app/lib/server/functions/getUsernameSuggestion.ts deleted file mode 100644 index f91e39e8ba088..0000000000000 --- a/apps/meteor/app/lib/server/functions/getUsernameSuggestion.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; -import limax from 'limax'; - -import { settings } from '../../../settings/server'; - -function slug(text: string): string { - return limax(text, { replacement: '.' }).replace(/[^0-9a-z-_.]/g, ''); -} - -async function usernameIsAvailable(username: string): Promise { - if (username.length === 0) { - return false; - } - - if (username === 'all') { - return false; - } - - return !(await Users.findOneByUsernameIgnoringCase(username, {})); -} - -const name = (username: string): string => (settings.get('UTF8_Names_Slugify') ? slug(username) : username); - -export async function generateUsernameSuggestion(user: Pick): Promise { - let usernames = []; - - if (user.name) { - usernames.push(name(user.name)); - - const nameParts = user.name.split(' '); - - if (nameParts.length > 1) { - const [first] = nameParts; - const last = nameParts[nameParts.length - 1]; - usernames.push(name(first[0] + last)); - usernames.push(name(first + last[0])); - } - } - - if (user?.name) { - usernames.push(name(user.name)); - } - - if (Array.isArray(user.services)) { - const services = [ - ...new Set(user.services.flatMap(({ name, username, firstName, lastName }) => [name, username, firstName, lastName])), - ]; - usernames.push(...services.map(name)); - } - - if (user.emails && user.emails.length > 0) { - for (const email of user.emails) { - if (email.address && email.verified === true) { - usernames.push(slug(email.address.replace(/@.+$/, ''))); - usernames.push(slug(email.address.replace(/(.+)@(\w+).+/, '$1.$2'))); - } - } - } - - usernames = usernames.filter((e) => e); - - for (const item of usernames) { - if (await usernameIsAvailable(item)) { - return item; - } - } - - usernames.push(settings.get('Accounts_DefaultUsernamePrefixSuggestion')); - - let index = await Users.countDocuments({ username: new RegExp(`^${usernames[0]}-[0-9]+`) }); - const username = ''; - while (!username) { - if (await usernameIsAvailable(`${usernames[0]}-${index}`)) { - return `${usernames[0]}-${index}`; - } - index++; - } -} diff --git a/apps/meteor/app/lib/server/functions/isTheLastMessage.ts b/apps/meteor/app/lib/server/functions/isTheLastMessage.ts deleted file mode 100644 index f1f1fb4c1497d..0000000000000 --- a/apps/meteor/app/lib/server/functions/isTheLastMessage.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { IMessage, IRoom, AtLeast } from '@rocket.chat/core-typings'; - -import { settings } from '../../../settings/server'; - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export const isTheLastMessage = (room: AtLeast, message: Pick) => - settings.get('Store_Last_Message') && (!room.lastMessage || room.lastMessage._id === message._id); diff --git a/apps/meteor/app/lib/server/functions/notifications/index.ts b/apps/meteor/app/lib/server/functions/notifications/index.ts deleted file mode 100644 index 47a38ac0f068c..0000000000000 --- a/apps/meteor/app/lib/server/functions/notifications/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { IMessage, IUser } from '@rocket.chat/core-typings'; -import { isFileAttachment, isFileImageAttachment } from '@rocket.chat/core-typings'; -import { escapeRegExp } from '@rocket.chat/string-helpers'; - -import { callbacks } from '../../../../../server/lib/callbacks'; -import { i18n } from '../../../../../server/lib/i18n'; -import { settings } from '../../../../settings/server'; - -/** - * This function returns a string ready to be shown in the notification - * - * @param {object} message the message to be parsed - */ -export async function parseMessageTextPerUser( - messageText: string, - message: Pick, - receiver: Pick, -): Promise { - const lng = receiver.language || settings.get('Language') || 'en'; - - const firstAttachment = message.attachments?.[0]; - if (!message.msg && firstAttachment && isFileAttachment(firstAttachment) && isFileImageAttachment(firstAttachment)) { - return firstAttachment.image_type ? i18n.t('User_uploaded_image', { lng }) : i18n.t('User_uploaded_file', { lng }); - } - - if (message.msg && message.t === 'e2e') { - return i18n.t('Encrypted_message', { lng }); - } - - // perform processing required before sending message as notification such as markdown filtering - return callbacks.run('renderNotification', messageText); -} - -/** - * Replaces @username with full name - * - * @param {string} message The message to replace - * @param {object[]} mentions Array of mentions used to make replacements - * - * @returns {string} - */ -export function replaceMentionedUsernamesWithFullNames(message: string, mentions: NonNullable): string { - mentions.forEach((mention) => { - if (mention.name) { - message = message.replace(new RegExp(escapeRegExp(`@${mention.username}`), 'g'), mention.name); - } - }); - return message; -} diff --git a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts deleted file mode 100644 index b58710aaeb5e1..0000000000000 --- a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Apps, AppEvents } from '@rocket.chat/apps'; -import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; -import { Message, Team, Room } from '@rocket.chat/core-services'; -import type { IRoom, IUser, MessageTypesValues } from '@rocket.chat/core-typings'; -import { Subscriptions, Rooms } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { afterLeaveRoomCallback } from '../../../../server/lib/callbacks/afterLeaveRoomCallback'; -import { beforeLeaveRoomCallback } from '../../../../server/lib/callbacks/beforeLeaveRoomCallback'; -import { settings } from '../../../settings/server'; -import { notifyOnRoomChangedById, notifyOnSubscriptionChanged } from '../lib/notifyListener'; - -/** - * Removes a user from a room when triggered by federation or other external events. - * Executes only the necessary database operations, with no callbacks, to prevent - * propagation loops during external event processing. - */ -export const performUserRemoval = async function ( - room: IRoom, - user: IUser, - options?: { byUser?: IUser; skipAppPreEvents?: boolean; customSystemMessage?: MessageTypesValues }, -): Promise { - const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { - projection: { _id: 1, status: 1 }, - }); - if (!subscription) { - return; - } - - // make TS happy, this should never happen - if (!user.username) { - throw new Error('User must have a username to be removed from the room'); - } - - // TODO: move before callbacks to service - await beforeLeaveRoomCallback.run(user, room); - - if (subscription) { - if (options?.customSystemMessage) { - await Message.saveSystemMessage(options?.customSystemMessage, room._id, user.username || '', user); - } else if (options?.byUser) { - const extraData = { - u: options.byUser, - }; - - if (room.teamMain) { - await Message.saveSystemMessage('removed-user-from-team', room._id, user.username, user, extraData); - } else { - await Message.saveSystemMessage('ru', room._id, user.username, user, extraData); - } - } else if (subscription.status === 'INVITED') { - await Message.saveSystemMessage('uir', room._id, user.username, user); - } else if (room.teamMain) { - await Message.saveSystemMessage('ult', room._id, user.username, user); - } else { - await Message.saveSystemMessage('ul', room._id, user.username, user); - } - } - - if (room.t === 'l') { - await Message.saveSystemMessage('command', room._id, 'survey', user); - } - - const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(room._id, user._id); - if (deletedSubscription) { - void notifyOnSubscriptionChanged(deletedSubscription, 'removed'); - } - - if (room.teamId && room.teamMain) { - await Team.removeMember(room.teamId, user._id); - } - - if (room.encrypted && settings.get('E2E_Enable')) { - await Rooms.removeUsersFromE2EEQueueByRoomId(room._id, [user._id]); - } - - // remove references to the user in direct message rooms - if (room.t === 'd') { - await Rooms.removeUserReferenceFromDMsById(room._id, user.username, user._id); - } - - void notifyOnRoomChangedById(room._id); -}; - -/** - * Removes a user from the given room by performing the required database updates - * and triggering all standard callbacks. Used for local actions (UI or API) - * that should propagate normally to federation and other subscribers. - */ -export const removeUserFromRoom = async function ( - rid: string, - user: IUser, - options?: { byUser?: IUser; skipAppPreEvents?: boolean; customSystemMessage?: MessageTypesValues }, -): Promise { - const room = await Rooms.findOneById(rid); - if (!room) { - return; - } - - // Rationale: for an abac room, we don't want apps to be able to prevent a user from leaving - if (!options?.skipAppPreEvents) { - try { - await Apps.self?.triggerEvent(AppEvents.IPreRoomUserLeave, room, user, options?.byUser); - } catch (error: any) { - if (error.name === AppsEngineException.name) { - throw new Meteor.Error('error-app-prevented', error.message); - } - - throw error; - } - } - - await Room.beforeLeave(room); - - await performUserRemoval(room, user, options); - - await afterLeaveRoomCallback.run({ user, kicker: options?.byUser }, room); - - await Apps.self?.triggerEvent(AppEvents.IPostRoomUserLeave, room, user, options?.byUser); -}; diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts deleted file mode 100644 index 73fa409999182..0000000000000 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { AppEvents, Apps } from '@rocket.chat/apps'; -import { Message } from '@rocket.chat/core-services'; -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; -import { Messages } from '@rocket.chat/models'; -import { isAbsoluteURL } from '@rocket.chat/tools'; -import { Match, check } from 'meteor/check'; - -import { isRelativeURL } from '../../../../lib/utils/isRelativeURL'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { FileUpload } from '../../../file-upload/server'; -import { settings } from '../../../settings/server'; -import { afterSaveMessage } from '../lib/afterSaveMessage'; -import { notifyOnRoomChangedById } from '../lib/notifyListener'; -import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; - -export type SendMessageOptions = { - upsert?: boolean; - previewUrls?: string[]; - skipNotifications?: boolean; -}; - -// TODO: most of the types here are wrong, but I don't want to change them now - -/** - * IMPORTANT - * - * This validator prevents malicious href values - * intending to run arbitrary js code in anchor tags. - * You should use it whenever the value you're checking - * is going to be rendered in the href attribute of a - * link. - */ -const validFullURLParam = Match.Where((value) => { - check(value, String); - - if (!isAbsoluteURL(value) && !value.startsWith(FileUpload.getPath())) { - throw new Error('Invalid href value provided'); - } - - if (/^javascript:/i.test(value)) { - throw new Error('Invalid href value provided'); - } - - return true; -}); - -const validPartialURLParam = Match.Where((value) => { - check(value, String); - - if (!isRelativeURL(value) && !isAbsoluteURL(value) && !value.startsWith(FileUpload.getPath())) { - throw new Error('Invalid href value provided'); - } - - if (/^javascript:/i.test(value)) { - throw new Error('Invalid href value provided'); - } - - return true; -}); - -const objectMaybeIncluding = (types: any) => - Match.Where((value: any) => { - Object.keys(types).forEach((field) => { - if (value[field] != null) { - try { - check(value[field], types[field]); - } catch (error: any) { - error.path = field; - throw error; - } - } - }); - - return true; - }); - -const validateAttachmentsFields = (attachmentField: any) => { - check( - attachmentField, - objectMaybeIncluding({ - short: Boolean, - title: String, - value: Match.OneOf(String, Number, Boolean), - }), - ); - - if (!attachmentField.value || !attachmentField.title) { - throw new Error('Invalid attachment field, title and value is required'); - } -}; - -const validateAttachmentsActions = (attachmentActions: any) => { - check( - attachmentActions, - objectMaybeIncluding({ - type: String, - text: String, - url: validFullURLParam, - image_url: validFullURLParam, - is_webview: Boolean, - webview_height_ratio: String, - msg: String, - msg_in_chat_window: Boolean, - }), - ); -}; - -const validateAttachment = (attachment: any) => { - check( - attachment, - objectMaybeIncluding({ - color: String, - text: String, - ts: Match.OneOf(String, Number), - thumb_url: validFullURLParam, - button_alignment: String, - actions: [Match.Any], - message_link: validFullURLParam, - collapsed: Boolean, - author_name: String, - author_link: validFullURLParam, - author_icon: validFullURLParam, - title: String, - title_link: validFullURLParam, - title_link_download: Boolean, - image_dimensions: Object, - image_url: validFullURLParam, - image_preview: String, - image_type: String, - image_size: Number, - audio_url: validFullURLParam, - audio_type: String, - audio_size: Number, - video_url: validFullURLParam, - video_type: String, - video_size: Number, - fields: [Match.Any], - }), - ); - - if (attachment.fields?.length) { - attachment.fields.map(validateAttachmentsFields); - } - - if (attachment.actions?.length) { - attachment.actions.map(validateAttachmentsActions); - } -}; - -const validateBodyAttachments = (attachments: any[]) => attachments.map(validateAttachment); - -export const validateMessage = async (message: any, room: any, user: any) => { - check( - message, - objectMaybeIncluding({ - _id: String, - msg: String, - text: String, - alias: String, - emoji: String, - tmid: String, - tshow: Boolean, - avatar: validPartialURLParam, - attachments: [Match.Any], - blocks: [Match.Any], - }), - ); - - if (message.alias || message.avatar) { - const isLiveChatGuest = !message.avatar && user.token && user.token === room.v?.token; - - if (!isLiveChatGuest && !(await hasPermissionAsync(user._id, 'message-impersonate', room._id))) { - throw new Error('Not enough permission'); - } - } - - if (Array.isArray(message.attachments) && message.attachments.length) { - validateBodyAttachments(message.attachments); - } - - if (message.customFields) { - validateCustomMessageFields({ - customFields: message.customFields, - messageCustomFieldsEnabled: settings.get('Message_CustomFields_Enabled'), - messageCustomFields: settings.get('Message_CustomFields'), - }); - } -}; - -export function prepareMessageObject( - message: Partial, - rid: IRoom['_id'], - user: { _id: string; username?: string; name?: string }, -): asserts message is IMessage { - if (!message.ts) { - message.ts = new Date(); - } - - if (message.tshow !== true) { - delete message.tshow; - } - - const { _id, username, name } = user; - message.u = { - _id, - username: username as string, // FIXME: this is wrong but I don't want to change it now - name, - }; - message.rid = rid; - - if (!Match.test(message.msg, String)) { - message.msg = ''; - } - - if (message.ts == null) { - message.ts = new Date(); - } -} - -/** - * Validates and sends the message object. This function does not verify the Message_MaxAllowedSize settings. - * Caller of the function should verify the Message_MaxAllowedSize if needed. - * There might be same use cases which needs to override this setting. Example - sending error logs. - */ -export const sendMessage = async function (user: any, message: any, room: any, options: SendMessageOptions = {}) { - const { upsert = false, previewUrls } = options; - - if (!user || !message || !room._id) { - return false; - } - - await validateMessage(message, room, user); - prepareMessageObject(message, room._id, user); - - if (settings.get('Message_Read_Receipt_Enabled')) { - message.unread = true; - } - - // For the Rocket.Chat Apps :) - if (Apps.self?.isLoaded()) { - const prevent = await Apps.self?.triggerEvent(AppEvents.IPreMessageSentPrevent, message); - - if (prevent) { - return; - } - - const result = await Apps.self?.triggerEvent( - AppEvents.IPreMessageSentModify, - await Apps.self?.triggerEvent(AppEvents.IPreMessageSentExtend, message), - ); - - if (typeof result === 'object') { - message = Object.assign(message, result); - - // Some app may have inserted malicious/invalid values in the message, let's check it again - await validateMessage(message, room, user); - } - } - - message = await Message.beforeSave({ message, room, user, previewUrls, parseUrls: message.parseUrls }); - - if (!message) { - return; - } - - if (message._id && upsert) { - const { _id } = message; - delete message._id; - await Messages.updateOne( - { - _id, - 'u._id': message.u._id, - }, - { $set: message }, - { upsert: true }, - ); - message._id = _id; - } else { - const messageAlreadyExists = message._id && (await Messages.findOneById(message._id, { projection: { _id: 1 } })); - if (messageAlreadyExists) { - return; - } - const { insertedId } = await Messages.insertOne(message); - message._id = insertedId; - } - - if (Apps.self?.isLoaded()) { - // If the message has a type (system message), we should notify the listener about it - const messageEvent = message.t ? AppEvents.IPostSystemMessageSent : AppEvents.IPostMessageSent; - void Apps.self?.triggerEvent(messageEvent, message); - } - - await afterSaveMessage(message, room, user, { options }); - - void notifyOnRoomChangedById(message.rid); - - return message; -}; diff --git a/apps/meteor/app/lib/server/functions/setEmail.ts b/apps/meteor/app/lib/server/functions/setEmail.ts deleted file mode 100644 index f9a911e85b3c3..0000000000000 --- a/apps/meteor/app/lib/server/functions/setEmail.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import type { Updater } from '@rocket.chat/models'; -import { Users } from '@rocket.chat/models'; -import { escapeHTML } from '@rocket.chat/string-helpers'; -import { Meteor } from 'meteor/meteor'; -import type { ClientSession } from 'mongodb'; - -import { onceTransactionCommitedSuccessfully } from '../../../../server/database/utils'; -import * as Mailer from '../../../mailer/server/api'; -import { settings } from '../../../settings/server'; -import { validateEmailDomain } from '../lib'; -import { checkEmailAvailability } from './checkEmailAvailability'; -import { sendConfirmationEmail } from '../../../../server/methods/sendConfirmationEmail'; - -let html = ''; -Meteor.startup(() => { - Mailer.getTemplate('Email_Changed_Email', (template) => { - html = template; - }); -}); - -const _sendEmailChangeNotification = async function (to: string, newEmail: string) { - const subject = String(settings.get('Email_Changed_Email_Subject')); - const email = { - to, - from: String(settings.get('From_Email')), - subject, - html, - data: { - email: escapeHTML(newEmail), - }, - }; - - try { - await Mailer.send(email); - } catch (error: any) { - throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${error.message}`, { - function: 'setEmail', - message: error.message, - }); - } -}; - -export const setEmail = async function ( - userId: string, - email: string, - shouldSendVerificationEmail = true, - verified = false, - updater?: Updater, - session?: ClientSession, -) { - email = email.trim(); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { function: '_setEmail' }); - } - - if (!email) { - throw new Meteor.Error('error-invalid-email', 'Invalid email', { function: '_setEmail' }); - } - - await validateEmailDomain(email); - - const user = await Users.findOneById(userId, { session }); - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { function: '_setEmail' }); - } - - // User already has desired username, return - if (user?.emails?.[0] && user.emails[0].address === email) { - return user; - } - - // Check email availability - if (!(await checkEmailAvailability(email))) { - throw new Meteor.Error('error-field-unavailable', `${email} is already in use :(`, { - function: '_setEmail', - field: email, - }); - } - - const oldEmail = user?.emails?.[0]; - - if (oldEmail) { - await onceTransactionCommitedSuccessfully(async () => { - await _sendEmailChangeNotification(oldEmail.address, email); - }, session); - } - - // Set new email - if (updater) { - updater.set('emails', [{ address: email, verified }]); - } else { - await Users.setEmail(user?._id, email, verified, { session }); - } - - const result = { - ...user, - email, - }; - if (shouldSendVerificationEmail === true) { - await onceTransactionCommitedSuccessfully(async () => { - await sendConfirmationEmail(result.email); - }, session); - } - return result; -}; diff --git a/apps/meteor/app/lib/server/functions/setRealName.ts b/apps/meteor/app/lib/server/functions/setRealName.ts deleted file mode 100644 index d33aa01406230..0000000000000 --- a/apps/meteor/app/lib/server/functions/setRealName.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { api } from '@rocket.chat/core-services'; -import type { IUser } from '@rocket.chat/core-typings'; -import type { Updater } from '@rocket.chat/models'; -import { Users } from '@rocket.chat/models'; -import type { ClientSession } from 'mongodb'; - -import { onceTransactionCommitedSuccessfully } from '../../../../server/database/utils'; -import { settings } from '../../../settings/server'; - -export const setRealName = async function ( - userId: string, - name: string, - fullUser?: IUser, - updater?: Updater, - session?: ClientSession, -): Promise { - name = name.trim(); - - if (!userId || (settings.get('Accounts_RequireNameForSignUp') && !name)) { - return; - } - - const user = fullUser || (await Users.findOneById(userId, { session })); - - if (!user) { - return; - } - - // User already has desired name, return - if (user.name && user.name.trim() === name) { - return user; - } - - // Set new name - if (name) { - if (updater) { - updater.set('name', name); - } else { - await Users.setName(user._id, name, { session }); - } - } else if (updater) { - updater.unset('name'); - } else { - await Users.unsetName(user._id, { session }); - } - user.name = name; - - await onceTransactionCommitedSuccessfully(() => { - if (settings.get('UI_Use_Real_Name') === true) { - void api.broadcast('user.nameChanged', { - _id: user._id, - name: user.name, - username: user.username, - }); - } - void api.broadcast('user.realNameChanged', { - _id: user._id, - name, - username: user.username, - }); - }, session); - - return user; -}; diff --git a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts deleted file mode 100644 index 6734c9bbb781f..0000000000000 --- a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts +++ /dev/null @@ -1,169 +0,0 @@ -import type { IUser, IUserEmail } from '@rocket.chat/core-typings'; -import { isUserFederated, isDirectMessageRoom } from '@rocket.chat/core-typings'; -import { Rooms, Users, Subscriptions, OAuthAccessTokens, OAuthRefreshTokens, OAuthAuthCodes } from '@rocket.chat/models'; -import { Accounts } from 'meteor/accounts-base'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { closeOmnichannelConversations } from './closeOmnichannelConversations'; -import { shouldRemoveOrChangeOwner, getSubscribedRoomsForUserWithDetails } from './getRoomsWithSingleOwner'; -import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; -import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; -import { callbacks } from '../../../../server/lib/callbacks'; -import * as Mailer from '../../../mailer/server/api'; -import { settings } from '../../../settings/server'; -import { - notifyOnRoomChangedById, - notifyOnRoomChangedByUserDM, - notifyOnSubscriptionChangedByNameAndRoomType, - notifyOnUserChange, -} from '../lib/notifyListener'; - -async function reactivateDirectConversations(userId: string) { - // since both users can be deactivated at the same time, we should just reactivate rooms if both users are active - // for that, we need to fetch the direct messages, fetch the users involved and then the ids of rooms we can reactivate - const directConversations = await Rooms.getDirectConversationsByUserId(userId, { - projection: { _id: 1, uids: 1, t: 1 }, - }).toArray(); - - const userIds = directConversations.reduce((acc: string[], r) => { - if (isDirectMessageRoom(r)) { - acc.push(...r.uids); - } - return acc; - }, []); - const uniqueUserIds = [...new Set(userIds)]; - const activeUsers = await Users.findActiveByUserIds(uniqueUserIds, { projection: { _id: 1 } }).toArray(); - const activeUserIds = activeUsers.map((u: IUser) => u._id); - const roomsToReactivate = directConversations.reduce((acc: string[], room) => { - const otherUserId = isDirectMessageRoom(room) ? room.uids.find((u: string) => u !== userId) : undefined; - if (otherUserId && activeUserIds.includes(otherUserId)) { - acc.push(room._id); - } - return acc; - }, []); - - const setDmReadOnlyResponse = await Rooms.setDmReadOnlyByUserId(userId, roomsToReactivate, false, false); - if (setDmReadOnlyResponse.modifiedCount) { - void notifyOnRoomChangedById(roomsToReactivate); - } -} - -export async function setUserActiveStatus( - userId: string, - active: boolean, - confirmRelinquish = false, - executedBy?: string, -): Promise { - check(userId, String); - check(active, Boolean); - - const user = await Users.findOneById(userId); - - if (!user) { - return false; - } - - if (isUserFederated(user)) { - throw new Meteor.Error('error-user-is-federated', 'Cannot change federated users status', { - method: 'setUserActiveStatus', - }); - } - - // Users without username can't do anything, so there is no need to check for owned rooms - if (user.username != null && !active) { - const userAdmin = await Users.findOneAdmin(userId || ''); - const adminsCount = await Users.countActiveUsersInRoles(['admin']); - if (userAdmin && adminsCount === 1) { - throw new Meteor.Error('error-action-not-allowed', 'Leaving the app without an active admin is not allowed', { - method: 'removeUserFromRole', - action: 'Remove_last_admin', - }); - } - - const subscribedRooms = await getSubscribedRoomsForUserWithDetails(userId); - // give omnichannel rooms a special treatment :) - const chatSubscribedRooms = subscribedRooms.filter(({ t }) => t !== 'l'); - const livechatSubscribedRooms = subscribedRooms.filter(({ t }) => t === 'l'); - - if (shouldRemoveOrChangeOwner(chatSubscribedRooms) && !confirmRelinquish) { - const rooms = await getUserSingleOwnedRooms(chatSubscribedRooms as []); - throw new Meteor.Error('user-last-owner', '', rooms); - } - - // We don't want one killing the other :) - await Promise.allSettled([ - closeOmnichannelConversations(user, livechatSubscribedRooms, executedBy), - relinquishRoomOwnerships(user._id, chatSubscribedRooms, false), - ]); - } - - if (active && !user.active) { - await callbacks.run('beforeActivateUser', user); - } - - await Users.setUserActive(userId, active); - - if (active && !user.active) { - await callbacks.run('afterActivateUser', user); - } - - if (!active && user.active) { - await callbacks.run('afterDeactivateUser', user); - } - - if (user.username) { - const { modifiedCount } = await Subscriptions.setArchivedForDMsWithUsername(user.username, !active); - if (modifiedCount) { - void notifyOnSubscriptionChangedByNameAndRoomType({ t: 'd', name: user.username }); - } - } - - if (active === false) { - await Users.unsetLoginTokens(userId); - await Promise.all([ - OAuthAccessTokens.deleteByUserId(userId), - OAuthRefreshTokens.deleteByUserId(userId), - OAuthAuthCodes.deleteByUserId(userId), - ]); - await Rooms.setDmReadOnlyByUserId(userId, undefined, true, false); - - void notifyOnUserChange({ clientAction: 'updated', id: userId, diff: { 'services.resume.loginTokens': [], active } }); - void notifyOnRoomChangedByUserDM(userId); - } else { - await Users.unsetReason(userId); - - void notifyOnUserChange({ clientAction: 'updated', id: userId, diff: { active } }); - await reactivateDirectConversations(userId); - } - - if (active && !settings.get('Accounts_Send_Email_When_Activating')) { - return true; - } - if (!active && !settings.get('Accounts_Send_Email_When_Deactivating')) { - return true; - } - if (!user.emails || !Array.isArray(user.emails) || user.emails.length === 0) { - return true; - } - - const destinations = user.emails.map((email: IUserEmail) => `${user.name || user.username}<${email.address}>`); - - type UserActivated = { - subject: (params: { active: boolean }) => string; - html: (params: { active: boolean; name: string; username: string }) => string; - }; - const { subject, html } = (Accounts.emailTemplates as unknown as { userActivated: UserActivated }).userActivated; - const email = { - to: String(destinations), - from: String(settings.get('From_Email')), - subject: subject({ active } as any), - html: html({ - active, - name: user.name, - username: user.username, - } as any), - }; - - void Mailer.sendNoWrap(email); -} diff --git a/apps/meteor/app/lib/server/functions/unarchiveRoom.ts b/apps/meteor/app/lib/server/functions/unarchiveRoom.ts deleted file mode 100644 index 353124a02037c..0000000000000 --- a/apps/meteor/app/lib/server/functions/unarchiveRoom.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Message } from '@rocket.chat/core-services'; -import type { IMessage } from '@rocket.chat/core-typings'; -import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; - -import { notifyOnRoomChangedById, notifyOnSubscriptionChangedByRoomId } from '../lib/notifyListener'; - -const BATCH_SIZE = 100_000; - -async function getActiveUserIds(userIds: string[]): Promise> { - const activeUserIds = new Set(); - - for (let i = 0; i < userIds.length; i += BATCH_SIZE) { - const batch = await Users.findActiveByIds(userIds.slice(i, i + BATCH_SIZE), { projection: { _id: 1 } }).toArray(); - for (const u of batch) { - activeUserIds.add(u._id); - } - } - - return activeUserIds; -} - -async function unarchiveSubscriptionsByIds(ids: string[]): Promise { - for (let i = 0; i < ids.length; i += BATCH_SIZE) { - await Subscriptions.unarchiveByIds(ids.slice(i, i + BATCH_SIZE)); - } -} - -export const unarchiveRoom = async function (rid: string, user: IMessage['u']): Promise { - await Rooms.unarchiveById(rid); - - const archivedSubs = await Subscriptions.findArchivedByRoomId(rid, { projection: { 'u._id': 1 } }).toArray(); - - if (archivedSubs.length > 0) { - const activeUserIds = await getActiveUserIds(archivedSubs.map((s) => s.u._id)); - const idsToUnarchive = archivedSubs.filter((s) => activeUserIds.has(s.u._id)).map((s) => s._id); - - await unarchiveSubscriptionsByIds(idsToUnarchive); - void notifyOnSubscriptionChangedByRoomId(rid); - } - - await Message.saveSystemMessage('room-unarchived', rid, '', user); - - void notifyOnRoomChangedById(rid); -}; diff --git a/apps/meteor/app/lib/server/functions/unblockUser.ts b/apps/meteor/app/lib/server/functions/unblockUser.ts deleted file mode 100644 index 03ee770f69d6b..0000000000000 --- a/apps/meteor/app/lib/server/functions/unblockUser.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Subscriptions } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { notifyOnSubscriptionChangedByRoomIdAndUserIds } from '../lib/notifyListener'; - -export const unblockUserMethod = async (userId: string, { rid, blocked }: { rid: string; blocked: string }): Promise => { - const [blockedUser, blockerUser] = await Promise.all([ - Subscriptions.findOneByRoomIdAndUserId(rid, blocked, { projection: { _id: 1 } }), - Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection: { _id: 1 } }), - ]); - - if (!blockedUser || !blockerUser) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'unblockUser' }); - } - - const [blockedResponse, blockerResponse] = await Subscriptions.unsetBlockedByRoomId(rid, blocked, userId); - - const listenerUsers = [...(blockedResponse?.modifiedCount ? [blocked] : []), ...(blockerResponse?.modifiedCount ? [userId] : [])]; - - if (listenerUsers.length) { - void notifyOnSubscriptionChangedByRoomIdAndUserIds(rid, listenerUsers); - } -}; diff --git a/apps/meteor/app/lib/server/functions/updateMessage.ts b/apps/meteor/app/lib/server/functions/updateMessage.ts deleted file mode 100644 index 7f089f6872975..0000000000000 --- a/apps/meteor/app/lib/server/functions/updateMessage.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { AppEvents, Apps } from '@rocket.chat/apps'; -import { Message } from '@rocket.chat/core-services'; -import type { IMessage, IUser, AtLeast } from '@rocket.chat/core-typings'; -import { Messages, Rooms } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../../settings/server'; -import { afterSaveMessage } from '../lib/afterSaveMessage'; -import { notifyOnRoomChangedById } from '../lib/notifyListener'; -import { validateCustomMessageFields } from '../lib/validateCustomMessageFields'; - -export const updateMessage = async function ( - { - parseUrls, - ...message - }: (AtLeast | AtLeast) & { - parseUrls?: boolean; - }, - user: IUser, - originalMsg?: IMessage, - previewUrls?: string[], -): Promise { - const originalMessage = originalMsg || (await Messages.findOneById(message._id)); - if (!originalMessage) { - throw new Error('Invalid message ID.'); - } - - let messageData: IMessage = Object.assign({}, originalMessage, message); - - // For the Rocket.Chat Apps :) - if (message && Apps.self && Apps.isLoaded()) { - const prevent = await Apps.self?.triggerEvent(AppEvents.IPreMessageUpdatedPrevent, messageData); - if (prevent) { - throw new Meteor.Error('error-app-prevented-updating', 'A Rocket.Chat App prevented the message updating.'); - } - - let result = await Apps.self?.triggerEvent(AppEvents.IPreMessageUpdatedExtend, messageData); - result = await Apps.self?.triggerEvent(AppEvents.IPreMessageUpdatedModify, result); - - if (typeof result === 'object') { - Object.assign(messageData, result); - } - } - - // If we keep history of edits, insert a new message to store history information - if (settings.get('Message_KeepHistory')) { - await Messages.cloneAndSaveAsHistoryById(messageData._id, user as Required>); - } - - Object.assign(messageData, { - editedAt: new Date(), - editedBy: { - _id: user._id, - username: user.username, - }, - }); - - const room = await Rooms.findOneById(messageData.rid); - if (!room) { - return; - } - - messageData = await Message.beforeSave({ message: messageData, room, user, previewUrls, parseUrls }); - - if (messageData.customFields) { - validateCustomMessageFields({ - customFields: messageData.customFields, - messageCustomFieldsEnabled: settings.get('Message_CustomFields_Enabled'), - messageCustomFields: settings.get('Message_CustomFields'), - }); - } - - const { _id, ...editedMessage } = messageData; - - if (!editedMessage.msg) { - delete editedMessage.md; - } - - // do not send $unset if not defined. Can cause exceptions in certain mongo versions. - await Messages.updateOne( - { _id }, - { - $set: { - ...editedMessage, - }, - ...(!editedMessage.md && { $unset: { md: 1 } }), - }, - ); - - if (Apps.self?.isLoaded()) { - // This returns a promise, but it won't mutate anything about the message - // so, we don't really care if it is successful or fails - void Apps.self?.triggerEvent(AppEvents.IPostMessageUpdated, messageData); - } - - setImmediate(async () => { - const msg = await Messages.findOneById(_id); - if (!msg) { - return; - } - - await afterSaveMessage(msg, room, user); - - if (room?.lastMessage?._id === msg._id) { - void notifyOnRoomChangedById(message.rid); - } - }); -}; diff --git a/apps/meteor/app/lib/server/index.ts b/apps/meteor/app/lib/server/index.ts deleted file mode 100644 index 9fb7c341b83e4..0000000000000 --- a/apps/meteor/app/lib/server/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -import '../lib/MessageTypes'; -import './lib/bugsnag'; -import './lib/debug'; -import './lib/loginErrorMessageOverride'; -import './oauth/oauth'; -import './oauth/facebook'; -import './oauth/google'; -import './oauth/proxy'; -import './oauth/twitter'; -import './methods/addOAuthService'; -import './methods/addUsersToRoom'; -import './methods/addUserToRoom'; -import './methods/archiveRoom'; -import './methods/blockUser'; -import './methods/checkRegistrationSecretURL'; -import './methods/cleanRoomHistory'; -import './methods/createChannel'; -import './methods/createPrivateGroup'; -import './methods/deleteUserOwnAccount'; -import './methods/executeSlashCommandPreview'; -import './startup/mentionUserNotInChannel'; -import './methods/getChannelHistory'; -import './methods/getRoomJoinCode'; -import './methods/getRoomRoles'; -import './methods/getSingleMessage'; -import './methods/getMessages'; -import './methods/getSlashCommandPreviews'; -import './methods/getUsernameSuggestion'; -import './methods/joinDefaultChannels'; -import './methods/joinRoom'; -import './methods/leaveRoom'; -import './methods/refreshOAuthService'; -import './methods/removeOAuthService'; -import './methods/restartServer'; -import './methods/saveSetting'; -import './methods/saveSettings'; -import './methods/sendMessage'; -import './methods/sendSMTPTestEmail'; -import './methods/setEmail'; -import './methods/setRealName'; -import './methods/unarchiveRoom'; -import './methods/unblockUser'; -import './methods/updateMessage'; -import './methods/checkFederationConfiguration'; - -export * from './lib'; diff --git a/apps/meteor/app/lib/server/lib/afterSaveMessage.ts b/apps/meteor/app/lib/server/lib/afterSaveMessage.ts deleted file mode 100644 index d1cf0e15927bf..0000000000000 --- a/apps/meteor/app/lib/server/lib/afterSaveMessage.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Message } from '@rocket.chat/core-services'; -import type { IMessage, IUser, IRoom } from '@rocket.chat/core-typings'; -import type { Updater } from '@rocket.chat/models'; -import { Rooms } from '@rocket.chat/models'; - -import { callbacks } from '../../../../server/lib/callbacks'; -import type { SendMessageOptions } from '../functions/sendMessage'; - -export async function afterSaveMessage( - message: IMessage, - room: IRoom, - user: IUser, - { - roomUpdater, - options, - }: { - roomUpdater?: Updater; - options?: SendMessageOptions; - } = {}, -): Promise { - const updater = roomUpdater ?? Rooms.getUpdater(); - - const data: IMessage = (await callbacks.run('afterSaveMessage', message, { - room, - user, - roomUpdater: updater, - options, - })) as unknown as IMessage; - - if (!roomUpdater && updater.hasChanges()) { - await Rooms.updateFromUpdater({ _id: room._id }, updater); - } - - void Message.afterSave({ message: data }); - - // TODO: Fix type - callback configuration needs to be updated - return data; -} - -export function afterSaveMessageAsync( - message: IMessage, - room: IRoom, - user: IUser, - { - roomUpdater: updater, - options, - }: { - roomUpdater?: Updater; - options?: SendMessageOptions; - } = {}, -): void { - const roomUpdater = updater ?? Rooms.getUpdater(); - - callbacks.runAsync('afterSaveMessage', message, { room, user, roomUpdater, options }); - - if (roomUpdater.hasChanges()) { - void Rooms.updateFromUpdater({ _id: room._id }, roomUpdater); - } - - void Message.afterSave({ message }); -} diff --git a/apps/meteor/app/lib/server/lib/afterUserActions.ts b/apps/meteor/app/lib/server/lib/afterUserActions.ts deleted file mode 100644 index cbc5410acefc9..0000000000000 --- a/apps/meteor/app/lib/server/lib/afterUserActions.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { Subscriptions } from '@rocket.chat/models'; - -import { notifyOnSubscriptionChangedByUserId } from './notifyListener'; -import { callbacks } from '../../../../server/lib/callbacks'; -import { unarchiveUserSubscriptions } from '../functions/unarchiveUserSubscriptions'; - -const handleDeactivateUser = async (user: IUser): Promise => { - const { modifiedCount } = await Subscriptions.setArchivedByUserId(user._id, true); - if (modifiedCount) { - void notifyOnSubscriptionChangedByUserId(user._id); - } -}; - -const handleActivateUser = async (user: IUser): Promise => { - const unarchived = await unarchiveUserSubscriptions(user._id); - if (unarchived) { - void notifyOnSubscriptionChangedByUserId(user._id); - } -}; - -callbacks.add('afterDeactivateUser', handleDeactivateUser, callbacks.priority.LOW, 'subscription-archive-on-deactivate'); - -callbacks.add('afterActivateUser', handleActivateUser, callbacks.priority.LOW, 'subscription-unarchive-on-activate'); diff --git a/apps/meteor/app/lib/server/lib/index.ts b/apps/meteor/app/lib/server/lib/index.ts deleted file mode 100644 index be0fad1f89f41..0000000000000 --- a/apps/meteor/app/lib/server/lib/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - What is this file? Great question! To make Rocket.Chat more "modular" - and to make the "rocketchat:lib" package more of a core package - with the libraries, this index file contains the exported members - for the *server* pieces of code which does include the shared - library files. -*/ -import './afterUserActions'; -import './notifyUsersOnMessage'; - -export { sendNotification } from './sendNotificationsOnMessage'; -export { passwordPolicy } from './passwordPolicy'; -export { validateEmailDomain } from './validateEmailDomain'; -export { RateLimiterClass as RateLimiter } from './RateLimiter'; -export { msgStream } from './msgStream'; diff --git a/apps/meteor/app/lib/server/lib/msgStream.ts b/apps/meteor/app/lib/server/lib/msgStream.ts deleted file mode 100644 index 208c3ab14f549..0000000000000 --- a/apps/meteor/app/lib/server/lib/msgStream.ts +++ /dev/null @@ -1,3 +0,0 @@ -import notifications from '../../../notifications/server/lib/Notifications'; - -export const msgStream = notifications.streamRoomMessage; diff --git a/apps/meteor/app/lib/server/methods/addUserToRoom.ts b/apps/meteor/app/lib/server/methods/addUserToRoom.ts deleted file mode 100644 index 4c23ae7494ad4..0000000000000 --- a/apps/meteor/app/lib/server/methods/addUserToRoom.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { addUsersToRoomMethod } from './addUsersToRoom'; -import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - addUserToRoom(data: { rid: string; username: string }): void; - } -} - -Meteor.methods({ - async addUserToRoom(data) { - methodDeprecationLogger.method('addUserToRoom', '9.0.0', ['/v1/channels.invite', '/v1/groups.invite']); - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'addUserToRoom', - }); - } - - await addUsersToRoomMethod(userId, { - rid: data.rid, - users: [data.username], - }); - }, -}); diff --git a/apps/meteor/app/lib/server/methods/archiveRoom.ts b/apps/meteor/app/lib/server/methods/archiveRoom.ts deleted file mode 100644 index 5a7e989df175d..0000000000000 --- a/apps/meteor/app/lib/server/methods/archiveRoom.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { isRegisterUser } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Users, Rooms } from '@rocket.chat/models'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; -import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { archiveRoom } from '../functions/archiveRoom'; -import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - archiveRoom(rid: string): Promise; - } -} - -export const executeArchiveRoom = async (userId: string, rid: string) => { - check(rid, String); - - const user = await Users.findOneById(userId, { projection: { username: 1, name: 1 } }); - if (!user || !isRegisterUser(user)) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'archiveRoom' }); - } - - const room = await Rooms.findOneById(rid); - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'archiveRoom' }); - } - - if (!(await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.ARCHIVE, userId))) { - throw new Meteor.Error('error-direct-message-room', `rooms type: ${room.t} can not be archived`, { method: 'archiveRoom' }); - } - - if (!(await hasPermissionAsync(userId, 'archive-room', room._id))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'archiveRoom' }); - } - - return archiveRoom(rid, user); -}; - -Meteor.methods({ - async archiveRoom(rid) { - methodDeprecationLogger.method('archiveRoom', '9.0.0', '/v1/channels.archive'); - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'archiveRoom' }); - } - - return executeArchiveRoom(userId, rid); - }, -}); diff --git a/apps/meteor/app/lib/server/methods/blockUser.ts b/apps/meteor/app/lib/server/methods/blockUser.ts deleted file mode 100644 index cc873f62e8e24..0000000000000 --- a/apps/meteor/app/lib/server/methods/blockUser.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { blockUserMethod } from '../functions/blockUser'; -import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - blockUser({ rid, blocked }: { rid: string; blocked: string }): boolean; - } -} - -Meteor.methods({ - async blockUser({ rid, blocked }) { - methodDeprecationLogger.method('blockUser', '9.0.0', '/v1/im.blockUser'); - check(rid, String); - check(blocked, String); - - const userId = Meteor.userId(); - - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'blockUser' }); - } - - await blockUserMethod(userId, { rid, blocked }); - - return true; - }, -}); diff --git a/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts b/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts deleted file mode 100644 index 23bd39f3090fb..0000000000000 --- a/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Match, check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { findRoomByIdOrName } from '../../../api/server/v1/rooms'; -import { canAccessRoomAsync } from '../../../authorization/server'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { cleanRoomHistory } from '../functions/cleanRoomHistory'; - -type CleanRoomHistoryParams = { - roomId: string; - latest: Date; - oldest: Date; - inclusive?: boolean; - limit?: number; - excludePinned?: boolean; - ignoreDiscussion?: boolean; - filesOnly?: boolean; - fromUsers?: string[]; - ignoreThreads?: boolean; -}; -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - cleanRoomHistory(data: CleanRoomHistoryParams): number; - } -} - -export const cleanRoomHistoryMethod = async ( - userId: string, - { - roomId, - latest, - oldest, - inclusive = true, - limit, - excludePinned = false, - ignoreDiscussion = true, - filesOnly = false, - fromUsers = [], - ignoreThreads, - }: CleanRoomHistoryParams, -): Promise => { - if (!(await hasPermissionAsync(userId, 'clean-channel-history', roomId))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' }); - } - - const room = await findRoomByIdOrName({ params: { roomId } }); - - if (!room || !(await canAccessRoomAsync(room, { _id: userId }))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' }); - } - - return cleanRoomHistory({ - rid: roomId, - latest, - oldest, - inclusive, - limit, - excludePinned, - ignoreDiscussion, - filesOnly, - fromUsers, - ignoreThreads, - }); -}; - -Meteor.methods({ - async cleanRoomHistory({ - roomId, - latest, - oldest, - inclusive = true, - limit, - excludePinned = false, - ignoreDiscussion = true, - filesOnly = false, - fromUsers = [], - ignoreThreads, - }) { - check(roomId, String); - check(latest, Date); - check(oldest, Date); - check(inclusive, Boolean); - check(limit, Match.Maybe(Number)); - check(excludePinned, Match.Maybe(Boolean)); - check(filesOnly, Match.Maybe(Boolean)); - check(ignoreThreads, Match.Maybe(Boolean)); - check(fromUsers, Match.Maybe([String])); - - const userId = Meteor.userId(); - - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cleanRoomHistory' }); - } - - return cleanRoomHistoryMethod(userId, { - roomId, - latest, - oldest, - inclusive, - limit, - excludePinned, - ignoreDiscussion, - filesOnly, - fromUsers, - ignoreThreads, - }); - }, -}); diff --git a/apps/meteor/app/lib/server/methods/createToken.ts b/apps/meteor/app/lib/server/methods/createToken.ts deleted file mode 100644 index 63de5b98f4210..0000000000000 --- a/apps/meteor/app/lib/server/methods/createToken.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { MeteorError, User } from '@rocket.chat/core-services'; -import { Accounts } from 'meteor/accounts-base'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - createToken(userId: string): { userId: string; authToken: string }; - } -} - -const { CREATE_TOKENS_FOR_USERS_SECRET } = process.env; - -export async function generateAccessToken(userId: string, secret: string) { - if (secret !== CREATE_TOKENS_FOR_USERS_SECRET) { - throw new MeteorError('error-not-authorized', 'Not authorized'); - } - - const token = Accounts._generateStampedLoginToken(); - Accounts._insertLoginToken(userId, token); - - await User.ensureLoginTokensLimit(userId); - - return { - userId, - authToken: token.token, - }; -} diff --git a/apps/meteor/app/lib/server/methods/getUsernameSuggestion.ts b/apps/meteor/app/lib/server/methods/getUsernameSuggestion.ts deleted file mode 100644 index 601f0e9564b71..0000000000000 --- a/apps/meteor/app/lib/server/methods/getUsernameSuggestion.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { generateUsernameSuggestion } from '../functions/getUsernameSuggestion'; -import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - getUsernameSuggestion(): Promise; - } -} - -Meteor.methods({ - async getUsernameSuggestion() { - methodDeprecationLogger.method('getUsernameSuggestion', '9.0.0', '/v1/users.getUsernameSuggestion'); - const user = await Meteor.userAsync(); - - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'getUsernameSuggestion', - }); - } - - return generateUsernameSuggestion(user); - }, -}); diff --git a/apps/meteor/app/lib/server/methods/saveSettings.ts b/apps/meteor/app/lib/server/methods/saveSettings.ts deleted file mode 100644 index c01f40481b106..0000000000000 --- a/apps/meteor/app/lib/server/methods/saveSettings.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { ISetting } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; -import { saveSettingsBulk } from '../functions/saveSettingsBulk'; -import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - saveSettings( - changes: { - _id: ISetting['_id']; - value: ISetting['value']; - }[], - ): Promise; - } -} - -Meteor.methods({ - saveSettings: twoFactorRequired(async function ( - params: { - _id: ISetting['_id']; - value: ISetting['value']; - }[] = [], - ) { - methodDeprecationLogger.method('saveSettings', '9.0.0', '/v1/settings'); - - const uid = Meteor.userId(); - if (uid === null) { - throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { - method: 'saveSetting', - }); - } - - await saveSettingsBulk(uid, params, { - username: (await Meteor.userAsync())!.username!, - ip: this.connection.clientAddress || '', - useragent: this.connection.httpHeaders['user-agent'] || '', - }); - - return true; - }, {}), -}); diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts deleted file mode 100644 index cf5deabc68a66..0000000000000 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { api } from '@rocket.chat/core-services'; -import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; -import { MessageTypes } from '@rocket.chat/message-types'; -import { Messages, Users } from '@rocket.chat/models'; -import type { TOptions } from 'i18next'; -import { check, Match } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; -import moment from 'moment'; - -import { i18n } from '../../../../server/lib/i18n'; -import { SystemLogger } from '../../../../server/lib/logger/system'; -import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { applyAirGappedRestrictionsValidation } from '../../../license/server/airGappedRestrictionsWrapper'; -import { metrics } from '../../../metrics/server'; -import { settings } from '../../../settings/server'; -import { sendMessage } from '../functions/sendMessage'; -import { RateLimiter } from '../lib'; -/** - * - * @param uid - * @param message - * @param extraInfo - * - ts: The timestamp of the message. the message object already has a ts, but this value is validated and only a window of 10 seconds is allowed to be used. this value overrides the message.ts value without validation. - * - * - * @returns - */ -export async function executeSendMessage( - uid: IUser['_id'] | IUser, - message: AtLeast, - extraInfo?: { ts?: Date; previewUrls?: string[] }, -) { - if (message.tshow && !message.tmid) { - throw new Meteor.Error('invalid-params', 'tshow provided but missing tmid', { - method: 'sendMessage', - }); - } - - if (message.tmid && !settings.get('Threads_enabled')) { - throw new Meteor.Error('error-not-allowed', 'not-allowed', { - method: 'sendMessage', - }); - } - - const isTimestampFromClient = Boolean(!extraInfo?.ts && message.ts); - const now = new Date(); - message.ts = extraInfo?.ts ?? message.ts ?? now; - if (isTimestampFromClient) { - const tsDiff = Math.abs(moment(message.ts).diff(Date.now())); - 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(), - }); - } - if (tsDiff > 10000) { - message.ts = now; - } - } - - if (message.msg) { - if (message.msg.length > (settings.get('Message_MaxAllowedSize') ?? 0)) { - throw new Meteor.Error('error-message-size-exceeded', 'Message size exceeds Message_MaxAllowedSize', { - method: 'sendMessage', - }); - } - } - - const user = typeof uid === 'string' ? await Users.findOneById(uid) : uid; - if (!user?.username) { - throw new Meteor.Error('error-invalid-user', 'Invalid user'); - } - - let { rid } = message; - - // do not allow nested threads - if (message.tmid) { - const parentMessage = await Messages.findOneById(message.tmid, { projection: { rid: 1, tmid: 1 } }); - message.tmid = parentMessage?.tmid || message.tmid; - - if (parentMessage?.rid) { - rid = parentMessage?.rid; - } - } - - if (!rid) { - throw new Error("The 'rid' property on the message object is missing."); - } - - check(rid, String); - - try { - const room = await canSendMessageAsync(rid, user); - - if (room.encrypted && settings.get('E2E_Enable') && !settings.get('E2E_Allow_Unencrypted_Messages')) { - if (message.t !== 'e2e') { - throw new Meteor.Error('error-not-allowed', 'Not allowed to send un-encrypted messages in an encrypted room', { - method: 'sendMessage', - }); - } - } - - const result = await sendMessage(user, message, room, { previewUrls: extraInfo?.previewUrls }); - - metrics.messagesSent.inc(); - metrics.messagesSentTotal.inc(); - - return result; - } catch (err: any) { - SystemLogger.error({ msg: 'Error sending message:', err }); - - const errorMessage: RocketchatI18nKeys = typeof err === 'string' ? err : err.error || err.message; - const errorContext: TOptions = err.details ?? {}; - void api.broadcast('notify.ephemeralMessage', user._id, message.rid, { - msg: i18n.t(errorMessage, { ...errorContext, lng: user.language }), - }); - - if (typeof err === 'string') { - throw new Error(err); - } - - throw err; - } -} - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - sendMessage(message: AtLeast, previewUrls?: string[]): any; - } -} - -Meteor.methods({ - async sendMessage(message, previewUrls) { - check(message, { - _id: Match.Maybe(String), - rid: Match.Maybe(String), - msg: Match.Maybe(String), - tmid: Match.Maybe(String), - tshow: Match.Maybe(Boolean), - ts: Match.Maybe(Date), - t: Match.Maybe(String), - bot: Match.Maybe(Object), - content: Match.Maybe(Object), - e2e: Match.Maybe(String), - e2eMentions: Match.Maybe(Object), - customFields: Match.Maybe(Object), - federation: Match.Maybe(Object), - groupable: Match.Maybe(Boolean), - sentByEmail: Match.Maybe(Boolean), - }); - - const user = (await Meteor.userAsync()) as IUser; - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'sendMessage', - }); - } - - if (MessageTypes.isSystemMessage(message)) { - throw new Error("Cannot send system messages using 'sendMessage'"); - } - - try { - return await applyAirGappedRestrictionsValidation(() => executeSendMessage(user, message, { previewUrls })); - } catch (error: any) { - if (['error-not-allowed', 'restricted-workspace'].includes(error.error || error.message)) { - throw new Meteor.Error(error.error || error.message, error.reason, { - method: 'sendMessage', - }); - } - } - }, -}); -// Limit a user, who does not have the "bot" role, to sending 5 msgs/second -RateLimiter.limitMethod('sendMessage', 5, 1000, { - async userId(userId: IUser['_id']) { - return !(await hasPermissionAsync(userId, 'send-many-messages')); - }, -}); diff --git a/apps/meteor/app/lib/server/methods/setEmail.ts b/apps/meteor/app/lib/server/methods/setEmail.ts deleted file mode 100644 index 67c59572631d1..0000000000000 --- a/apps/meteor/app/lib/server/methods/setEmail.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../../settings/server'; -import { setEmail } from '../functions/setEmail'; -import { RateLimiter } from '../lib'; -import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - setEmail(email: string): string; - } -} - -export const setEmailFunction = async (email: string, user: Meteor.User | IUser) => { - check(email, String); - - if (!settings.get('Accounts_AllowEmailChange')) { - throw new Meteor.Error('error-action-not-allowed', 'Changing email is not allowed', { - method: 'setEmail', - action: 'Changing_email', - }); - } - - if (user.emails?.[0]?.address === email) { - return email; - } - - if (!(await setEmail(user._id, email))) { - throw new Meteor.Error('error-could-not-change-email', 'Could not change email', { - method: 'setEmail', - }); - } - - return email; -}; - -Meteor.methods({ - async setEmail(email) { - methodDeprecationLogger.method('setEmail', '9.0.0', '/v1/users.updateOwnBasicInfo'); - const user = await Meteor.userAsync(); - - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setEmail' }); - } - - return setEmailFunction(email, user); - }, -}); - -RateLimiter.limitMethod('setEmail', 1, 1000, { - userId(/* userId*/) { - return true; - }, -}); diff --git a/apps/meteor/app/lib/server/methods/setRealName.ts b/apps/meteor/app/lib/server/methods/setRealName.ts deleted file mode 100644 index da72e1b6257bd..0000000000000 --- a/apps/meteor/app/lib/server/methods/setRealName.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../../settings/server'; -import { setRealName } from '../functions/setRealName'; -import { RateLimiter } from '../lib'; -import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - setRealName(name: string): string; - } -} - -Meteor.methods({ - async setRealName(name) { - methodDeprecationLogger.method('setRealName', '9.0.0', '/v1/users.updateOwnBasicInfo'); - check(name, String); - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setRealName' }); - } - - if (!settings.get('Accounts_AllowRealNameChange')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setRealName' }); - } - - if (!(await setRealName(userId, name))) { - throw new Meteor.Error('error-could-not-change-name', 'Could not change name', { - method: 'setRealName', - }); - } - - return name; - }, -}); - -RateLimiter.limitMethod('setRealName', 1, 1000, { - userId: () => true, -}); diff --git a/apps/meteor/app/lib/server/methods/unarchiveRoom.ts b/apps/meteor/app/lib/server/methods/unarchiveRoom.ts deleted file mode 100644 index 389f0af6c8d5f..0000000000000 --- a/apps/meteor/app/lib/server/methods/unarchiveRoom.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { isRegisterUser } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Users, Rooms } from '@rocket.chat/models'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { unarchiveRoom } from '../functions/unarchiveRoom'; -import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - unarchiveRoom(rid: string): Promise; - } -} - -export const executeUnarchiveRoom = async (userId: string, rid: string) => { - check(rid, String); - - const user = await Users.findOneById(userId, { projection: { username: 1, name: 1 } }); - if (!user || !isRegisterUser(user)) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'archiveRoom' }); - } - - const room = await Rooms.findOneById(rid); - - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'unarchiveRoom' }); - } - - if (!(await hasPermissionAsync(userId, 'unarchive-room', room._id))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'unarchiveRoom' }); - } - - return unarchiveRoom(rid, user); -}; - -Meteor.methods({ - async unarchiveRoom(rid) { - methodDeprecationLogger.method('unarchiveRoom', '9.0.0', '/v1/channels.unarchive'); - const userId = Meteor.userId(); - - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'unarchiveRoom' }); - } - - return executeUnarchiveRoom(userId, rid); - }, -}); diff --git a/apps/meteor/app/lib/server/methods/unblockUser.ts b/apps/meteor/app/lib/server/methods/unblockUser.ts deleted file mode 100644 index ef940bf40b4eb..0000000000000 --- a/apps/meteor/app/lib/server/methods/unblockUser.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { unblockUserMethod } from '../functions/unblockUser'; -import { methodDeprecationLogger } from '../lib/deprecationWarningLogger'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - unblockUser({ rid, blocked }: { rid: string; blocked: string }): boolean; - } -} - -Meteor.methods({ - async unblockUser({ rid, blocked }) { - methodDeprecationLogger.method('unblockUser', '9.0.0', '/v1/im.blockUser'); - check(rid, String); - check(blocked, String); - - const userId = Meteor.userId(); - - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'unblockUser' }); - } - - await unblockUserMethod(userId, { rid, blocked }); - - return true; - }, -}); diff --git a/apps/meteor/app/lib/server/methods/updateMessage.ts b/apps/meteor/app/lib/server/methods/updateMessage.ts deleted file mode 100644 index 45ba42f25f000..0000000000000 --- a/apps/meteor/app/lib/server/methods/updateMessage.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { IEditedMessage, IMessage, IUser, AtLeast } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Messages, Users } from '@rocket.chat/models'; -import { Match, check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; -import moment from 'moment'; - -import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { applyAirGappedRestrictionsValidation } from '../../../license/server/airGappedRestrictionsWrapper'; -import { settings } from '../../../settings/server'; -import { updateMessage } from '../functions/updateMessage'; - -const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg', 'customFields', 'content', 'e2eMentions']; - -export async function executeUpdateMessage( - uid: IUser['_id'], - message: AtLeast | AtLeast, - previewUrls?: string[], -) { - const originalMessage = await Messages.findOneById(message._id); - if (!originalMessage?._id) { - return; - } - - Object.entries(message).forEach(([key, value]) => { - if (!allowedEditedFields.includes(key) && value !== originalMessage[key as keyof IMessage]) { - throw new Meteor.Error('error-invalid-update-key', `Cannot update the message ${key}`, { - method: 'updateMessage', - }); - } - }); - - // IF the message has custom fields, always update - // Ideally, we'll compare the custom fields to check for change, but since we don't know the shape of - // custom fields, as it's user defined, we're gonna update - const msgText = originalMessage?.attachments?.[0]?.description ?? originalMessage.msg; - if (msgText === message.msg && !previewUrls && !message.customFields) { - return; - } - - if (!!message.tmid && originalMessage._id === message.tmid) { - throw new Meteor.Error('error-message-same-as-tmid', 'Cannot set tmid the same as the _id', { - method: 'updateMessage', - }); - } - - if (!originalMessage.tmid && !!message.tmid) { - throw new Meteor.Error('error-message-change-to-thread', 'Cannot update message to a thread', { method: 'updateMessage' }); - } - - const _hasPermission = await hasPermissionAsync(uid, 'edit-message', message.rid); - const editAllowed = settings.get('Message_AllowEditing'); - const editOwn = originalMessage.u && originalMessage.u._id === uid; - - if (!_hasPermission && (!editAllowed || !editOwn)) { - throw new Meteor.Error('error-action-not-allowed', 'Message editing not allowed', { - method: 'updateMessage', - action: 'Message_editing', - }); - } - - const blockEditInMinutes = settings.get('Message_AllowEditing_BlockEditInMinutes'); - const bypassBlockTimeLimit = await hasPermissionAsync(uid, 'bypass-time-limit-edit-and-delete', message.rid); - - if (!bypassBlockTimeLimit && Match.test(blockEditInMinutes, Number) && blockEditInMinutes !== 0) { - let currentTsDiff = 0; - let msgTs; - - if (originalMessage.ts instanceof Date || Match.test(originalMessage.ts, Number)) { - msgTs = moment(originalMessage.ts); - } - if (msgTs) { - currentTsDiff = moment().diff(msgTs, 'minutes'); - } - if (currentTsDiff >= blockEditInMinutes) { - throw new Meteor.Error('error-message-editing-blocked', 'Message editing is blocked', { - method: 'updateMessage', - }); - } - } - - const user = await Users.findOneById(uid); - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateMessage' }); - } - await canSendMessageAsync(message.rid, { uid: user._id, username: user.username ?? undefined, ...user }); - - // It is possible to have an empty array as the attachments property, so ensure both things exist - if (originalMessage.attachments && originalMessage.attachments.length > 0 && originalMessage.attachments[0].description !== undefined) { - originalMessage.attachments[0].description = message.msg; - message.attachments = originalMessage.attachments; - message.msg = originalMessage.msg; - } - - message.u = originalMessage.u; - - return updateMessage(message, user, originalMessage, previewUrls); -} - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - updateMessage(message: IEditedMessage, previewUrls?: string[]): void; - } -} - -Meteor.methods({ - async updateMessage(message: IEditedMessage, previewUrls?: string[]) { - check(message, Match.ObjectIncluding({ _id: String })); - check(previewUrls, Match.Maybe([String])); - - const uid = Meteor.userId(); - - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateMessage' }); - } - - return applyAirGappedRestrictionsValidation(() => executeUpdateMessage(uid, message, previewUrls)); - }, -}); diff --git a/apps/meteor/app/lib/server/startup/index.ts b/apps/meteor/app/lib/server/startup/index.ts deleted file mode 100644 index 94084edafb398..0000000000000 --- a/apps/meteor/app/lib/server/startup/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './rateLimiter'; -import './robots'; diff --git a/apps/meteor/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx b/apps/meteor/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx index 297837a3bddf6..aebccb5cf3fc0 100644 --- a/apps/meteor/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx +++ b/apps/meteor/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal.tsx @@ -11,7 +11,7 @@ import { } from '@rocket.chat/fuselage'; import { useTranslation } from 'react-i18next'; -type PlaceChatOnHoldModalProps = { +export type PlaceChatOnHoldModalProps = { onOnHoldChat: () => void; confirm?: () => void; onCancel: () => void; diff --git a/apps/meteor/app/livechat/imports/server/rest/agent.ts b/apps/meteor/app/livechat/imports/server/rest/agent.ts deleted file mode 100644 index 65ac3cd0b6714..0000000000000 --- a/apps/meteor/app/livechat/imports/server/rest/agent.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { isGETLivechatAgentsAgentIdDepartmentsParams } from '@rocket.chat/rest-typings'; - -import { API } from '../../../../api/server'; -import { findAgentDepartments } from '../../../server/api/lib/agents'; - -API.v1.addRoute( - 'livechat/agents/:agentId/departments', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETLivechatAgentsAgentIdDepartmentsParams }, - { - async get() { - const departments = await findAgentDepartments({ - enabledDepartmentsOnly: this.queryParams.enabledDepartmentsOnly && this.queryParams.enabledDepartmentsOnly === 'true', - agentId: this.urlParams.agentId, - }); - - return API.v1.success(departments); - }, - }, -); diff --git a/apps/meteor/app/livechat/imports/server/rest/businessHours.ts b/apps/meteor/app/livechat/imports/server/rest/businessHours.ts deleted file mode 100644 index 11f7a99afd856..0000000000000 --- a/apps/meteor/app/livechat/imports/server/rest/businessHours.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; -import { - isGETBusinessHourParams, - isPOSTLivechatBusinessHoursSaveParams, - isPOSTLivechatBusinessHoursRemoveParams, - POSTLivechatBusinessHoursRemoveSuccessResponse, - POSTLivechatBusinessHoursSaveSuccessResponse, - validateBadRequestErrorResponse, - validateUnauthorizedErrorResponse, -} from '@rocket.chat/rest-typings'; - -import { API } from '../../../../api/server'; -import type { ExtractRoutesFromAPI } from '../../../../api/server/ApiClass'; -import { findLivechatBusinessHour } from '../../../server/api/lib/businessHours'; -import { businessHourManager } from '../../../server/business-hour'; - -API.v1.addRoute( - 'livechat/business-hour', - { authRequired: true, permissionsRequired: ['view-livechat-business-hours'], validateParams: isGETBusinessHourParams }, - { - async get() { - const { _id, type } = this.queryParams; - const { businessHour } = await findLivechatBusinessHour(_id, type); - return API.v1.success({ - businessHour, - }); - }, - }, -); - -const livechatBusinessHoursEndpoints = API.v1 - .post( - 'livechat/business-hours.save', - { - response: { - 200: POSTLivechatBusinessHoursSaveSuccessResponse, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - authRequired: true, - body: isPOSTLivechatBusinessHoursSaveParams, - }, - async function action() { - const params = this.bodyParams; - - // TODO: Remove typecasting after refactoring saveBusinessHour logic with proper type logic. See: CORE-1552 - const result = await businessHourManager.saveBusinessHour(params as unknown as ILivechatBusinessHour); - - return API.v1.success(result); - } - ) - .post( - 'livechat/business-hours.remove', - { - response: { - 200: POSTLivechatBusinessHoursRemoveSuccessResponse, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - authRequired: true, - body: isPOSTLivechatBusinessHoursRemoveParams, - }, - async function action() { - const { _id, type } = this.bodyParams; - - await businessHourManager.removeBusinessHourByIdAndType(_id, type); - - return API.v1.success(); - } - ); - -type LivechatBusinessHoursEndpoints = ExtractRoutesFromAPI; - -declare module '@rocket.chat/rest-typings' { - // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface - interface Endpoints extends LivechatBusinessHoursEndpoints {} -} diff --git a/apps/meteor/app/livechat/imports/server/rest/dashboards.ts b/apps/meteor/app/livechat/imports/server/rest/dashboards.ts deleted file mode 100644 index 1e4b6850b45b8..0000000000000 --- a/apps/meteor/app/livechat/imports/server/rest/dashboards.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { OmnichannelAnalytics } from '@rocket.chat/core-services'; -import { Users } from '@rocket.chat/models'; -import { - isGETDashboardTotalizerParams, - isGETDashboardsAgentStatusParams, - isGETLivechatAnalyticsDashboardsChartDataParams, - validateUnauthorizedErrorResponse, - validateForbiddenErrorResponse, - GETLivechatAnalyticsDashboardsChartDataSuccessSchema, -} from '@rocket.chat/rest-typings'; - -import { API } from '../../../../api/server'; -import type { ExtractRoutesFromAPI } from '../../../../api/server/ApiClass'; -import { - getProductivityMetricsAsyncCached, - getConversationsMetricsAsyncCached, - getAgentsProductivityMetricsAsyncCached, - getChatsMetricsAsyncCached, - findAllChatsStatusAsyncCached, - findAllChatMetricsByAgentAsyncCached, - findAllAgentsStatusAsyncCached, - findAllChatMetricsByDepartmentAsyncCached, - findAllResponseTimeMetricsAsyncCached, -} from '../../../server/lib/analytics/dashboards'; - -API.v1.addRoute( - 'livechat/analytics/dashboards/conversation-totalizers', - { - authRequired: true, - permissionsRequired: ['view-livechat-manager'], - validateParams: isGETDashboardTotalizerParams, - }, - { - async get() { - const { start, end } = this.queryParams; - const { departmentId } = this.queryParams; - - if (isNaN(Date.parse(start))) { - return API.v1.failure('The "start" query parameter must be a valid date.'); - } - - if (isNaN(Date.parse(end))) { - return API.v1.failure('The "end" query parameter must be a valid date.'); - } - - const user = await Users.findOneById(this.userId, { projection: { utcOffset: 1, language: 1 } }); - if (!user) { - return API.v1.failure('User not found'); - } - - const totalizers = await getConversationsMetricsAsyncCached({ start, end, departmentId, user }); - return API.v1.success(totalizers); - }, - }, -); - -API.v1.addRoute( - 'livechat/analytics/dashboards/agents-productivity-totalizers', - { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETDashboardTotalizerParams }, - { - async get() { - const { start, end } = this.queryParams; - const { departmentId } = this.queryParams; - - if (isNaN(Date.parse(start))) { - return API.v1.failure('The "start" query parameter must be a valid date.'); - } - - if (isNaN(Date.parse(end))) { - return API.v1.failure('The "end" query parameter must be a valid date.'); - } - - const user = await Users.findOneById(this.userId, { projection: { utcOffset: 1, language: 1 } }); - if (!user) { - return API.v1.failure('User not found'); - } - - const totalizers = await getAgentsProductivityMetricsAsyncCached({ start, end, departmentId, user }); - return API.v1.success(totalizers); - }, - }, -); - -API.v1.addRoute( - 'livechat/analytics/dashboards/chats-totalizers', - { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETDashboardTotalizerParams }, - { - async get() { - const { start, end } = this.queryParams; - const { departmentId } = this.queryParams; - - if (isNaN(Date.parse(start))) { - return API.v1.failure('The "start" query parameter must be a valid date.'); - } - const startDate = new Date(start); - - if (isNaN(Date.parse(end))) { - return API.v1.failure('The "end" query parameter must be a valid date.'); - } - const endDate = new Date(end); - - const totalizers = await getChatsMetricsAsyncCached({ start: startDate, end: endDate, departmentId }); - return API.v1.success(totalizers); - }, - }, -); - -API.v1.addRoute( - 'livechat/analytics/dashboards/productivity-totalizers', - { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETDashboardTotalizerParams }, - { - async get() { - const { start, end } = this.queryParams; - const { departmentId } = this.queryParams; - - if (isNaN(Date.parse(start))) { - return API.v1.failure('The "start" query parameter must be a valid date.'); - } - - if (isNaN(Date.parse(end))) { - return API.v1.failure('The "end" query parameter must be a valid date.'); - } - - const user = await Users.findOneById(this.userId, { projection: { utcOffset: 1, language: 1 } }); - if (!user) { - return API.v1.failure('User not found'); - } - - const totalizers = await getProductivityMetricsAsyncCached({ start, end, departmentId, user }); - - return API.v1.success(totalizers); - }, - }, -); - -API.v1.addRoute( - 'livechat/analytics/dashboards/charts/chats', - { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETDashboardTotalizerParams }, - { - async get() { - const { start, end } = this.queryParams; - const { departmentId } = this.queryParams; - - if (isNaN(Date.parse(start))) { - return API.v1.failure('The "start" query parameter must be a valid date.'); - } - const startDate = new Date(start); - - if (isNaN(Date.parse(end))) { - return API.v1.failure('The "end" query parameter must be a valid date.'); - } - const endDate = new Date(end); - - const result = await findAllChatsStatusAsyncCached({ start: startDate, end: endDate, departmentId }); - - return API.v1.success(result); - }, - }, -); - -API.v1.addRoute( - 'livechat/analytics/dashboards/charts/chats-per-agent', - { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETDashboardTotalizerParams }, - { - async get() { - const { start, end } = this.queryParams; - const { departmentId } = this.queryParams; - - if (isNaN(Date.parse(start))) { - return API.v1.failure('The "start" query parameter must be a valid date.'); - } - const startDate = new Date(start); - - if (isNaN(Date.parse(end))) { - return API.v1.failure('The "end" query parameter must be a valid date.'); - } - const endDate = new Date(end); - const result = await findAllChatMetricsByAgentAsyncCached({ start: startDate, end: endDate, departmentId }); - - return API.v1.success(result); - }, - }, -); - -API.v1.addRoute( - 'livechat/analytics/dashboards/charts/agents-status', - { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETDashboardsAgentStatusParams }, - { - async get() { - const { departmentId } = this.queryParams; - - const result = await findAllAgentsStatusAsyncCached({ departmentId }); - - return API.v1.success(result); - }, - }, -); - -API.v1.addRoute( - 'livechat/analytics/dashboards/charts/chats-per-department', - { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETDashboardTotalizerParams }, - { - async get() { - const { start, end } = this.queryParams; - const { departmentId } = this.queryParams; - - if (isNaN(Date.parse(start))) { - return API.v1.failure('The "start" query parameter must be a valid date.'); - } - const startDate = new Date(start); - - if (isNaN(Date.parse(end))) { - return API.v1.failure('The "end" query parameter must be a valid date.'); - } - const endDate = new Date(end); - - const result = (await findAllChatMetricsByDepartmentAsyncCached({ start: startDate, end: endDate, departmentId })) as { - [k: string]: { open: number; closed: number }; - }; - - return API.v1.success(result); - }, - }, -); - -API.v1.addRoute( - 'livechat/analytics/dashboards/charts/timings', - { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETDashboardTotalizerParams }, - { - async get() { - const { start, end } = this.queryParams; - const { departmentId } = this.queryParams; - - if (isNaN(Date.parse(start))) { - return API.v1.failure('The "start" query parameter must be a valid date.'); - } - const startDate = new Date(start); - - if (isNaN(Date.parse(end))) { - return API.v1.failure('The "end" query parameter must be a valid date.'); - } - const endDate = new Date(end); - - const result = await findAllResponseTimeMetricsAsyncCached({ start: startDate, end: endDate, departmentId }); - - return API.v1.success(result); - }, - }, -); - -const livechatAnalyticsEndpoints = API.v1.get( - 'livechat/analytics/dashboards/charts-data', - { - response: { - 200: GETLivechatAnalyticsDashboardsChartDataSuccessSchema, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - authRequired: true, - permissionsRequired: ['view-livechat-manager'], - query: isGETLivechatAnalyticsDashboardsChartDataParams, - }, - async function action() { - const { chartName, start, end, departmentId } = this.queryParams; - - const chartData = await OmnichannelAnalytics.getAnalyticsChartData({ - daterange: { - from: start, - to: end, - }, - chartOptions: { - name: chartName, - }, - utcOffset: this.user.utcOffset, - executedBy: this.user._id, - departmentId, - }); - - if (!chartData) { - return API.v1.success({ - chartLabel: chartName, - dataLabels: [], - dataPoints: [], - }); - } - - return API.v1.success(chartData); - }, -); - -type LivechatAnalyticsEndpoints = ExtractRoutesFromAPI; - -declare module '@rocket.chat/rest-typings' { - // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface - interface Endpoints extends LivechatAnalyticsEndpoints {} -} diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts deleted file mode 100644 index 6c4bada41c6e0..0000000000000 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ /dev/null @@ -1,326 +0,0 @@ -import type { ILivechatDepartment } from '@rocket.chat/core-typings'; -import { LivechatDepartment, LivechatDepartmentAgents } from '@rocket.chat/models'; -import { isDepartmentCreationAvailable } from '@rocket.chat/omni-core'; -import { isGETLivechatDepartmentProps, isPOSTLivechatDepartmentProps } from '@rocket.chat/rest-typings'; -import { - isLivechatDepartmentDepartmentIdAgentsGETProps, - isLivechatDepartmentDepartmentIdAgentsPOSTProps, -} from '@rocket.chat/rest-typings/src/v1/omnichannel'; -import { Match, check } from 'meteor/check'; - -import { API } from '../../../../api/server'; -import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; -import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { settings } from '../../../../settings/server'; -import { - findDepartments, - findDepartmentById, - findDepartmentsToAutocomplete, - findDepartmentsBetweenIds, - findDepartmentAgents, - findArchivedDepartments, -} from '../../../server/api/lib/departments'; -import { - saveDepartment, - archiveDepartment, - unarchiveDepartment, - saveDepartmentAgents, - removeDepartment, -} from '../../../server/lib/departmentsLib'; - -API.v1.addRoute( - 'livechat/department', - { - authRequired: true, - validateParams: { GET: isGETLivechatDepartmentProps, POST: isPOSTLivechatDepartmentProps }, - permissionsRequired: { - GET: { permissions: ['view-livechat-departments', 'view-l-room'], operation: 'hasAny' }, - POST: { permissions: ['manage-livechat-departments'], operation: 'hasAll' }, - }, - }, - { - async get() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - - const { text, enabled, onlyMyDepartments, excludeDepartmentId, showArchived } = this.queryParams; - - const { departments, total } = await findDepartments({ - userId: this.userId, - text, - enabled: enabled === 'true', - showArchived: showArchived === 'true', - onlyMyDepartments: onlyMyDepartments === 'true', - excludeDepartmentId, - pagination: { - offset, - count, - // IMO, sort type shouldn't be record, but a generic of the model we're trying to sort - // or the form { [k: keyof T]: number | string } - sort: sort as any, - }, - }); - - return API.v1.success({ departments, count: departments.length, offset, total }); - }, - async post() { - check(this.bodyParams, { - department: Object, - agents: Match.Maybe(Array), - departmentUnit: Match.Maybe({ _id: Match.Optional(String) }), - }); - - const agents = this.bodyParams.agents ? { upsert: this.bodyParams.agents } : {}; - const { departmentUnit } = this.bodyParams; - const department = await saveDepartment( - this.userId, - null, - this.bodyParams.department as ILivechatDepartment, - agents, - departmentUnit || {}, - ); - - if (department) { - return API.v1.success({ - department, - agents: await LivechatDepartmentAgents.find({ departmentId: department._id }).toArray(), - }); - } - - return API.v1.failure(); - }, - }, -); - -API.v1.addRoute( - 'livechat/department/isDepartmentCreationAvailable', - { - authRequired: true, - permissionsRequired: { - GET: { permissions: ['view-livechat-departments', 'manage-livechat-departments'], operation: 'hasAny' }, - }, - }, - { - async get() { - const available = await isDepartmentCreationAvailable(); - return API.v1.success({ isDepartmentCreationAvailable: available }); - }, - }, -); - -API.v1.addRoute( - 'livechat/department/:_id', - { - authRequired: true, - permissionsRequired: { - GET: { permissions: ['view-livechat-departments', 'view-l-room'], operation: 'hasAny' }, - PUT: { permissions: ['manage-livechat-departments', 'add-livechat-department-agents'], operation: 'hasAny' }, - DELETE: { permissions: ['manage-livechat-departments', 'remove-livechat-department'], operation: 'hasAny' }, - }, - }, - { - async get() { - check(this.urlParams, { - _id: String, - }); - - const { onlyMyDepartments } = this.queryParams; - - const { department, agents } = await findDepartmentById({ - userId: this.userId, - departmentId: this.urlParams._id, - includeAgents: this.queryParams.includeAgents && this.queryParams.includeAgents === 'true', - onlyMyDepartments: onlyMyDepartments === 'true', - }); - - // TODO: return 404 when department is not found - // Currently, FE relies on the fact that this endpoint returns an empty payload - // to show the "new" view. Returning 404 breaks it - - return API.v1.success({ department, agents }); - }, - async put() { - const permissionToSave = await hasPermissionAsync(this.userId, 'manage-livechat-departments'); - const permissionToAddAgents = await hasPermissionAsync(this.userId, 'add-livechat-department-agents'); - - check(this.bodyParams, { - department: Object, - agents: Match.Maybe(Array), - departmentUnit: Match.Maybe({ _id: Match.Optional(String) }), - }); - - const { _id } = this.urlParams; - const { department, agents, departmentUnit } = this.bodyParams; - - if (!permissionToSave) { - throw new Error('error-not-allowed'); - } - - const agentParam = permissionToAddAgents && agents ? { upsert: agents } : {}; - await saveDepartment(this.userId, _id, department, agentParam, departmentUnit || {}); - - return API.v1.success({ - department: await LivechatDepartment.findOneById(_id), - agents: await LivechatDepartmentAgents.findByDepartmentId(_id).toArray(), - }); - }, - async delete() { - check(this.urlParams, { - _id: String, - }); - - const isRemoveEnabled = settings.get('Omnichannel_enable_department_removal'); - - if (!isRemoveEnabled) { - return API.v1.failure('error-department-removal-disabled'); - } - - await removeDepartment(this.urlParams._id); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'livechat/departments/archived', - { - authRequired: true, - validateParams: { GET: isGETLivechatDepartmentProps }, - permissionsRequired: { - GET: { permissions: ['view-livechat-departments', 'view-l-room'], operation: 'hasAny' }, - }, - }, - { - async get() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - - const { text, onlyMyDepartments, excludeDepartmentId } = this.queryParams; - - const { departments, total } = await findArchivedDepartments({ - userId: this.userId, - text, - onlyMyDepartments: onlyMyDepartments === 'true', - excludeDepartmentId, - pagination: { - offset, - count, - sort: sort as any, - }, - }); - - return API.v1.success({ departments, count: departments.length, offset, total }); - }, - }, -); - -API.v1.addRoute( - 'livechat/department/:_id/archive', - { - authRequired: true, - permissionsRequired: ['manage-livechat-departments'], - }, - { - async post() { - await archiveDepartment(this.urlParams._id); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'livechat/department/:_id/unarchive', - { - authRequired: true, - permissionsRequired: ['manage-livechat-departments'], - }, - { - async post() { - await unarchiveDepartment(this.urlParams._id); - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'livechat/department.autocomplete', - { authRequired: true, permissionsRequired: { GET: { permissions: ['view-livechat-departments', 'view-l-room'], operation: 'hasAny' } } }, - { - async get() { - const { selector, onlyMyDepartments, showArchived } = this.queryParams; - if (!selector) { - return API.v1.failure("The 'selector' param is required"); - } - - return API.v1.success( - await findDepartmentsToAutocomplete({ - uid: this.userId, - selector: JSON.parse(selector), - onlyMyDepartments: onlyMyDepartments === 'true', - showArchived: showArchived === 'true', - }), - ); - }, - }, -); - -API.v1.addRoute( - 'livechat/department/:_id/agents', - { - authRequired: true, - permissionsRequired: { - GET: { permissions: ['view-livechat-departments', 'view-l-room'], operation: 'hasAny' }, - POST: { permissions: ['manage-livechat-departments', 'add-livechat-department-agents'], operation: 'hasAny' }, - }, - validateParams: { GET: isLivechatDepartmentDepartmentIdAgentsGETProps, POST: isLivechatDepartmentDepartmentIdAgentsPOSTProps }, - }, - { - async get() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - - const agents = await findDepartmentAgents({ - userId: this.userId, - departmentId: this.urlParams._id, - pagination: { - offset, - count, - sort, - }, - }); - - return API.v1.success(agents); - }, - async post() { - await saveDepartmentAgents(this.urlParams._id, this.bodyParams); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'livechat/department.listByIds', - { authRequired: true, permissionsRequired: { GET: { permissions: ['view-livechat-departments', 'view-l-room'], operation: 'hasAny' } } }, - { - async get() { - const { ids } = this.queryParams; - const { fields } = await this.parseJsonQuery(); - if (!ids) { - return API.v1.failure("The 'ids' param is required"); - } - if (!Array.isArray(ids)) { - return API.v1.failure("The 'ids' param must be an array"); - } - - return API.v1.success( - await findDepartmentsBetweenIds({ - ids, - fields, - }), - ); - }, - }, -); diff --git a/apps/meteor/app/livechat/imports/server/rest/inquiries.ts b/apps/meteor/app/livechat/imports/server/rest/inquiries.ts deleted file mode 100644 index 4846fb5100602..0000000000000 --- a/apps/meteor/app/livechat/imports/server/rest/inquiries.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { LivechatInquiryStatus } from '@rocket.chat/core-typings'; -import { LivechatInquiry, LivechatDepartment, Users, LivechatRooms } from '@rocket.chat/models'; -import { - isGETLivechatInquiriesListParams, - isPOSTLivechatInquiriesTakeParams, - isGETLivechatInquiriesQueuedForUserParams, - isGETLivechatInquiriesGetOneParams, - validateBadRequestErrorResponse, - validateUnauthorizedErrorResponse, - validateForbiddenErrorResponse, - isPOSTLivechatInquiriesReturnAsInquiry, - POSTLivechatInquiriesReturnAsInquirySuccessResponse, -} from '@rocket.chat/rest-typings'; - -import { API } from '../../../../api/server'; -import type { ExtractRoutesFromAPI } from '../../../../api/server/ApiClass'; -import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; -import { findInquiries, findOneInquiryByRoomId } from '../../../server/api/lib/inquiries'; -import { returnRoomAsInquiry } from '../../../server/lib/rooms'; -import { takeInquiry } from '../../../server/lib/takeInquiry'; -import { Meteor } from 'meteor/meteor'; - -API.v1.addRoute( - 'livechat/inquiries.list', - { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETLivechatInquiriesListParams }, - { - async get() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - const { department } = this.queryParams; - const ourQuery: { status: string; department?: string } = { status: 'queued' }; - if (department) { - const departmentFromDB = await LivechatDepartment.findOneByIdOrName(department, { projection: { _id: 1 } }); - if (departmentFromDB) { - ourQuery.department = departmentFromDB._id; - } - } - // @ts-expect-error - attachments... - const { cursor, totalCount } = LivechatInquiry.findPaginated(ourQuery, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - projection: { - rid: 1, - name: 1, - ts: 1, - status: 1, - department: 1, - }, - }); - - const [inquiries, total] = await Promise.all([cursor.toArray(), totalCount]); - - return API.v1.success({ - inquiries, - offset, - count: inquiries.length, - total, - }); - }, - }, -); - -API.v1.addRoute( - 'livechat/inquiries.take', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isPOSTLivechatInquiriesTakeParams }, - { - async post() { - if (this.bodyParams.userId && !(await Users.findOneById(this.bodyParams.userId, { projection: { _id: 1 } }))) { - return API.v1.failure('The user is invalid'); - } - return API.v1.success({ - inquiry: await takeInquiry(this.bodyParams.userId || this.userId, this.bodyParams.inquiryId, this.bodyParams.options), - }); - }, - }, -); - -API.v1.addRoute( - 'livechat/inquiries.queuedForUser', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETLivechatInquiriesQueuedForUserParams }, - { - async get() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - const { department } = this.queryParams; - - return API.v1.success( - await findInquiries({ - userId: this.userId, - department, - status: LivechatInquiryStatus.QUEUED, - pagination: { - offset, - count, - sort, - }, - }), - ); - }, - }, -); - -API.v1.addRoute( - 'livechat/inquiries.getOne', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETLivechatInquiriesGetOneParams }, - { - async get() { - const { roomId } = this.queryParams; - - return API.v1.success( - await findOneInquiryByRoomId({ - roomId, - }), - ); - }, - }, -); - -const livechatInquiriesEndpoints = API.v1.post( - 'livechat/inquiries.returnAsInquiry', - { - response: { - 200: POSTLivechatInquiriesReturnAsInquirySuccessResponse, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - authRequired: true, - permissionsRequired: ['view-l-room'], - body: isPOSTLivechatInquiriesReturnAsInquiry, - }, - async function action() { - const { roomId, departmentId } = this.bodyParams; - - try { - const room = await LivechatRooms.findOneById(roomId); - if (!room) { - return API.v1.failure('error-room-not-found'); - } - - const result = await returnRoomAsInquiry(room, departmentId); - - return API.v1.success({ result }); - } catch (error) { - if (error instanceof Meteor.Error && typeof error.error === 'string') { - return API.v1.failure(error.error as string); - } - - return API.v1.failure('error-returning-inquiry'); - } - }, -); - -type LivechatInquiriesEndpoints = ExtractRoutesFromAPI; - -declare module '@rocket.chat/rest-typings' { - // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface - interface Endpoints extends LivechatInquiriesEndpoints {} -} diff --git a/apps/meteor/app/livechat/imports/server/rest/integrations.ts b/apps/meteor/app/livechat/imports/server/rest/integrations.ts deleted file mode 100644 index 5d86202768b8d..0000000000000 --- a/apps/meteor/app/livechat/imports/server/rest/integrations.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { API } from '../../../../api/server'; -import { findIntegrationSettings } from '../../../server/api/lib/integrations'; - -API.v1.addRoute( - 'livechat/integrations.settings', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, - { - async get() { - return API.v1.success(await findIntegrationSettings()); - }, - }, -); diff --git a/apps/meteor/app/livechat/imports/server/rest/rooms.ts b/apps/meteor/app/livechat/imports/server/rest/rooms.ts deleted file mode 100644 index 4fe1e8bb279f6..0000000000000 --- a/apps/meteor/app/livechat/imports/server/rest/rooms.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { LivechatRooms } from '@rocket.chat/models'; -import { isGETLivechatRoomsParams } from '@rocket.chat/rest-typings'; - -import { API } from '../../../../api/server'; -import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; -import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { findRooms } from '../../../server/api/lib/rooms'; - -const validateDateParams = (property: string, date?: string) => { - let parsedDate: { start?: string; end?: string } | undefined = undefined; - if (date) { - parsedDate = JSON.parse(date) as { start?: string; end?: string }; - } - - if (parsedDate?.start && isNaN(Date.parse(parsedDate.start))) { - throw new Error(`The "${property}.start" query parameter must be a valid date.`); - } - if (parsedDate?.end && isNaN(Date.parse(parsedDate.end))) { - throw new Error(`The "${property}.end" query parameter must be a valid date.`); - } - return parsedDate; -}; - -const isBoolean = (value?: string | boolean): boolean => value === 'true' || value === 'false' || typeof value === 'boolean'; - -API.v1.addRoute( - 'livechat/rooms', - { authRequired: true, validateParams: isGETLivechatRoomsParams }, - { - async get() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort, fields, query } = await this.parseJsonQuery(); - const { agents, departmentId, open, tags, roomName, onhold, queued, units } = this.queryParams; - const { createdAt, customFields, closedAt } = this.queryParams; - - const createdAtParam = validateDateParams('createdAt', createdAt); - const closedAtParam = validateDateParams('closedAt', closedAt); - - const hasAdminAccess = await hasPermissionAsync(this.userId, 'view-livechat-rooms'); - const hasAgentAccess = - (await hasPermissionAsync(this.userId, 'view-l-room')) && agents?.includes(this.userId) && agents?.length === 1; - if (!hasAdminAccess && !hasAgentAccess) { - return API.v1.forbidden(); - } - - let parsedCf: { [key: string]: string } | undefined = undefined; - if (customFields) { - try { - const parsedCustomFields = JSON.parse(customFields) as { [key: string]: string }; - if (typeof parsedCustomFields !== 'object' || Array.isArray(parsedCustomFields) || parsedCustomFields === null) { - throw new Error('Invalid custom fields'); - } - - // Model's already checking for the keys, so we don't need to do it here. - parsedCf = parsedCustomFields; - } catch (e) { - throw new Error('The "customFields" query parameter must be a valid JSON.'); - } - } - - return API.v1.success( - await findRooms({ - agents, - roomName, - departmentId, - ...(isBoolean(open) && { open: open === true || open === 'true' }), - createdAt: createdAtParam, - closedAt: closedAtParam, - tags, - customFields: parsedCf, - onhold, - queued, - units, - query, - options: { offset, count, sort, fields }, - callerId: this.userId, - }), - ); - }, - }, -); - -API.v1.addRoute( - 'livechat/rooms/filters', - { authRequired: true, permissionsRequired: ['view-l-room'] }, - { - async get() { - return API.v1.success({ - filters: (await LivechatRooms.findAvailableSources().toArray())[0].fullTypes, - }); - }, - }, -); diff --git a/apps/meteor/app/livechat/imports/server/rest/triggers.ts b/apps/meteor/app/livechat/imports/server/rest/triggers.ts deleted file mode 100644 index a7660827b0ce1..0000000000000 --- a/apps/meteor/app/livechat/imports/server/rest/triggers.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { LivechatTrigger } from '@rocket.chat/models'; -import { isGETLivechatTriggersParams, isPOSTLivechatTriggersParams } from '@rocket.chat/rest-typings'; - -import { API } from '../../../../api/server'; -import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; -import { findTriggers, findTriggerById, deleteTrigger } from '../../../server/api/lib/triggers'; - -API.v1.addRoute( - 'livechat/triggers', - { - authRequired: true, - permissionsRequired: ['view-livechat-manager'], - validateParams: { - GET: isGETLivechatTriggersParams, - POST: isPOSTLivechatTriggersParams, - }, - }, - { - async get() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - - const triggers = await findTriggers({ - pagination: { - offset, - count, - sort, - }, - }); - - return API.v1.success(triggers); - }, - async post() { - const { _id, name, description, enabled, runOnce, conditions, actions } = this.bodyParams; - - if (_id) { - await LivechatTrigger.updateById(_id, { name, description, enabled, runOnce, conditions, actions }); - } else { - await LivechatTrigger.insertOne({ name, description, enabled, runOnce, conditions, actions }); - } - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'livechat/triggers/:_id', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, - { - async get() { - const trigger = await findTriggerById({ - triggerId: this.urlParams._id, - }); - - return API.v1.success({ - trigger, - }); - }, - async delete() { - await deleteTrigger({ - triggerId: this.urlParams._id, - }); - - return API.v1.success(); - }, - }, -); diff --git a/apps/meteor/app/livechat/imports/server/rest/users.ts b/apps/meteor/app/livechat/imports/server/rest/users.ts deleted file mode 100644 index 53039e8836c4e..0000000000000 --- a/apps/meteor/app/livechat/imports/server/rest/users.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Users } from '@rocket.chat/models'; -import { isLivechatUsersManagerGETProps, isPOSTLivechatUsersTypeProps } from '@rocket.chat/rest-typings'; -import { check } from 'meteor/check'; - -import { API } from '../../../../api/server'; -import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; -import { hasAtLeastOnePermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { findAgents, findManagers } from '../../../server/api/lib/users'; -import { addManager, addAgent, removeAgent, removeManager } from '../../../server/lib/omni-users'; - -const emptyStringArray: string[] = []; - -API.v1.addRoute( - 'livechat/users/:type', - { - authRequired: true, - permissionsRequired: { - 'POST': ['view-livechat-manager'], - '*': emptyStringArray, - }, - validateParams: { - GET: isLivechatUsersManagerGETProps, - POST: isPOSTLivechatUsersTypeProps, - }, - }, - { - async get() { - check(this.urlParams, { - type: String, - }); - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - const { text } = this.queryParams; - - if (this.urlParams.type === 'agent') { - if (!(await hasAtLeastOnePermissionAsync(this.userId, ['transfer-livechat-guest', 'edit-omnichannel-contact']))) { - return API.v1.forbidden(); - } - - const { onlyAvailable, excludeId, showIdleAgents } = this.queryParams; - return API.v1.success( - await findAgents({ - text, - onlyAvailable, - excludeId, - showIdleAgents, - pagination: { - offset, - count, - sort, - }, - }), - ); - } - if (this.urlParams.type === 'manager') { - if (!(await hasAtLeastOnePermissionAsync(this.userId, ['view-livechat-manager']))) { - return API.v1.forbidden(); - } - - return API.v1.success( - await findManagers({ - text, - pagination: { - offset, - count, - sort, - }, - }), - ); - } - throw new Error('Invalid type'); - }, - async post() { - if (this.urlParams.type === 'agent') { - const user = await addAgent(this.bodyParams.username); - if (user) { - return API.v1.success({ user }); - } - } else if (this.urlParams.type === 'manager') { - const user = await addManager(this.bodyParams.username); - if (user) { - return API.v1.success({ user }); - } - } else { - throw new Error('Invalid type'); - } - - return API.v1.failure(); - }, - }, -); - -API.v1.addRoute( - 'livechat/users/:type/:_id', - { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, - { - async get() { - if (!['agent', 'manager'].includes(this.urlParams.type)) { - throw new Error('Invalid type'); - } - const role = this.urlParams.type === 'agent' ? 'livechat-agent' : 'livechat-manager'; - - const user = await Users.findOneByIdAndRole(this.urlParams._id, role, { - projection: { _id: 1, username: 1, name: 1, status: 1, statusLivechat: 1, emails: 1, livechat: 1 }, - }); - - // TODO: throw error instead of returning null - return API.v1.success({ user }); - }, - async delete() { - if (this.urlParams.type === 'agent') { - if (await removeAgent(this.urlParams._id)) { - return API.v1.success(); - } - } else if (this.urlParams.type === 'manager') { - if (await removeManager(this.urlParams._id)) { - return API.v1.success(); - } - } else { - throw new Error('Invalid type'); - } - - return API.v1.failure(); - }, - }, -); diff --git a/apps/meteor/app/livechat/imports/server/rest/visitors.ts b/apps/meteor/app/livechat/imports/server/rest/visitors.ts deleted file mode 100644 index 5504d85b699f8..0000000000000 --- a/apps/meteor/app/livechat/imports/server/rest/visitors.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { Messages, LivechatRooms } from '@rocket.chat/models'; -import { - isLivechatVisitorsInfoProps, - isGETLivechatVisitorsPagesVisitedRoomIdParams, - isGETLivechatVisitorsChatHistoryRoomRoomIdVisitorVisitorIdParams, - isGETLivechatVisitorsSearchChatsRoomRoomIdVisitorVisitorIdParams, - isGETLivechatVisitorsAutocompleteParams, - isLivechatRidMessagesProps, - isGETLivechatVisitorsSearch, -} from '@rocket.chat/rest-typings'; -import { escapeRegExp } from '@rocket.chat/string-helpers'; - -import { API } from '../../../../api/server'; -import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; -import { canAccessRoomAsync } from '../../../../authorization/server'; -import { normalizeMessagesForUser } from '../../../../utils/server/lib/normalizeMessagesForUser'; -import { - findVisitorInfo, - findVisitedPages, - findChatHistory, - searchChats, - findVisitorsToAutocomplete, - findVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField, -} from '../../../server/api/lib/visitors'; - -API.v1.addRoute( - 'livechat/visitors.info', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isLivechatVisitorsInfoProps }, - { - async get() { - const visitor = await findVisitorInfo({ visitorId: this.queryParams.visitorId }); - return API.v1.success(visitor); - }, - }, -); - -API.v1.addRoute( - 'livechat/visitors.pagesVisited/:roomId', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETLivechatVisitorsPagesVisitedRoomIdParams }, - { - async get() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - - const pages = await findVisitedPages({ - roomId: this.urlParams.roomId, - pagination: { - offset, - count, - sort, - }, - }); - return API.v1.success(pages); - }, - }, -); - -API.v1.addRoute( - 'livechat/visitors.chatHistory/room/:roomId/visitor/:visitorId', - { - authRequired: true, - permissionsRequired: ['view-l-room'], - validateParams: isGETLivechatVisitorsChatHistoryRoomRoomIdVisitorVisitorIdParams, - }, - { - async get() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - const history = await findChatHistory({ - userId: this.userId, - roomId: this.urlParams.roomId, - visitorId: this.urlParams.visitorId, - pagination: { - offset, - count, - sort, - }, - }); - - return API.v1.success(history); - }, - }, -); - -API.v1.addRoute( - 'livechat/visitors.searchChats/room/:roomId/visitor/:visitorId', - { - authRequired: true, - permissionsRequired: ['view-l-room'], - validateParams: isGETLivechatVisitorsSearchChatsRoomRoomIdVisitorVisitorIdParams, - }, - { - async get() { - const { roomId, visitorId } = this.urlParams; - const { searchText, closedChatsOnly, servedChatsOnly, source } = this.queryParams; - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - const history = await searchChats({ - userId: this.userId, - roomId, - visitorId, - searchText, - closedChatsOnly, - servedChatsOnly, - source, - pagination: { - offset, - count, - sort, - }, - }); - return API.v1.success(history); - }, - }, -); - -API.v1.addRoute( - 'livechat/visitors.autocomplete', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETLivechatVisitorsAutocompleteParams }, - { - async get() { - const { selector } = this.queryParams; - - return API.v1.success( - await findVisitorsToAutocomplete({ - selector: JSON.parse(selector), - }), - ); - }, - }, -); - -API.v1.addRoute( - 'livechat/visitors.search', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETLivechatVisitorsSearch }, - { - async get() { - const { term } = this.queryParams; - - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - - const nameOrUsername = term ? new RegExp(escapeRegExp(term), 'i') : undefined; - - return API.v1.success( - await findVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField({ - emailOrPhone: term, - nameOrUsername, - pagination: { - offset, - count, - sort, - }, - }), - ); - }, - }, -); - -API.v1.addRoute( - 'livechat/:rid/messages', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isLivechatRidMessagesProps }, - { - async get() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - const { searchTerm } = this.queryParams; - - const room = await LivechatRooms.findOneById(this.urlParams.rid); - - if (!room) { - throw new Error('invalid-room'); - } - - if (!(await canAccessRoomAsync(room, this.user))) { - throw new Error('not-allowed'); - } - - const { cursor, totalCount } = Messages.findLivechatClosedMessages(this.urlParams.rid, searchTerm, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - }); - - const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); - - return API.v1.success({ - messages: await normalizeMessagesForUser(messages, this.userId), - offset, - count, - total, - }); - }, - }, -); diff --git a/apps/meteor/app/livechat/lib/stream/constants.ts b/apps/meteor/app/livechat/lib/stream/constants.ts deleted file mode 100644 index 17c85f566f445..0000000000000 --- a/apps/meteor/app/livechat/lib/stream/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const LIVECHAT_INQUIRY_QUEUE_STREAM_OBSERVER = 'livechat-inquiry-queue-observer'; diff --git a/apps/meteor/app/livechat/server/api.ts b/apps/meteor/app/livechat/server/api.ts deleted file mode 100644 index 9227a85089386..0000000000000 --- a/apps/meteor/app/livechat/server/api.ts +++ /dev/null @@ -1,14 +0,0 @@ -import '../imports/server/rest/agent'; -import '../imports/server/rest/departments'; -import '../imports/server/rest/sms'; -import '../imports/server/rest/users'; -import '../imports/server/rest/upload'; -import '../imports/server/rest/inquiries'; -import '../imports/server/rest/rooms'; -import '../imports/server/rest/appearance'; -import '../imports/server/rest/triggers'; -import '../imports/server/rest/integrations'; -import '../imports/server/rest/visitors'; -import '../imports/server/rest/dashboards'; -import '../imports/server/rest/queue'; -import '../imports/server/rest/businessHours'; diff --git a/apps/meteor/app/livechat/server/api/lib/businessHours.ts b/apps/meteor/app/livechat/server/api/lib/businessHours.ts deleted file mode 100644 index 898e5ed9db3ed..0000000000000 --- a/apps/meteor/app/livechat/server/api/lib/businessHours.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; - -import { businessHourManager } from '../../business-hour'; - -export async function findLivechatBusinessHour(id?: string, type?: string): Promise> { - return { - businessHour: (await businessHourManager.getBusinessHour(id, type)) as ILivechatBusinessHour, - }; -} diff --git a/apps/meteor/app/livechat/server/api/lib/departments.ts b/apps/meteor/app/livechat/server/api/lib/departments.ts deleted file mode 100644 index cf430e3ad8561..0000000000000 --- a/apps/meteor/app/livechat/server/api/lib/departments.ts +++ /dev/null @@ -1,197 +0,0 @@ -import type { ILivechatDepartment, ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; -import { LivechatDepartment, LivechatDepartmentAgents } from '@rocket.chat/models'; -import { applyDepartmentRestrictions } from '@rocket.chat/omni-core'; -import type { PaginatedResult } from '@rocket.chat/rest-typings'; -import { escapeRegExp } from '@rocket.chat/string-helpers'; -import type { Document, Filter, FilterOperators, FindOptions } from 'mongodb'; - -import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; - -type Pagination = { pagination: { offset: number; count: number; sort: FindOptions['sort'] } }; -type FindDepartmentParams = { - userId: string; - onlyMyDepartments?: boolean; - text?: string; - enabled?: boolean; - excludeDepartmentId?: string; - showArchived?: boolean; -} & Pagination; -type FindDepartmentByIdParams = { - userId: string; - departmentId: string; - includeAgents?: boolean; - onlyMyDepartments?: boolean; -}; -type FindDepartmentToAutocompleteParams = { - uid: string; - selector: { - exceptions: string[]; - conditions: Filter; - term: string; - }; - onlyMyDepartments?: boolean; - showArchived?: boolean; -}; -type FindDepartmentAgentsParams = { - userId: string; - departmentId: string; -} & Pagination; - -export async function findDepartments({ - userId, - onlyMyDepartments = false, - text, - enabled, - excludeDepartmentId, - showArchived = false, - pagination: { offset, count, sort }, -}: FindDepartmentParams): Promise> { - let query: FilterOperators = { - $or: [{ type: { $eq: 'd' } }, { type: { $exists: false } }], - ...(!showArchived && { archived: { $ne: !showArchived } }), - ...(enabled && { enabled: Boolean(enabled) }), - ...(text && { name: new RegExp(escapeRegExp(text), 'i') }), - ...(excludeDepartmentId && { _id: { $ne: excludeDepartmentId } }), - }; - - if (onlyMyDepartments) { - query = await applyDepartmentRestrictions(query, userId); - } - - const { cursor, totalCount } = LivechatDepartment.findPaginated(query, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - }); - - const [departments, total] = await Promise.all([cursor.toArray(), totalCount]); - - return { - departments, - count: departments.length, - offset, - total, - }; -} - -export async function findArchivedDepartments({ - userId, - onlyMyDepartments = false, - text, - excludeDepartmentId, - pagination: { offset, count, sort }, -}: FindDepartmentParams): Promise> { - let query: FilterOperators = { - $or: [{ type: { $eq: 'd' } }, { type: { $exists: false } }], - archived: { $eq: true }, - ...(text && { name: new RegExp(escapeRegExp(text), 'i') }), - ...(excludeDepartmentId && { _id: { $ne: excludeDepartmentId } }), - }; - - if (onlyMyDepartments) { - query = await applyDepartmentRestrictions(query, userId); - } - - const { cursor, totalCount } = LivechatDepartment.findPaginated(query, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - }); - - const [departments, total] = await Promise.all([cursor.toArray(), totalCount]); - - return { - departments, - count: departments.length, - offset, - total, - }; -} - -export async function findDepartmentById({ - userId, - departmentId, - includeAgents = true, - onlyMyDepartments = false, -}: FindDepartmentByIdParams): Promise<{ - department: ILivechatDepartment | null; - agents?: ILivechatDepartmentAgents[]; -}> { - const canViewLivechatDepartments = includeAgents && (await hasPermissionAsync(userId, 'view-livechat-departments')); - - let query: FilterOperators = { _id: departmentId }; - - if (onlyMyDepartments) { - query = await applyDepartmentRestrictions(query, userId); - } - - const result = { - department: await LivechatDepartment.findOne(query), - ...(includeAgents && - canViewLivechatDepartments && { - agents: await LivechatDepartmentAgents.findByDepartmentId(departmentId).toArray(), - }), - }; - - return result; -} - -export async function findDepartmentsToAutocomplete({ - uid, - selector, - onlyMyDepartments = false, - showArchived = false, -}: FindDepartmentToAutocompleteParams): Promise<{ items: ILivechatDepartment[] }> { - const { exceptions = [] } = selector; - let { conditions = {} } = selector; - - if (onlyMyDepartments) { - conditions = await applyDepartmentRestrictions(conditions, uid); - } - - const conditionsWithArchived = { archived: { $ne: !showArchived }, ...conditions }; - - const items = await LivechatDepartment.findByNameRegexWithExceptionsAndConditions(selector.term, exceptions, conditionsWithArchived, { - projection: { - _id: 1, - name: 1, - }, - sort: { - name: 1, - }, - }).toArray(); - return { - items, - }; -} - -export async function findDepartmentAgents({ - departmentId, - pagination: { offset, count, sort }, -}: FindDepartmentAgentsParams): Promise> { - const { cursor, totalCount } = LivechatDepartmentAgents.findAgentsByDepartmentId(departmentId, { - sort: sort || { username: 1 }, - skip: offset, - limit: count, - }); - - const [agents, total] = await Promise.all([cursor.toArray(), totalCount]); - - return { - agents, - count: agents.length, - offset, - total, - }; -} - -export async function findDepartmentsBetweenIds({ - ids, - fields, -}: { - ids: string[]; - fields: Record; -}): Promise<{ departments: ILivechatDepartment[] }> { - const departments = await LivechatDepartment.findInIds(ids, { projection: fields }).toArray(); - return { departments }; -} diff --git a/apps/meteor/app/livechat/server/api/lib/inquiries.ts b/apps/meteor/app/livechat/server/api/lib/inquiries.ts deleted file mode 100644 index fb923b1e000b2..0000000000000 --- a/apps/meteor/app/livechat/server/api/lib/inquiries.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { LivechatInquiryStatus } from '@rocket.chat/core-typings'; -import type { ILivechatInquiryRecord, IRoom, IUser } from '@rocket.chat/core-typings'; -import { LivechatDepartmentAgents, LivechatDepartment, LivechatInquiry } from '@rocket.chat/models'; -import type { PaginatedResult } from '@rocket.chat/rest-typings'; -import type { Filter } from 'mongodb'; - -import { getOmniChatSortQuery } from '../../../lib/inquiries'; -import { getInquirySortMechanismSetting } from '../../lib/settings'; - -const agentDepartments = async (userId: IUser['_id']): Promise => { - const agentDepartments = (await LivechatDepartmentAgents.findByAgentId(userId, { projection: { departmentId: 1 } }).toArray()).map( - ({ departmentId }) => departmentId, - ); - return (await LivechatDepartment.findEnabledInIds(agentDepartments, { projection: { _id: 1 } }).toArray()).map(({ _id }) => _id); -}; - -const applyDepartmentRestrictions = async ( - userId: IUser['_id'], - filterDepartment?: string, -): Promise<{ $in: string[] } | { $exists: false } | string> => { - const allowedDepartments = await agentDepartments(userId); - if (allowedDepartments && Array.isArray(allowedDepartments) && allowedDepartments.length > 0) { - if (!filterDepartment) { - return { $in: allowedDepartments }; - } - - if (!allowedDepartments.includes(filterDepartment)) { - throw new Error('error-not-authorized'); - } - return filterDepartment; - } - - return { $exists: false }; -}; - -export async function findInquiries({ - userId, - department: filterDepartment, - status, - pagination: { offset, count, sort }, -}: { - userId: IUser['_id']; - department?: string; - status?: LivechatInquiryStatus; - pagination: { offset: number; count: number; sort: Record }; -}): Promise }>> { - const department = await applyDepartmentRestrictions(userId, filterDepartment); - const defaultSort = getOmniChatSortQuery(getInquirySortMechanismSetting()); - const options = { - limit: count, - skip: offset, - sort: { ...sort, ...defaultSort }, - }; - - const filter: Filter = { - // V in Enum only works for numeric enums - ...(status && Object.values(LivechatInquiryStatus).includes(status) && { status }), - $or: [ - // Cases where this user is the default agent - { - 'defaultAgent': { $exists: true }, - 'defaultAgent.agentId': userId, - }, - // Cases with no default agent assigned yet, AND either: - // - belongs to one of user's departments, or - // - has no department (public queue) - { - defaultAgent: { $exists: false }, - $or: [...(department ? [{ department }] : []), { department: { $exists: false } }], - }, - ], - }; - - const { cursor, totalCount } = LivechatInquiry.findPaginated(filter, options); - - const [inquiries, total] = await Promise.all([cursor.toArray(), totalCount]); - - return { - inquiries, - count: inquiries.length, - offset, - total, - }; -} - -export async function findOneInquiryByRoomId({ roomId }: { roomId: IRoom['_id'] }): Promise<{ inquiry: ILivechatInquiryRecord | null }> { - return { - inquiry: await LivechatInquiry.findOneByRoomId(roomId, {}), - }; -} diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.ts b/apps/meteor/app/livechat/server/api/lib/livechat.ts deleted file mode 100644 index 4390137e95a28..0000000000000 --- a/apps/meteor/app/livechat/server/api/lib/livechat.ts +++ /dev/null @@ -1,196 +0,0 @@ -import type { ILivechatAgent, ILivechatDepartment, ILivechatTrigger, ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings'; -import { License } from '@rocket.chat/license'; -import { EmojiCustom, LivechatTrigger, LivechatVisitors, LivechatRooms, LivechatDepartment } from '@rocket.chat/models'; -import { makeFunction } from '@rocket.chat/patch-injection'; -import { Meteor } from 'meteor/meteor'; - -import { callbacks } from '../../../../../server/lib/callbacks'; -import { normalizeAgent } from '../../lib/Helper'; -import { getInitSettings } from '../../lib/settings'; - -async function findTriggers(): Promise[]> { - const triggers = await LivechatTrigger.findEnabled().toArray(); - const hasLicense = License.hasModule('livechat-enterprise'); - const premiumActions = ['use-external-service']; - - return triggers - .filter(({ actions }) => hasLicense || actions.some((c) => !premiumActions.includes(c.name))) - .map(({ _id, actions, conditions, runOnce }) => ({ - _id, - actions, - conditions, - runOnce, - })); -} - -export type CheckUnitsFromUser = { - userId?: string; - businessUnit?: string; -}; - -export const checkUnitsFromUser = makeFunction(async (_params: CheckUnitsFromUser): Promise => undefined); - -async function findDepartments( - businessUnit?: string, - userId?: string, -): Promise[]> { - // TODO: check this function usage - await checkUnitsFromUser({ userId, businessUnit }); - - return LivechatDepartment.findEnabledWithAgentsAndBusinessUnit< - Pick - >(businessUnit, { - _id: 1, - name: 1, - showOnRegistration: 1, - showOnOfflineForm: 1, - departmentsAllowedToForward: 1, - }).toArray(); -} - -export function findGuest(token: string): Promise { - return LivechatVisitors.getVisitorByToken(token); -} - -export function findGuestWithoutActivity(token: string): Promise { - return LivechatVisitors.getVisitorByToken(token, { projection: { name: 1, username: 1, token: 1, visitorEmails: 1, department: 1 } }); -} - -export async function findRoom(token: string, rid?: string): Promise { - const fields = { - t: 1, - departmentId: 1, - servedBy: 1, - open: 1, - v: 1, - ts: 1, - }; - - if (!rid) { - return LivechatRooms.findOneByVisitorToken(token, fields); - } - - return LivechatRooms.findOneByIdAndVisitorToken(rid, token, fields); -} - -export async function findOpenRoom(token: string, departmentId?: string, callerId?: string): Promise { - const options = { - projection: { - departmentId: 1, - servedBy: 1, - open: 1, - }, - }; - - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: callerId }); - const rooms = departmentId - ? await LivechatRooms.findOpenByVisitorTokenAndDepartmentId(token, departmentId, options, extraQuery).toArray() - : await LivechatRooms.findOpenByVisitorToken(token, options, extraQuery).toArray(); - if (rooms && rooms.length > 0) { - return rooms[0]; - } -} - -export async function findAgent(agentId?: string): Promise { - return normalizeAgent(agentId); -} - -export function normalizeHttpHeaderData(headers: Headers = new Headers()): { - httpHeaders: Record; -} { - const httpHeaders = Object.fromEntries(headers.entries()); - return { httpHeaders }; -} - -export async function settings({ businessUnit = '', userId }: { businessUnit?: string; userId?: string } = {}): Promise< - Record -> { - const [initSettings, triggers, departments, emojis] = await Promise.all([ - getInitSettings(), - findTriggers(), - findDepartments(businessUnit, userId), - EmojiCustom.find().toArray(), - ]); - const sound = `${Meteor.absoluteUrl()}sounds/chime.mp3`; - return { - enabled: initSettings.Livechat_enabled, - settings: { - registrationForm: initSettings.Livechat_registration_form, - allowSwitchingDepartments: initSettings.Livechat_allow_switching_departments, - nameFieldRegistrationForm: initSettings.Livechat_name_field_registration_form, - emailFieldRegistrationForm: initSettings.Livechat_email_field_registration_form, - displayOfflineForm: initSettings.Livechat_display_offline_form, - videoCall: initSettings.Omnichannel_call_provider === 'default-provider', - fileUpload: initSettings.Livechat_fileupload_enabled && initSettings.FileUpload_Enabled, - language: initSettings.Language, - transcript: initSettings.Livechat_enable_transcript, - historyMonitorType: initSettings.Livechat_history_monitor_type, - forceAcceptDataProcessingConsent: initSettings.Livechat_force_accept_data_processing_consent, - showConnecting: initSettings.Livechat_Show_Connecting, - agentHiddenInfo: initSettings.Livechat_show_agent_info === false, - clearLocalStorageWhenChatEnded: initSettings.Livechat_clear_local_storage_when_chat_ended, - limitTextLength: - initSettings.Livechat_enable_message_character_limit && - (initSettings.Livechat_message_character_limit || initSettings.Message_MaxAllowedSize), - hiddenSystemMessages: initSettings.Livechat_hide_system_messages, - livechatLogo: initSettings.Assets_livechat_widget_logo, - hideWatermark: initSettings.Livechat_hide_watermark || false, - visitorsCanCloseChat: initSettings.Omnichannel_allow_visitors_to_close_conversation, - }, - theme: { - title: initSettings.Livechat_title, - color: initSettings.Livechat_title_color, - offlineTitle: initSettings.Livechat_offline_title, - offlineColor: initSettings.Livechat_offline_title_color, - position: initSettings.Livechat_widget_position || 'right', - background: initSettings.Livechat_background, - hideExpandChat: initSettings.Livechat_hide_expand_chat, - actionLinks: { - jitsi: [ - { icon: 'icon-videocam', i18nLabel: 'Accept' }, - { icon: 'icon-cancel', i18nLabel: 'Decline' }, - ], - }, - }, - messages: { - offlineMessage: initSettings.Livechat_offline_message, - offlineSuccessMessage: initSettings.Livechat_offline_success_message, - offlineUnavailableMessage: initSettings.Livechat_offline_form_unavailable, - conversationFinishedMessage: initSettings.Livechat_conversation_finished_message, - conversationFinishedText: initSettings.Livechat_conversation_finished_text, - transcriptMessage: initSettings.Livechat_transcript_message, - registrationFormMessage: initSettings.Livechat_registration_form_message, - dataProcessingConsentText: initSettings.Livechat_data_processing_consent_text, - }, - survey: { - items: ['satisfaction', 'agentKnowledge', 'agentResposiveness', 'agentFriendliness'], - values: ['1', '2', '3', '4', '5'], - }, - triggers, - departments, - resources: { - sound, - emojis, - }, - }; -} - -export const getExtraConfigInfo = makeFunction( - async (options: { - room?: IOmnichannelRoom; - }): Promise<{ - queueInfo?: unknown; - customFields?: { - options?: string[] | undefined; - _id: string; - label: string; - regexp: string | undefined; - required: boolean; - type: string | undefined; - defaultValue: string | null; - }[]; - room?: IOmnichannelRoom; - }> => options, -); - -export const onCheckRoomParams = makeFunction((params: any) => params); diff --git a/apps/meteor/app/livechat/server/api/lib/rooms.ts b/apps/meteor/app/livechat/server/api/lib/rooms.ts deleted file mode 100644 index 5632a56d6411d..0000000000000 --- a/apps/meteor/app/livechat/server/api/lib/rooms.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type { ILivechatDepartment, IOmnichannelRoom } from '@rocket.chat/core-typings'; -import { LivechatRooms, LivechatDepartment } from '@rocket.chat/models'; -import type { PaginatedResult } from '@rocket.chat/rest-typings'; - -import { callbacks } from '../../../../../server/lib/callbacks'; - -export async function findRooms({ - agents, - roomName, - departmentId, - open, - createdAt, - closedAt, - tags, - customFields, - onhold, - queued, - units, - query, - options: { offset, count, fields, sort }, - callerId, -}: { - agents?: Array; - roomName?: string; - departmentId?: string; - open?: boolean; - createdAt?: { - start?: string | undefined; - end?: string | undefined; - }; - closedAt?: { - start?: string | undefined; - end?: string | undefined; - }; - tags?: Array; - customFields?: Record; - onhold?: string | boolean; - queued?: string | boolean; - units?: Array; - query?: Record; - options: { offset: number; count: number; fields: Record; sort: Record }; - callerId: string; -}): Promise }>> { - const extraQueryBase = await callbacks.run('livechat.applyRoomRestrictions', {}, { unitsFilter: units, userId: callerId }); - const extraQuery = { ...extraQueryBase, ...(query || {}) }; - const { cursor, totalCount } = LivechatRooms.findRoomsWithCriteria({ - agents, - roomName, - departmentId, - open, - createdAt, - closedAt, - tags, - customFields, - onhold: ['t', 'true', '1'].includes(`${onhold}`), - queued: ['t', 'true', '1'].includes(`${queued}`), - options: { - sort: sort || { ts: -1 }, - offset, - count, - fields, - }, - extraQuery, - }); - - const [rooms, total] = await Promise.all([cursor.toArray(), totalCount]); - - const isRoomWithDepartmentId = (depId: string | undefined): depId is string => !!depId; - - const departmentsIds = [...new Set(rooms.map((room) => room.departmentId).filter(isRoomWithDepartmentId))]; - if (departmentsIds.length) { - const departments = await LivechatDepartment.findInIds(departmentsIds, { - projection: { name: 1 }, - }).toArray(); - - rooms.forEach((room: IOmnichannelRoom & { department?: ILivechatDepartment }) => { - if (!room.departmentId) { - return; - } - const department = departments.find((dept) => dept._id === room.departmentId); - if (department) { - room.department = department; - } - }); - } - return { - rooms, - count: rooms.length, - offset, - total, - }; -} diff --git a/apps/meteor/app/livechat/server/api/lib/transfer.ts b/apps/meteor/app/livechat/server/api/lib/transfer.ts deleted file mode 100644 index d01d85402cae5..0000000000000 --- a/apps/meteor/app/livechat/server/api/lib/transfer.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { IOmnichannelSystemMessage } from '@rocket.chat/core-typings'; -import { Messages } from '@rocket.chat/models'; -import type { PaginatedResult } from '@rocket.chat/rest-typings'; - -const normalizeTransferHistory = ({ transferData }: IOmnichannelSystemMessage): IOmnichannelSystemMessage['transferData'] => transferData; - -const removeNulls = (value: S | undefined): value is S => value != null; - -export async function findLivechatTransferHistory({ - rid, - pagination: { offset, count, sort }, -}: { - rid: string; - pagination: { offset: number; count: number; sort: Record }; -}): Promise> { - const { cursor, totalCount } = Messages.findPaginated( - { rid, t: 'livechat_transfer_history' }, - { - projection: { transferData: 1 }, - sort: sort || { ts: 1 }, - skip: offset, - limit: count, - }, - ); - - const [history, total] = await Promise.all([cursor.map(normalizeTransferHistory).toArray(), totalCount]); - - return { - history: history.filter(removeNulls), - count: history.length, - offset, - total, - }; -} diff --git a/apps/meteor/app/livechat/server/api/lib/triggers.ts b/apps/meteor/app/livechat/server/api/lib/triggers.ts deleted file mode 100644 index 4cbafcb0dc737..0000000000000 --- a/apps/meteor/app/livechat/server/api/lib/triggers.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { ILivechatTrigger } from '@rocket.chat/core-typings'; -import { LivechatTrigger } from '@rocket.chat/models'; -import type { PaginatedResult } from '@rocket.chat/rest-typings'; - -export async function findTriggers({ - pagination: { offset, count, sort }, -}: { - pagination: { offset: number; count: number; sort: Record }; -}): Promise }>> { - const { cursor, totalCount } = LivechatTrigger.findPaginated( - {}, - { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - }, - ); - - const [triggers, total] = await Promise.all([cursor.toArray(), totalCount]); - - return { - triggers, - count: triggers.length, - offset, - total, - }; -} - -export async function findTriggerById({ triggerId }: { triggerId: string }): Promise { - return LivechatTrigger.findOneById(triggerId); -} - -export async function deleteTrigger({ triggerId }: { triggerId: string }): Promise { - await LivechatTrigger.removeById(triggerId); -} diff --git a/apps/meteor/app/livechat/server/api/lib/visitors.ts b/apps/meteor/app/livechat/server/api/lib/visitors.ts deleted file mode 100644 index c302141aded02..0000000000000 --- a/apps/meteor/app/livechat/server/api/lib/visitors.ts +++ /dev/null @@ -1,216 +0,0 @@ -import type { ILivechatVisitor, IMessage, IOmnichannelRoom, IRoom, IUser, IVisitor } from '@rocket.chat/core-typings'; -import { LivechatVisitors, Messages, LivechatRooms, LivechatCustomField } from '@rocket.chat/models'; -import type { FindOptions } from 'mongodb'; - -import { callbacks } from '../../../../../server/lib/callbacks'; -import { canAccessRoomAsync } from '../../../../authorization/server/functions/canAccessRoom'; - -export async function findVisitorInfo({ visitorId }: { visitorId: IVisitor['_id'] }) { - const visitor = await LivechatVisitors.findOneEnabledById(visitorId, { projection: { token: 0 } }); - if (!visitor) { - throw new Error('visitor-not-found'); - } - - return { - visitor, - }; -} - -export async function findVisitedPages({ - roomId, - pagination: { offset, count, sort }, -}: { - roomId: IRoom['_id']; - pagination: { offset: number; count: number; sort: FindOptions['sort'] }; -}) { - const room = await LivechatRooms.findOneById(roomId); - if (!room) { - throw new Error('invalid-room'); - } - const { cursor, totalCount } = Messages.findPaginatedByRoomIdAndType(room._id, 'livechat_navigation_history', { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - }); - - const [pages, total] = await Promise.all([cursor.toArray(), totalCount]); - - return { - pages, - count: pages.length, - offset, - total, - }; -} - -export async function findChatHistory({ - userId, - roomId, - visitorId, - pagination: { offset, count, sort }, -}: { - userId: IUser['_id']; - roomId: IRoom['_id']; - visitorId: IVisitor['_id']; - pagination: { offset: number; count: number; sort: FindOptions['sort'] }; -}) { - const room = await LivechatRooms.findOneById(roomId); - if (!room) { - throw new Error('invalid-room'); - } - if (!(await canAccessRoomAsync(room, { _id: userId }))) { - throw new Error('error-not-allowed'); - } - - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId }); - const { cursor, totalCount } = LivechatRooms.findPaginatedByVisitorId( - visitorId, - { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - }, - extraQuery, - ); - - const [history, total] = await Promise.all([cursor.toArray(), totalCount]); - - return { - history, - count: history.length, - offset, - total, - }; -} - -export async function searchChats({ - userId, - roomId, - visitorId, - searchText, - closedChatsOnly, - servedChatsOnly: served, - source, - pagination: { offset, count, sort }, -}: { - userId: IUser['_id']; - roomId: IRoom['_id']; - visitorId: IVisitor['_id']; - searchText?: string; - closedChatsOnly?: string; - servedChatsOnly?: string; - source?: string; - pagination: { offset: number; count: number; sort: FindOptions['sort'] }; -}) { - const room = await LivechatRooms.findOneById(roomId); - if (!room) { - throw new Error('invalid-room'); - } - - if (!(await canAccessRoomAsync(room, { _id: userId }))) { - throw new Error('error-not-allowed'); - } - - const options = { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - }; - - const [total] = await LivechatRooms.findRoomsByVisitorIdAndMessageWithCriteria({ - visitorId, - open: closedChatsOnly !== 'true', - served: served === 'true', - searchText, - onlyCount: true, - source, - }).toArray(); - const cursor = await LivechatRooms.findRoomsByVisitorIdAndMessageWithCriteria({ - visitorId, - open: closedChatsOnly !== 'true', - served: served === 'true', - searchText, - options, - source, - }); - - const history = await cursor.toArray(); - - return { - history, - count: history.length, - offset, - total: total?.count ?? 0, - }; -} - -export async function findVisitorsToAutocomplete({ - selector, -}: { - selector: { - exceptions?: ILivechatVisitor['_id'][]; - conditions?: Record; - term: string; - }; -}) { - const { exceptions = [], conditions = {} } = selector; - - const options: FindOptions = { - projection: { - _id: 1, - name: 1, - username: 1, - }, - limit: 10, - sort: { - name: 1, - }, - }; - - const items = await LivechatVisitors.findByNameRegexWithExceptionsAndConditions(selector.term, exceptions, conditions, options).toArray(); - return { - items, - }; -} - -export async function findVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField({ - emailOrPhone, - nameOrUsername, - pagination: { offset, count, sort }, -}: { - emailOrPhone?: string; - nameOrUsername?: RegExp; - pagination: { offset: number; count: number; sort: FindOptions['sort'] }; -}) { - const allowedCF = await LivechatCustomField.findMatchingCustomFields('visitor', true, { projection: { _id: 1 } }) - .map((cf) => cf._id) - .toArray(); - - const { cursor, totalCount } = await LivechatVisitors.findPaginatedVisitorsByEmailOrPhoneOrNameOrUsernameOrCustomField( - emailOrPhone, - nameOrUsername, - allowedCF, - { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - projection: { - username: 1, - name: 1, - phone: 1, - livechatData: 1, - visitorEmails: 1, - lastChat: 1, - }, - }, - ); - - const [visitors, total] = await Promise.all([cursor.toArray(), totalCount]); - - return { - visitors, - count: visitors.length, - offset, - total, - }; -} diff --git a/apps/meteor/app/livechat/server/api/rest.ts b/apps/meteor/app/livechat/server/api/rest.ts deleted file mode 100644 index 0689e22fda5d3..0000000000000 --- a/apps/meteor/app/livechat/server/api/rest.ts +++ /dev/null @@ -1,14 +0,0 @@ -import './v1/config'; -import './v1/visitor'; -import './v1/transcript'; -import './v1/offlineMessage'; -import './v1/pageVisited'; -import './v1/agent'; -import './v1/message'; -import './v1/customField'; -import './v1/room'; -import './v1/transfer'; -import './v1/contact'; -import './v1/webhooks'; -import './v1/integration'; -import './v1/statistics'; diff --git a/apps/meteor/app/livechat/server/api/v1/agent.ts b/apps/meteor/app/livechat/server/api/v1/agent.ts deleted file mode 100644 index 3c271e832b07f..0000000000000 --- a/apps/meteor/app/livechat/server/api/v1/agent.ts +++ /dev/null @@ -1,170 +0,0 @@ -import type { ILivechatAgent } from '@rocket.chat/core-typings'; -import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; -import { - isGETAgentNextToken, - isPOSTLivechatAgentSaveInfoParams, - isPOSTLivechatAgentStatusProps, - POSTLivechatAgentSaveInfoSuccessResponse, - validateBadRequestErrorResponse, - validateForbiddenErrorResponse, - validateUnauthorizedErrorResponse, -} from '@rocket.chat/rest-typings'; - -import { API } from '../../../../api/server'; -import type { ExtractRoutesFromAPI } from '../../../../api/server/ApiClass'; -import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { hasRoleAsync } from '../../../../authorization/server/functions/hasRole'; -import { RoutingManager } from '../../lib/RoutingManager'; -import { getRequiredDepartment } from '../../lib/departmentsLib'; -import { saveAgentInfo } from '../../lib/omni-users'; -import { setUserStatusLivechat, allowAgentChangeServiceStatus } from '../../lib/utils'; -import { findRoom, findGuest, findAgent, findOpenRoom } from '../lib/livechat'; - -API.v1.addRoute('livechat/agent.info/:rid/:token', { - async get() { - const visitor = await findGuest(this.urlParams.token); - if (!visitor) { - throw new Error('invalid-token'); - } - - const room = await findRoom(this.urlParams.token, this.urlParams.rid); - if (!room) { - throw new Error('invalid-room'); - } - - const agent = room?.servedBy && (await findAgent(room.servedBy._id)); - if (!agent) { - throw new Error('invalid-agent'); - } - - return API.v1.success({ agent }); - }, -}); - -API.v1.addRoute( - 'livechat/agent.next/:token', - { validateParams: isGETAgentNextToken }, - { - async get() { - const { token } = this.urlParams; - const room = await findOpenRoom(token, undefined, this.userId); - if (room) { - return API.v1.success(); - } - - let { department } = this.queryParams; - if (!department) { - const requireDepartment = await getRequiredDepartment(); - if (requireDepartment) { - department = requireDepartment._id; - } - } - - const agentData = await RoutingManager.getNextAgent(department); - if (!agentData) { - throw new Error('agent-not-found'); - } - - const agent = await findAgent(agentData.agentId); - if (!agent) { - throw new Error('invalid-agent'); - } - - return API.v1.success({ agent }); - }, - }, -); - -API.v1.addRoute( - 'livechat/agent.status', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isPOSTLivechatAgentStatusProps }, - { - async post() { - const { status, agentId: inputAgentId } = this.bodyParams; - - const agentId = inputAgentId || this.userId; - - const agent = await Users.findOneAgentById>(agentId, { - projection: { - status: 1, - statusLivechat: 1, - active: 1, - }, - }); - if (!agent) { - return API.v1.notFound('Agent not found'); - } - - if (!agent.active) { - return API.v1.failure('error-user-deactivated'); - } - - const newStatus: ILivechatAgentStatus = - status || - (agent.statusLivechat === ILivechatAgentStatus.AVAILABLE ? ILivechatAgentStatus.NOT_AVAILABLE : ILivechatAgentStatus.AVAILABLE); - if (newStatus === agent.statusLivechat) { - return API.v1.success({ status: agent.statusLivechat }); - } - - const canChangeStatus = await allowAgentChangeServiceStatus(newStatus, agentId); - - if (agentId !== this.userId) { - if (!(await hasPermissionAsync(this.userId, 'manage-livechat-agents'))) { - return API.v1.forbidden(); - } - - // Silent fail for admins when BH is closed - // Next version we'll update this to return an error - // And update the FE accordingly - if (canChangeStatus) { - await setUserStatusLivechat(agentId, newStatus); - return API.v1.success({ status: newStatus }); - } - - return API.v1.success({ status: agent.statusLivechat }); - } - - if (!canChangeStatus) { - return API.v1.failure('error-business-hours-are-closed'); - } - - await setUserStatusLivechat(agentId, newStatus); - - return API.v1.success({ status: newStatus }); - }, - }, -); - -const livechatAgentsEndpoints = API.v1.post( - 'livechat/agents.saveInfo', - { - response: { - 200: POSTLivechatAgentSaveInfoSuccessResponse, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - authRequired: true, - permissionsRequired: ['manage-livechat-agents'], - body: isPOSTLivechatAgentSaveInfoParams, - }, - async function action() { - const { agentId, agentData, agentDepartments } = this.bodyParams; - - if (!(await hasRoleAsync(agentId, 'livechat-agent'))) { - return API.v1.failure('error-user-is-not-agent'); - } - - await saveAgentInfo(agentId, agentData, agentDepartments); - - return API.v1.success(); - }, -); - -type LivechatAgentsEndpoints = ExtractRoutesFromAPI; - -declare module '@rocket.chat/rest-typings' { - // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface - interface Endpoints extends LivechatAgentsEndpoints {} -} diff --git a/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts b/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts deleted file mode 100644 index f6fb29d174423..0000000000000 --- a/apps/meteor/app/livechat/server/api/v1/offlineMessage.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { isPOSTLivechatOfflineMessageParams } from '@rocket.chat/rest-typings'; - -import { i18n } from '../../../../../server/lib/i18n'; -import { API } from '../../../../api/server'; -import { sendOfflineMessage } from '../../lib/messages'; - -API.v1.addRoute( - 'livechat/offline.message', - { - validateParams: isPOSTLivechatOfflineMessageParams, - rateLimiterOptions: { numRequestsAllowed: 1, intervalTimeInMS: 5000 }, - }, - { - async post() { - const { name, email, message, department, host } = this.bodyParams; - try { - await sendOfflineMessage({ name, email, message, department, host }); - return API.v1.success({ message: i18n.t('Livechat_offline_message_sent') }); - } catch (e) { - return API.v1.failure(i18n.t('Error_sending_livechat_offline_message')); - } - }, - }, -); diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts deleted file mode 100644 index 146c119dee02f..0000000000000 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ /dev/null @@ -1,541 +0,0 @@ -import { Omnichannel } from '@rocket.chat/core-services'; -import type { - ILivechatAgent, - IOmnichannelInquiryExtraData, - IOmnichannelRoom, - IUser, - SelectedAgent, - TransferByData, - TransferData, -} from '@rocket.chat/core-typings'; -import { isOmnichannelRoom, OmnichannelSourceType } from '@rocket.chat/core-typings'; -import { LivechatVisitors, Users, LivechatRooms } from '@rocket.chat/models'; -import { - isLiveChatRoomForwardProps, - isPOSTLivechatRoomCloseParams, - isPOSTLivechatRoomSurveyParams, - isLiveChatRoomJoinProps, - isLiveChatRoomSaveInfoProps, - isPOSTLivechatRoomCloseByUserParams, - isPOSTLivechatRoomsCloseAll, - isPOSTLivechatRoomsCloseAllSuccessResponse, - POSTLivechatRemoveRoomSuccess, - isPOSTLivechatRemoveRoomParams, - validateBadRequestErrorResponse, - validateUnauthorizedErrorResponse, - validateForbiddenErrorResponse, - ajv, -} from '@rocket.chat/rest-typings'; -import { isPOSTLivechatVisitorDepartmentTransferParams } from '@rocket.chat/rest-typings/src/v1/omnichannel'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { callbacks } from '../../../../../server/lib/callbacks'; -import { i18n } from '../../../../../server/lib/i18n'; -import { API } from '../../../../api/server'; -import type { ExtractRoutesFromAPI } from '../../../../api/server/ApiClass'; -import { isWidget } from '../../../../api/server/helpers/isWidget'; -import { canAccessRoomAsync } from '../../../../authorization/server'; -import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { addUserToRoom } from '../../../../lib/server/functions/addUserToRoom'; -import { closeLivechatRoom } from '../../../../lib/server/functions/closeLivechatRoom'; -import { settings as rcSettings } from '../../../../settings/server'; -import { normalizeTransferredByData } from '../../lib/Helper'; -import { closeRoom } from '../../lib/closeRoom'; -import { saveGuest } from '../../lib/guests'; -import type { CloseRoomParams } from '../../lib/localTypes'; -import { livechatLogger } from '../../lib/logger'; -import { createRoom, removeOmnichannelRoom, saveRoomInfo } from '../../lib/rooms'; -import { transfer } from '../../lib/transfer'; -import { findGuest, findRoom, settings, findAgent, onCheckRoomParams } from '../lib/livechat'; - -const isAgentWithInfo = (agentObj: ILivechatAgent | { hiddenInfo: boolean }): agentObj is ILivechatAgent => !('hiddenInfo' in agentObj); - -API.v1.addRoute( - 'livechat/room', - { - rateLimiterOptions: { - numRequestsAllowed: 5, - intervalTimeInMS: 60000, - }, - }, - { - async get() { - // I'll temporary use check for validation, as validateParams doesnt support what's being done here - const extraCheckParams = onCheckRoomParams({ - token: String, - rid: Match.Maybe(String), - agentId: Match.Maybe(String), - }); - - check(this.queryParams, extraCheckParams); - - const { token, rid, agentId, ...extraParams } = this.queryParams; - - const guest = token && (await findGuest(token)); - if (!guest) { - throw new Error('invalid-token'); - } - - if (!rid) { - const room = await LivechatRooms.findOneOpenByVisitorToken(token, {}); - if (room) { - return API.v1.success({ room, newRoom: false }); - } - - let agent: SelectedAgent | undefined; - const agentObj = agentId && (await findAgent(agentId)); - if (agentObj) { - if (isAgentWithInfo(agentObj)) { - const { username = undefined } = agentObj; - agent = { agentId, username }; - } else { - agent = { agentId }; - } - } - - const roomInfo = { - source: { - ...(isWidget(this.request.headers) - ? { type: OmnichannelSourceType.WIDGET, destination: this.request.headers.get('host')! } - : { type: OmnichannelSourceType.API }), - }, - }; - - const newRoom = await createRoom({ - visitor: guest, - roomInfo, - agent, - extraData: extraParams as IOmnichannelInquiryExtraData, - }); - - return API.v1.success({ - room: newRoom, - newRoom: true, - }); - } - - const froom = await LivechatRooms.findOneOpenByRoomIdAndVisitorToken(rid, token, {}); - if (!froom) { - throw new Error('invalid-room'); - } - - return API.v1.success({ room: froom, newRoom: false }); - }, - }, -); - -// Note: use this route if a visitor is closing a room -// If a RC user(like eg agent) is closing a room, use the `livechat/room.closeByUser` route -API.v1.addRoute( - 'livechat/room.close', - { validateParams: isPOSTLivechatRoomCloseParams }, - { - async post() { - const { rid, token } = this.bodyParams; - - if (!rcSettings.get('Omnichannel_allow_visitors_to_close_conversation')) { - throw new Error('error-not-allowed-to-close-conversation'); - } - - const visitor = await findGuest(token); - if (!visitor) { - throw new Error('invalid-token'); - } - - const room = await findRoom(token, rid); - if (!room) { - throw new Error('invalid-room'); - } - - if (!room.open) { - throw new Error('room-closed'); - } - - const language = rcSettings.get('Language') || 'en'; - const comment = i18n.t('Closed_by_visitor', { lng: language }); - - const options: CloseRoomParams['options'] = {}; - if (room.servedBy) { - const servingAgent: Pick | null = - await Users.findOneById(room.servedBy._id, { - projection: { - name: 1, - username: 1, - utcOffset: 1, - settings: 1, - language: 1, - }, - }); - - if (servingAgent?.settings?.preferences?.omnichannelTranscriptPDF) { - options.pdfTranscript = { - requestedBy: servingAgent._id, - }; - } - - // We'll send the transcript by email only if the setting is disabled (that means, we're not asking the user if he wants to receive the transcript by email) - // And the agent has the preference enabled to send the transcript by email and the visitor has an email address - // When Livechat_enable_transcript is enabled, the email will be sent via livechat/transcript route - if ( - !rcSettings.get('Livechat_enable_transcript') && - servingAgent?.settings?.preferences?.omnichannelTranscriptEmail && - visitor.visitorEmails?.length && - visitor.visitorEmails?.[0]?.address - ) { - const visitorEmail = visitor.visitorEmails?.[0]?.address; - - const language = servingAgent.language || rcSettings.get('Language') || 'en'; - const t = i18n.getFixedT(language); - const subject = t('Transcript_of_your_livechat_conversation'); - - options.emailTranscript = { - sendToVisitor: true, - requestData: { - email: visitorEmail, - requestedAt: new Date(), - requestedBy: servingAgent, - subject, - }, - }; - } - } - - await closeRoom({ visitor, room, comment, options }); - - return API.v1.success({ rid, comment }); - }, - }, -); - -API.v1.addRoute( - 'livechat/room.closeByUser', - { - validateParams: isPOSTLivechatRoomCloseByUserParams, - authRequired: true, - permissionsRequired: ['close-livechat-room'], - }, - { - async post() { - const { rid, comment, tags, generateTranscriptPdf, transcriptEmail, forceClose } = this.bodyParams; - - const allowForceClose = rcSettings.get('Omnichannel_allow_force_close_conversations'); - const isForceClosing = allowForceClose && forceClose; - - if (isForceClosing) { - livechatLogger.warn({ msg: 'Force closing a conversation', user: this.userId, room: rid }); - } - - await closeLivechatRoom(this.user, rid, { - comment, - tags, - generateTranscriptPdf, - transcriptEmail, - forceClose: isForceClosing, - }); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'livechat/room.survey', - { validateParams: isPOSTLivechatRoomSurveyParams }, - { - async post() { - const { rid, token, data } = this.bodyParams; - - const visitor = await findGuest(token); - if (!visitor) { - throw new Error('invalid-token'); - } - - const room = await findRoom(token, rid); - if (!room) { - throw new Error('invalid-room'); - } - - const config = await settings(); - if (!config.survey?.items || !config.survey.values) { - throw new Error('invalid-livechat-config'); - } - - const updateData: { [k: string]: string } = {}; - for (const item of data) { - if ((config.survey.items.includes(item.name) && config.survey.values.includes(item.value)) || item.name === 'additionalFeedback') { - updateData[item.name] = item.value; - } - } - - if (Object.keys(updateData).length === 0) { - throw new Error('invalid-data'); - } - - if (!(await LivechatRooms.updateSurveyFeedbackById(room._id, updateData))) { - return API.v1.failure(); - } - - return API.v1.success({ rid, data: updateData }); - }, - }, -); - -API.v1.addRoute( - 'livechat/room.forward', - { authRequired: true, permissionsRequired: ['view-l-room', 'transfer-livechat-guest'], validateParams: isLiveChatRoomForwardProps }, - { - async post() { - const transferData = this.bodyParams as typeof this.bodyParams & { - transferredBy: TransferByData; - transferredTo?: { _id: string; username?: string; name?: string }; - }; - - const room = await LivechatRooms.findOneById(this.bodyParams.roomId); - if (room?.t !== 'l') { - throw new Error('error-invalid-room'); - } - - if (!room.open) { - throw new Error('This_conversation_is_already_closed'); - } - - if (!(await Omnichannel.isWithinMACLimit(room))) { - throw new Error('error-mac-limit-reached'); - } - - const guest = await LivechatVisitors.findOneEnabledById(room.v?._id); - if (!guest) { - throw new Error('error-invalid-visitor'); - } - - transferData.transferredBy = normalizeTransferredByData(this.user, room); - if (transferData.userId) { - const userToTransfer = await Users.findOneById(transferData.userId); - if (userToTransfer) { - transferData.transferredTo = { - _id: userToTransfer._id, - username: userToTransfer.username, - name: userToTransfer.name, - }; - } - } - - const chatForwardedResult = await transfer(room, guest, transferData); - if (!chatForwardedResult) { - throw new Error('error-forwarding-chat'); - } - - return API.v1.success(); - }, - }, -); - -const livechatVisitorDepartmentTransfer = API.v1.post( - 'livechat/visitor/department.transfer', - { - response: { - 200: ajv.compile({ - type: 'object', - properties: { - success: { - type: 'boolean', - enum: [true], - }, - }, - required: ['success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - }, - body: isPOSTLivechatVisitorDepartmentTransferParams, - }, - async function action() { - const { rid, token, department } = this.bodyParams; - - const visitor = await findGuest(token); - if (!visitor) { - return API.v1.failure('invalid-token'); - } - const room = await LivechatRooms.findOneById(rid); - - if (room?.t !== 'l') { - return API.v1.failure('error-invalid-room'); - } - - if (!room.open) { - return API.v1.failure('This_conversation_is_already_closed'); - } - - // As this is a visitor endpoint, we should not show the mac limit error - if (!(await Omnichannel.isWithinMACLimit(room))) { - return API.v1.failure('error-transefing-chat'); - } - - const guest = await LivechatVisitors.findOneEnabledById(room.v?._id); - if (!guest) { - return API.v1.failure('error-invalid-visitor'); - } - - const transferredBy = normalizeTransferredByData( - { _id: guest._id, username: guest.username, name: guest.name, userType: 'visitor' }, - room, - ); - - const transferData: TransferData = { transferredBy, departmentId: department }; - - const chatForwardedResult = await transfer(room, guest, transferData); - if (!chatForwardedResult) { - return API.v1.failure('error-transfering-chat'); - } - - return API.v1.success(); - }, -); - -type LivechatAnalyticsEndpoints = ExtractRoutesFromAPI; -declare module '@rocket.chat/rest-typings' { - // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface - interface Endpoints extends LivechatAnalyticsEndpoints {} -} - -API.v1.addRoute( - 'livechat/room.join', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isLiveChatRoomJoinProps }, - { - async get() { - const { roomId } = this.queryParams; - - const { user } = this; - - if (!user) { - throw new Error('error-invalid-user'); - } - - const room = await LivechatRooms.findOneById(roomId); - - if (!room) { - throw new Error('error-invalid-room'); - } - - if (!room.open) { - throw new Error('room-closed'); - } - - if (!(await Omnichannel.isWithinMACLimit(room))) { - throw new Error('error-mac-limit-reached'); - } - - if (!(await canAccessRoomAsync(room, user))) { - throw new Error('error-not-allowed'); - } - - await addUserToRoom(roomId, user); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'livechat/room.saveInfo', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isLiveChatRoomSaveInfoProps }, - { - async post() { - const { roomData, guestData } = this.bodyParams; - const room = await LivechatRooms.findOneById(roomData._id); - if (!room || !isOmnichannelRoom(room)) { - throw new Error('error-invalid-room'); - } - - if ( - (!room.servedBy || room.servedBy._id !== this.userId) && - !(await hasPermissionAsync(this.userId, 'save-others-livechat-room-info')) - ) { - return API.v1.forbidden(); - } - - if (room.sms) { - delete guestData.phone; - } - - // We want this both operations to be concurrent, so we have to go with Promise.allSettled - const result = await Promise.allSettled([saveGuest(guestData, this.userId), saveRoomInfo(roomData)]); - - const firstError = result.find((item) => item.status === 'rejected'); - if (firstError) { - throw new Error(firstError.reason.error); - } - - await callbacks.run('livechat.saveInfo', await LivechatRooms.findOneById(roomData._id), { - user: this.user, - oldRoom: room, - }); - - return API.v1.success(); - }, - }, -); - -const livechatRoomsEndpoints = API.v1 - .post( - 'livechat/rooms.delete', - { - response: { - 200: POSTLivechatRemoveRoomSuccess, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - }, - authRequired: true, - permissionsRequired: ['remove-closed-livechat-room'], - body: isPOSTLivechatRemoveRoomParams, - }, - async function action() { - const { roomId } = this.bodyParams; - - try { - await removeOmnichannelRoom(roomId); - return API.v1.success(); - } catch (error: unknown) { - if (error instanceof Meteor.Error) { - return API.v1.failure(error.reason); - } - - return API.v1.failure('error-removing-room'); - } - }, - ) - .post( - 'livechat/rooms.removeAllClosedRooms', - { - response: { - 200: isPOSTLivechatRoomsCloseAllSuccessResponse, - }, - authRequired: true, - permissionsRequired: ['remove-closed-livechat-rooms'], - body: isPOSTLivechatRoomsCloseAll, - }, - async function action() { - livechatLogger.info({ msg: 'User is removing all closed rooms', userId: this.userId }); - - const params = this.bodyParams; - - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: this.userId }); - const promises: Promise[] = []; - await LivechatRooms.findClosedRooms(params?.departmentIds, {}, extraQuery).forEach(({ _id }: IOmnichannelRoom) => { - promises.push(removeOmnichannelRoom(_id)); - }); - await Promise.all(promises); - - livechatLogger.info({ msg: 'User removed closed rooms', userId: this.userId, removedRooms: promises.length }); - return API.v1.success({ removedRooms: promises.length }); - }, - ); - -type LivechatRoomsEndpoints = ExtractRoutesFromAPI; - -declare module '@rocket.chat/rest-typings' { - // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface - interface Endpoints extends LivechatRoomsEndpoints {} -} diff --git a/apps/meteor/app/livechat/server/api/v1/statistics.ts b/apps/meteor/app/livechat/server/api/v1/statistics.ts deleted file mode 100644 index 8289e01e21bb0..0000000000000 --- a/apps/meteor/app/livechat/server/api/v1/statistics.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Users } from '@rocket.chat/models'; -import { isLivechatAnalyticsAgentOverviewProps, isLivechatAnalyticsOverviewProps } from '@rocket.chat/rest-typings'; - -import { API } from '../../../../api/server'; -import { settings } from '../../../../settings/server'; -import { getAgentOverviewDataCached, getAnalyticsOverviewDataCached } from '../../lib/AnalyticsTyped'; - -API.v1.addRoute( - 'livechat/analytics/agent-overview', - { - authRequired: true, - permissionsRequired: ['view-livechat-manager'], - validateParams: isLivechatAnalyticsAgentOverviewProps, - }, - { - async get() { - const { name, departmentId, from, to } = this.queryParams; - - if (!name) { - throw new Error('invalid-chart-name'); - } - - const user = await Users.findOneById(this.userId, { projection: { _id: 1, utcOffset: 1 } }); - return API.v1.success( - await getAgentOverviewDataCached({ - departmentId, - utcOffset: user?.utcOffset || 0, - daterange: { from, to }, - chartOptions: { name }, - executedBy: this.userId, - }), - ); - }, - }, -); - -API.v1.addRoute( - 'livechat/analytics/overview', - { - authRequired: true, - permissionsRequired: ['view-livechat-manager'], - validateParams: isLivechatAnalyticsOverviewProps, - }, - { - async get() { - const { name, departmentId, from, to } = this.queryParams; - - if (!name) { - throw new Error('invalid-chart-name'); - } - - const user = await Users.findOneById(this.userId, { projection: { _id: 1, utcOffset: 1 } }); - const language = user?.language || settings.get('Language') || 'en'; - - return API.v1.success( - await getAnalyticsOverviewDataCached({ - departmentId, - utcOffset: user?.utcOffset || 0, - daterange: { from, to }, - analyticsOptions: { name }, - language, - executedBy: this.userId, - }), - ); - }, - }, -); diff --git a/apps/meteor/app/livechat/server/api/v1/transcript.ts b/apps/meteor/app/livechat/server/api/v1/transcript.ts deleted file mode 100644 index dee730df84636..0000000000000 --- a/apps/meteor/app/livechat/server/api/v1/transcript.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Omnichannel } from '@rocket.chat/core-services'; -import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; -import { LivechatRooms, Users } from '@rocket.chat/models'; -import { isPOSTLivechatTranscriptParams, isPOSTLivechatTranscriptRequestParams } from '@rocket.chat/rest-typings'; - -import { i18n } from '../../../../../server/lib/i18n'; -import { API } from '../../../../api/server'; -import { sendTranscript, requestTranscript } from '../../lib/sendTranscript'; - -API.v1.addRoute( - 'livechat/transcript', - { validateParams: isPOSTLivechatTranscriptParams }, - { - async post() { - const { token, rid, email } = this.bodyParams; - if (!(await sendTranscript({ token, rid, email }))) { - return API.v1.failure({ message: i18n.t('Error_sending_livechat_transcript') }); - } - - return API.v1.success({ message: i18n.t('Livechat_transcript_sent') }); - }, - }, -); - -API.v1.addRoute( - 'livechat/transcript/:rid', - { - authRequired: true, - permissionsRequired: ['send-omnichannel-chat-transcript'], - validateParams: { - POST: isPOSTLivechatTranscriptRequestParams, - }, - }, - { - async delete() { - const { rid } = this.urlParams; - const room = await LivechatRooms.findOneById>(rid, { - projection: { open: 1, transcriptRequest: 1, v: 1 }, - }); - - if (!room?.open) { - throw new Error('error-invalid-room'); - } - if (!room.transcriptRequest) { - throw new Error('error-transcript-not-requested'); - } - - if (!(await Omnichannel.isWithinMACLimit(room))) { - throw new Error('error-mac-limit-reached'); - } - - await LivechatRooms.unsetEmailTranscriptRequestedByRoomId(rid); - - return API.v1.success(); - }, - async post() { - const { rid } = this.urlParams; - const { email, subject } = this.bodyParams; - - const user = await Users.findOneById(this.userId, { - projection: { _id: 1, username: 1, name: 1, utcOffset: 1 }, - }); - - if (!user) { - throw new Error('error-invalid-user'); - } - - await requestTranscript({ rid, email, subject, user }); - - return API.v1.success(); - }, - }, -); diff --git a/apps/meteor/app/livechat/server/api/v1/transfer.ts b/apps/meteor/app/livechat/server/api/v1/transfer.ts deleted file mode 100644 index 8d1b33fdcc3d0..0000000000000 --- a/apps/meteor/app/livechat/server/api/v1/transfer.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { LivechatRooms } from '@rocket.chat/models'; - -import { API } from '../../../../api/server'; -import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; -import { findLivechatTransferHistory } from '../lib/transfer'; - -API.v1.addRoute( - 'livechat/transfer.history/:rid', - { authRequired: true, permissionsRequired: ['view-livechat-rooms'] }, - { - async get() { - const { rid } = this.urlParams; - - const room = await LivechatRooms.findOneById(rid, { projection: { _id: 1 } }); - if (!room) { - throw new Error('invalid-room'); - } - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort } = await this.parseJsonQuery(); - - const history = await findLivechatTransferHistory({ - rid, - pagination: { - offset, - count, - sort, - }, - }); - - return API.v1.success(history); - }, - }, -); diff --git a/apps/meteor/app/livechat/server/api/v1/webhooks.ts b/apps/meteor/app/livechat/server/api/v1/webhooks.ts deleted file mode 100644 index 276a910502d69..0000000000000 --- a/apps/meteor/app/livechat/server/api/v1/webhooks.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Logger } from '@rocket.chat/logger'; -import type { ExtendedFetchOptions } from '@rocket.chat/server-fetch'; -import { serverFetch as fetch } from '@rocket.chat/server-fetch'; - -import { API } from '../../../../api/server'; -import { settings } from '../../../../settings/server'; - -const logger = new Logger('WebhookTest'); - -API.v1.addRoute( - 'livechat/webhook.test', - { authRequired: true, permissionsRequired: ['view-livechat-webhooks'] }, - { - async post() { - const sampleData = { - type: 'LivechatSession', - _id: 'fasd6f5a4sd6f8a4sdf', - label: 'title', - topic: 'asiodojf', - createdAt: new Date(), - lastMessageAt: new Date(), - tags: ['tag1', 'tag2', 'tag3'], - customFields: { - productId: '123456', - }, - visitor: { - _id: '', - name: 'visitor name', - username: 'visitor-username', - department: 'department', - email: 'email@address.com', - phone: '192873192873', - ip: '123.456.7.89', - browser: 'Chrome', - os: 'Linux', - customFields: { - customerId: '123456', - }, - }, - agent: { - _id: 'asdf89as6df8', - username: 'agent.username', - name: 'Agent Name', - email: 'agent@email.com', - }, - messages: [ - { - username: 'visitor-username', - msg: 'message content', - ts: new Date(), - }, - { - username: 'agent.username', - agentId: 'asdf89as6df8', - msg: 'message content from agent', - ts: new Date(), - }, - ], - }; - const options = { - method: 'POST', - headers: { - 'X-RocketChat-Livechat-Token': settings.get('Livechat_secret_token'), - 'Accept': 'application/json', - }, - body: sampleData, - // SECURITY: Webhooks can only be configured by users with enough privileges. It's ok to disable this check here. - ignoreSsrfValidation: true, - size: 10 * 1024 * 1024, - } as ExtendedFetchOptions; - - const webhookUrl = settings.get('Livechat_webhookUrl'); - - if (!webhookUrl) { - return API.v1.failure('Webhook_URL_not_set'); - } - - try { - logger.debug({ msg: 'Testing webhook', webhookUrl }); - const request = await fetch(webhookUrl, options); - const response = await request.text(); - - logger.debug({ msg: 'Webhook response', response }); - if (request.status === 200) { - return API.v1.success(); - } - - throw new Error('Invalid status code'); - } catch (error) { - logger.error({ msg: 'Error testing webhook', err: error }); - throw new Error('error-invalid-webhook-response'); - } - }, - }, -); diff --git a/apps/meteor/app/livechat/server/business-hour/Helper.ts b/apps/meteor/app/livechat/server/business-hour/Helper.ts deleted file mode 100644 index b18863ead06aa..0000000000000 --- a/apps/meteor/app/livechat/server/business-hour/Helper.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; -import { ILivechatAgentStatus, LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; -import { LivechatBusinessHours, Users } from '@rocket.chat/models'; -import moment from 'moment'; - -import { createDefaultBusinessHourRow } from './LivechatBusinessHours'; -import { filterBusinessHoursThatMustBeOpened } from './filterBusinessHoursThatMustBeOpened'; -import { notifyOnUserChangeAsync } from '../../../lib/server/lib/notifyListener'; -import { businessHourLogger } from '../lib/logger'; - -export { filterBusinessHoursThatMustBeOpened }; - -export const filterBusinessHoursThatMustBeOpenedByDay = async ( - businessHours: ILivechatBusinessHour[], - day: string, // Format: moment.format('dddd') -): Promise[]> => { - return filterBusinessHoursThatMustBeOpened( - businessHours.filter((businessHour) => - businessHour.workHours.some((workHour) => workHour.start.utc.dayOfWeek === day || workHour.finish.utc.dayOfWeek === day), - ), - ); -}; - -export const openBusinessHourDefault = async (): Promise => { - try { - await Users.removeBusinessHoursFromAllUsers(); - const currentTime = moment(moment().format('dddd:HH:mm'), 'dddd:HH:mm'); - const day = currentTime.format('dddd'); - const activeBusinessHours = await LivechatBusinessHours.findDefaultActiveAndOpenBusinessHoursByDay(day, { - projection: { - workHours: 1, - timezone: 1, - type: 1, - active: 1, - }, - }); - - const businessHoursToOpenIds = (await filterBusinessHoursThatMustBeOpened(activeBusinessHours)).map((businessHour) => businessHour._id); - businessHourLogger.debug({ msg: 'Opening default business hours', businessHoursToOpenIds }); - await Users.openAgentsBusinessHoursByBusinessHourId(businessHoursToOpenIds); - if (businessHoursToOpenIds.length) { - await makeOnlineAgentsAvailable(); - } - await makeAgentsUnavailableBasedOnBusinessHour(); - } catch (err) { - businessHourLogger.error({ msg: 'Error while opening default business hours', err }); - } -}; - -export const createDefaultBusinessHourIfNotExists = async (): Promise => { - if ((await LivechatBusinessHours.countDocuments({ type: LivechatBusinessHourTypes.DEFAULT })) === 0) { - await LivechatBusinessHours.insertOne(createDefaultBusinessHourRow()); - } -}; - -export async function makeAgentsUnavailableBasedOnBusinessHour(agentIds?: string[]) { - const results = await Users.findAgentsAvailableWithoutBusinessHours(agentIds).toArray(); - - const update = await Users.updateLivechatStatusByAgentIds( - results.map(({ _id }) => _id), - ILivechatAgentStatus.NOT_AVAILABLE, - ); - - if (update.modifiedCount === 0) { - return; - } - - void notifyOnUserChangeAsync(async () => - results.map(({ _id, openBusinessHours }) => { - return { - id: _id, - clientAction: 'updated', - diff: { - statusLivechat: 'not-available', - openBusinessHours, - }, - }; - }), - ); -} - -export async function makeOnlineAgentsAvailable(agentIds?: string[]) { - const results = await Users.findOnlineButNotAvailableAgents(agentIds).toArray(); - - const update = await Users.updateLivechatStatusByAgentIds( - results.map(({ _id }) => _id), - ILivechatAgentStatus.AVAILABLE, - ); - - if (update.modifiedCount === 0) { - return; - } - - void notifyOnUserChangeAsync(async () => - results.map(({ _id, openBusinessHours }) => { - return { - id: _id, - clientAction: 'updated', - diff: { - statusLivechat: 'available', - openBusinessHours, - }, - }; - }), - ); -} diff --git a/apps/meteor/app/livechat/server/business-hour/index.ts b/apps/meteor/app/livechat/server/business-hour/index.ts deleted file mode 100644 index 95540bb44e9aa..0000000000000 --- a/apps/meteor/app/livechat/server/business-hour/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { cronJobs } from '@rocket.chat/cron'; -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; - -import { BusinessHourManager } from './BusinessHourManager'; -import { DefaultBusinessHour } from './Default'; -import { SingleBusinessHourBehavior } from './Single'; -import { callbacks } from '../../../../server/lib/callbacks'; - -export const businessHourManager = new BusinessHourManager(cronJobs); - -Meteor.startup(async () => { - const { BusinessHourBehaviorClass } = await callbacks.run('on-business-hour-start', { - BusinessHourBehaviorClass: SingleBusinessHourBehavior, - }); - businessHourManager.registerBusinessHourBehavior(new BusinessHourBehaviorClass()); - businessHourManager.registerBusinessHourType(new DefaultBusinessHour()); - - Accounts.onLogin(({ user }: { user: IUser }) => { - void (user?.roles?.includes('livechat-agent') && !user?.roles?.includes('bot') && businessHourManager.onLogin(user._id)); - }); -}); diff --git a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts deleted file mode 100644 index 3b70b836d7ea6..0000000000000 --- a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; - -import { callbacks } from '../../../../server/lib/callbacks'; -import { afterAgentUserActivated, afterAgentAdded, afterRemoveAgent } from '../lib/hooks'; - -type IAfterSaveUserProps = { - user: IUser; - oldUser: IUser | null; -}; - -const wasAgent = (user: Pick | null) => user?.roles?.includes('livechat-agent'); -const isAgent = (user: Pick | null) => user?.roles?.includes('livechat-agent'); - -const handleAgentUpdated = async (userData: IAfterSaveUserProps) => { - const { user: newUser, oldUser } = userData; - - if (wasAgent(oldUser) && !isAgent(newUser)) { - await afterRemoveAgent(newUser); - } - - if (!wasAgent(oldUser) && isAgent(newUser)) { - await afterAgentAdded(newUser); - } -}; - -const handleAgentCreated = async (user: IUser) => { - // created === no prev roles :) - if (isAgent(user)) { - await afterAgentAdded(user); - } -}; - -const handleDeactivateUser = async (user: IUser) => { - if (wasAgent(user)) { - await Users.makeAgentUnavailable(user._id); - } -}; - -const handleActivateUser = async (user: IUser) => { - if (isAgent(user) && user.username) { - await afterAgentUserActivated(user); - } -}; - -callbacks.add('afterCreateUser', handleAgentCreated, callbacks.priority.LOW, 'livechat-after-create-user-update-agent'); - -callbacks.add('afterSaveUser', handleAgentUpdated, callbacks.priority.LOW, 'livechat-after-save-user-update-agent'); - -callbacks.add('afterDeactivateUser', handleDeactivateUser, callbacks.priority.LOW, 'livechat-after-deactivate-user-remove-agent'); - -callbacks.add('afterActivateUser', handleActivateUser, callbacks.priority.LOW, 'livechat-after-activate-user-add-agent'); diff --git a/apps/meteor/app/livechat/server/hooks/offlineMessage.ts b/apps/meteor/app/livechat/server/hooks/offlineMessage.ts deleted file mode 100644 index 0496d7da1a9e4..0000000000000 --- a/apps/meteor/app/livechat/server/hooks/offlineMessage.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { callbacks } from '../../../../server/lib/callbacks'; -import { settings } from '../../../settings/server'; -import { sendRequest } from '../lib/webhooks'; - -callbacks.add( - 'livechat.offlineMessage', - async (data) => { - if (!settings.get('Livechat_webhook_on_offline_msg')) { - return data; - } - - const postData = { - type: 'LivechatOfflineMessage', - sentAt: new Date(), - visitor: { - name: data.name, - email: data.email, - }, - message: data.message, - }; - - await sendRequest(postData); - }, - callbacks.priority.MEDIUM, - 'livechat-send-email-offline-message', -); diff --git a/apps/meteor/app/livechat/server/index.ts b/apps/meteor/app/livechat/server/index.ts deleted file mode 100644 index 6b1e5b1636746..0000000000000 --- a/apps/meteor/app/livechat/server/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import './livechat'; -import './startup'; -import './hooks/leadCapture'; -import './hooks/markRoomResponded'; -import './hooks/offlineMessage'; -import './hooks/offlineMessageToChannel'; -import './hooks/saveAnalyticsData'; -import './hooks/sendToCRM'; -import './hooks/processRoomAbandonment'; -import './hooks/saveLastVisitorMessageTs'; -import './hooks/markRoomNotResponded'; -import './hooks/sendEmailTranscriptOnClose'; -import './hooks/saveLastMessageToInquiry'; -import './hooks/afterUserActions'; -import './hooks/afterAgentRemoved'; -import './hooks/afterSaveOmnichannelMessage'; -import './methods/sendMessageLivechat'; -import './methods/sendFileLivechatMessage'; -import './lib/QueueManager'; -import './lib/RoutingManager'; -import './lib/routing/External'; -import './lib/routing/ManualSelection'; -import './lib/routing/AutoSelection'; -import './lib/stream/agentStatus'; -import './sendMessageBySMS'; -import './api'; -import './api/rest'; diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts deleted file mode 100644 index 89bdd382efd39..0000000000000 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ /dev/null @@ -1,964 +0,0 @@ -import { Apps, AppEvents } from '@rocket.chat/apps'; -import { LivechatTransferEventType } from '@rocket.chat/apps-engine/definition/livechat'; -import { api, Message, Omnichannel } from '@rocket.chat/core-services'; -import type { - ILivechatVisitor, - IOmnichannelRoom, - SelectedAgent, - ISubscription, - ILivechatInquiryRecord, - IUser, - TransferData, - ILivechatDepartmentAgents, - TransferByData, - ILivechatAgent, - ILivechatDepartment, - IOmnichannelRoomInfo, - IOmnichannelInquiryExtraData, - IOmnichannelRoomExtraData, - ILivechatContact, -} from '@rocket.chat/core-typings'; -import { LivechatInquiryStatus, OmnichannelSourceType, DEFAULT_SLA_CONFIG, UserStatus } from '@rocket.chat/core-typings'; -import { LivechatPriorityWeight } from '@rocket.chat/core-typings/src/ILivechatPriority'; -import { Logger } from '@rocket.chat/logger'; -import type { InsertionModel } from '@rocket.chat/model-typings'; -import { - LivechatDepartmentAgents, - LivechatInquiry, - LivechatRooms, - LivechatDepartment, - Subscriptions, - Users, - LivechatContacts, -} from '@rocket.chat/models'; -import { removeEmpty, validateEmail as validatorFunc } from '@rocket.chat/tools'; -import { Match, check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; -import type { ClientSession } from 'mongodb'; -import { ObjectId } from 'mongodb'; - -import { queueInquiry, saveQueueInquiry } from './QueueManager'; -import { RoutingManager } from './RoutingManager'; -import { isVerifiedChannelInSource } from './contacts/isVerifiedChannelInSource'; -import { migrateVisitorIfMissingContact } from './contacts/migrateVisitorIfMissingContact'; -import { afterRoomQueued, beforeNewRoom } from './hooks'; -import { checkOnlineAgents, getOnlineAgents } from './service-status'; -import { saveTransferHistory } from './transfer'; -import { callbacks } from '../../../../server/lib/callbacks'; -import { i18n } from '../../../../server/lib/i18n'; -import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; -import { sendNotification } from '../../../lib/server'; -import { - notifyOnLivechatDepartmentAgentChanged, - notifyOnLivechatDepartmentAgentChangedByAgentsAndDepartmentId, - notifyOnSubscriptionChangedById, - notifyOnSubscriptionChangedByRoomId, - notifyOnSubscriptionChanged, - notifyOnRoomChangedById, - notifyOnLivechatInquiryChangedByRoom, -} from '../../../lib/server/lib/notifyListener'; -import { settings } from '../../../settings/server'; - -const logger = new Logger('LivechatHelper'); -export const allowAgentSkipQueue = (agent: SelectedAgent) => { - check( - agent, - Match.ObjectIncluding({ - agentId: String, - }), - ); - - return hasRoleAsync(agent.agentId, 'bot'); -}; -export const prepareLivechatRoom = async ( - rid: string, - guest: ILivechatVisitor, - roomInfo: IOmnichannelRoomInfo = { source: { type: OmnichannelSourceType.OTHER } }, - extraData?: IOmnichannelRoomExtraData, -): Promise> => { - check(rid, String); - check( - guest, - Match.ObjectIncluding({ - _id: String, - username: String, - status: Match.Maybe(String), - department: Match.Maybe(String), - }), - ); - - const extraRoomInfo = await beforeNewRoom(roomInfo, extraData); - const { _id, username, token, department: departmentId, status = 'online' } = guest; - const newRoomAt = new Date(); - const source = extraRoomInfo.source || roomInfo.source; - - if (settings.get('Livechat_Require_Contact_Verification') === 'always') { - await LivechatContacts.setChannelVerifiedStatus({ visitorId: _id, source }, false); - } - - const contactId = await migrateVisitorIfMissingContact(_id, source); - const contact = - contactId && - (await LivechatContacts.findOneEnabledById>(contactId, { - projection: { name: 1, channels: 1, activity: 1 }, - })); - if (!contact) { - throw new Error('error-invalid-contact'); - } - const verified = Boolean(contact.channels.some((channel) => isVerifiedChannelInSource(channel, _id, source))); - - const activity = guest.activity || contact.activity; - logger.debug({ - msg: 'Creating livechat room for visitor', - visitor: { _id, username, departmentId, status, activity }, - }); - - // TODO: Solve `u` missing issue - return { - _id: rid, - msgs: 0, - usersCount: 1, - lm: newRoomAt, - fname: contact.name, - t: 'l' as const, - ts: newRoomAt, - departmentId, - v: { - _id, - username, - token, - status, - ...(activity?.length && { activity }), - }, - contactId, - cl: false, - open: true, - waitingResponse: true, - verified, - // this should be overridden by extraRoomInfo when provided - // in case it's not provided, we'll use this "default" type - source: { - type: OmnichannelSourceType.OTHER, - alias: 'unknown', - }, - queuedAt: newRoomAt, - priorityWeight: LivechatPriorityWeight.NOT_SPECIFIED, - estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE, - ...extraRoomInfo, - // marker field for unique index - only new rooms have this field (see #39087) - // allows index creation to succeed even if old duplicates exist - _enforceSingleRoom: true, - } as InsertionModel; -}; - -export const createLivechatRoom = async (room: InsertionModel, session: ClientSession) => { - const result = await LivechatRooms.findOneAndUpdate( - removeEmpty(room), - { - $set: {}, - }, - { - upsert: true, - returnDocument: 'after', - session, - }, - ); - - if (!result) { - throw new Error('Room not created'); - } - - return result; -}; - -export const createLivechatInquiry = async ({ - rid, - name, - guest, - message, - initialStatus, - extraData, - session, -}: { - rid: string; - name?: string; - guest?: Pick; - message?: string; - initialStatus?: LivechatInquiryStatus; - extraData?: IOmnichannelInquiryExtraData; - session?: ClientSession; -}) => { - check(rid, String); - check(name, String); - check( - guest, - Match.ObjectIncluding({ - _id: String, - username: String, - status: Match.Maybe(String), - department: Match.Maybe(String), - activity: Match.Maybe([String]), - }), - ); - - const extraInquiryInfo = await callbacks.run('livechat.beforeInquiry', extraData); - - const { _id, username, token, department, status = UserStatus.ONLINE } = guest; - const inquirySource = extraData?.source || { type: OmnichannelSourceType.OTHER }; - const activity = - guest.activity || - (await LivechatContacts.findOneByVisitor({ visitorId: guest._id, source: inquirySource }, { projection: { activity: 1 } }))?.activity; - - const ts = new Date(); - - logger.debug({ - msg: 'Creating livechat inquiry for visitor', - visitor: { _id, username, department, status, activity }, - }); - - const result = await LivechatInquiry.findOneAndUpdate( - removeEmpty({ - rid, - name, - ts, - department, - message: message ?? '', - status: initialStatus || LivechatInquiryStatus.READY, - v: { - _id, - username, - token, - status, - ...(activity?.length && { activity }), - }, - t: 'l', - priorityWeight: LivechatPriorityWeight.NOT_SPECIFIED, - estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE, - - ...extraInquiryInfo, - }), - { - $set: { - _id: new ObjectId().toHexString(), - }, - }, - { - upsert: true, - returnDocument: 'after', - session, - }, - ); - logger.debug({ - msg: 'Inquiry created for visitor', - inquiryId: result, - visitorId: _id, - }); - - if (!result) { - throw new Error('Inquiry not created'); - } - - return result; -}; - -export const createLivechatSubscription = async ( - rid: string, - name: string, - guest: Pick, - agent: SelectedAgent, - department?: string, -) => { - check(rid, String); - check(name, String); - check( - guest, - Match.ObjectIncluding({ - _id: String, - username: String, - status: Match.Maybe(String), - }), - ); - check( - agent, - Match.ObjectIncluding({ - agentId: String, - username: String, - }), - ); - - const existingSubscription = await Subscriptions.findOneByRoomIdAndUserId(rid, agent.agentId); - if (existingSubscription?._id) { - return existingSubscription; - } - - const { _id, username, token, status = UserStatus.ONLINE } = guest; - - const subscriptionData: InsertionModel = { - rid, - name, - fname: name, - alert: true, - open: true, - unread: 1, - userMentions: 1, - groupMentions: 0, - u: { - _id: agent.agentId, - username: agent.username, - }, - t: 'l', - desktopNotifications: 'all', - mobilePushNotifications: 'all', - emailNotifications: 'all', - v: { - _id, - username, - token, - status, - }, - ts: new Date(), - ...(department && { department }), - } as InsertionModel; - - const response = await Subscriptions.insertOne(subscriptionData); - - if (response?.insertedId) { - void notifyOnSubscriptionChangedById(response.insertedId, 'inserted'); - } - - return response; -}; - -export const removeAgentFromSubscription = async (rid: string, { _id, username }: Pick) => { - const room = await LivechatRooms.findOneById(rid); - const user = await Users.findOneById(_id); - - if (!room || !user) { - return; - } - - const deletedSubscription = await Subscriptions.removeByRoomIdAndUserId(rid, _id); - if (deletedSubscription) { - void notifyOnSubscriptionChanged(deletedSubscription, 'removed'); - } - - await Message.saveSystemMessage('ul', rid, username || '', { _id: user._id, username: user.username, name: user.name }); - - setImmediate(() => { - void Apps.self?.triggerEvent(AppEvents.IPostLivechatAgentUnassigned, { room, user }); - }); -}; - -export const parseAgentCustomFields = (customFields?: Record) => { - if (!customFields) { - return; - } - - const externalCustomFields = () => { - const accountCustomFields = settings.get('Accounts_CustomFields'); - if (!accountCustomFields || accountCustomFields.trim() === '') { - return []; - } - - try { - const parseCustomFields = JSON.parse(accountCustomFields); - return Object.keys(parseCustomFields).filter((customFieldKey) => parseCustomFields[customFieldKey].sendToIntegrations === true); - } catch (error) { - logger.error({ msg: 'Error parsing agent custom fields', err: error }); - return []; - } - }; - - const externalCF = externalCustomFields(); - return Object.keys(customFields).reduce( - (newObj, key) => (externalCF.includes(key) ? { ...newObj, [key]: customFields[key] } : newObj), - {}, - ); -}; - -export const normalizeAgent = async (agentId?: string) => { - if (!agentId) { - return; - } - - if (!settings.get('Livechat_show_agent_info')) { - return { hiddenInfo: true }; - } - - const agent = await Users.getAgentInfo(agentId, settings.get('Livechat_show_agent_email')); - if (!agent) { - return; - } - - const { customFields: agentCustomFields, ...extraData } = agent; - const customFields = parseAgentCustomFields(agentCustomFields); - - return Object.assign(extraData, { ...(customFields && { customFields }) }) as ILivechatAgent; -}; - -export const dispatchAgentDelegated = async (rid: string, agentId?: string) => { - const agent = await normalizeAgent(agentId); - - void api.broadcast('omnichannel.room', rid, { - type: 'agentData', - data: agent, - }); -}; - -/** - * @deprecated - */ - -export const dispatchInquiryQueued = async (inquiry: ILivechatInquiryRecord, agent?: SelectedAgent | null) => { - if (!inquiry?._id) { - return; - } - logger.debug({ - msg: 'Notifying agents of queued inquiry', - inquiryId: inquiry._id, - agentId: agent?.agentId, - }); - - const { department, rid, v } = inquiry; - const room = await LivechatRooms.findOneById(rid); - if (!room) { - return; - } - - void afterRoomQueued(room); - - if (RoutingManager.getConfig()?.autoAssignAgent) { - return; - } - - if (agent && (await allowAgentSkipQueue(agent))) { - return; - } - - await saveQueueInquiry(inquiry); - - // Alert only the online agents of the queued request - const onlineAgents = await getOnlineAgents(department, agent); - if (!onlineAgents) { - logger.debug({ msg: 'Cannot notify agents of queued inquiry. No online agents found' }); - return; - } - - const notificationUserName = v && (v.name || v.username); - - for await (const agent of onlineAgents) { - const { _id, active, emails, language, status, statusConnection, username } = agent; - await sendNotification({ - // fake a subscription in order to make use of the function defined above - subscription: { - rid, - u: { - _id, - }, - receiver: [ - { - active, - emails, - language, - status, - statusConnection, - username, - }, - ], - name: '', - }, - sender: v, - hasMentionToAll: true, // consider all agents to be in the room - hasReplyToThread: false, - disableAllMessageNotifications: false, - hasMentionToHere: false, - message: { _id: '', u: v, msg: '' }, - // we should use server's language for this type of messages instead of user's - notificationMessage: i18n.t('User_started_a_new_conversation', { username: notificationUserName, lng: language }), - room: Object.assign(room, { name: i18n.t('New_chat_in_queue', { lng: language }) }), - mentionIds: [], - }); - } -}; - -export const forwardRoomToAgent = async (room: IOmnichannelRoom, transferData: TransferData) => { - if (!room?.open) { - return false; - } - - logger.debug({ - msg: 'Forwarding room to agent', - roomId: room._id, - userId: transferData.userId, - }); - - const { userId: agentId, clientAction } = transferData; - if (!agentId) { - throw new Error('error-invalid-agent'); - } - const user = await Users.findOneOnlineAgentById( - agentId, - settings.get('Livechat_enabled_when_agent_idle'), - settings.get('Livechat_accept_chats_with_no_agents'), - {}, - ); - if (!user) { - logger.debug({ - msg: 'Agent is offline. Cannot forward', - agentId, - }); - throw new Error('error-user-is-offline'); - } - - const { _id: rid, servedBy: oldServedBy } = room; - const inquiry = await LivechatInquiry.findOneByRoomId(rid, {}); - if (!inquiry) { - logger.debug({ - msg: 'No inquiries found for room. Cannot forward', - roomId: room._id, - }); - throw new Error('error-invalid-inquiry'); - } - - if (oldServedBy && agentId === oldServedBy._id) { - throw new Error('error-selected-agent-room-agent-are-same'); - } - - const { username } = user; - const agent = { agentId, username }; - // Remove department from inquiry to make sure the routing algorithm treat this as forwarding to agent and not as forwarding to department - delete inquiry.department; - // There are some Enterprise features that may interrupt the forwarding process - // Due to that we need to check whether the agent has been changed or not - logger.debug({ - msg: 'Forwarding inquiry to agent', - inquiryId: inquiry._id, - agentId: agent.agentId, - }); - const roomTaken = await RoutingManager.takeInquiry( - inquiry, - agent, - { - ...(clientAction && { clientAction }), - }, - room, - ); - if (!roomTaken) { - logger.debug({ - msg: 'Cannot forward inquiry', - inquiryId: inquiry._id, - }); - return false; - } - - await saveTransferHistory(room, transferData); - - const { servedBy } = roomTaken; - if (servedBy) { - if (oldServedBy && servedBy._id !== oldServedBy._id) { - await RoutingManager.removeAllRoomSubscriptions(room, servedBy); - } - - setImmediate(() => { - void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomTransferred, { - type: LivechatTransferEventType.AGENT, - room: rid, - from: oldServedBy?._id, - to: servedBy._id, - }); - }); - } - - logger.debug({ - msg: 'Inquiry taken by agent after forwarding', - inquiryId: inquiry._id, - agentId: agent.agentId, - }); - await callbacks.run('livechat.afterForwardChatToAgent', { rid, servedBy, oldServedBy }); - return true; -}; - -export const updateChatDepartment = async ({ - rid, - newDepartmentId, - oldDepartmentId, -}: { - rid: string; - newDepartmentId: string; - oldDepartmentId?: string; -}) => { - const responses = await Promise.all([ - LivechatRooms.changeDepartmentIdByRoomId(rid, newDepartmentId), - LivechatInquiry.changeDepartmentIdByRoomId(rid, newDepartmentId), - Subscriptions.changeDepartmentByRoomId(rid, newDepartmentId), - ]); - - if (responses[0].modifiedCount) { - void notifyOnRoomChangedById(rid); - } - if (responses[1].modifiedCount) { - void notifyOnLivechatInquiryChangedByRoom(rid); - } - if (responses[2].modifiedCount) { - void notifyOnSubscriptionChangedByRoomId(rid); - } - - setImmediate(() => { - void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomTransferred, { - type: LivechatTransferEventType.DEPARTMENT, - room: rid, - from: oldDepartmentId, - to: newDepartmentId, - }); - }); - - return callbacks.run('livechat.afterForwardChatToDepartment', { - rid, - newDepartmentId, - oldDepartmentId, - }); -}; - -export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILivechatVisitor, transferData: TransferData) => { - if (!room?.open) { - return false; - } - logger.debug({ - msg: 'Attempting to forward room to department', - roomId: room._id, - departmentId: transferData.departmentId, - }); - - await callbacks.run('livechat.beforeForwardRoomToDepartment', { room, transferData }); - const { _id: rid, servedBy: oldServedBy, departmentId: oldDepartmentId } = room; - let agent = null; - - const inquiry = await LivechatInquiry.findOneByRoomId(rid, {}); - if (!inquiry) { - logger.debug({ - msg: 'Cannot forward room. No inquiries found', - roomId: room._id, - }); - throw new Error('error-transferring-inquiry'); - } - - const { departmentId } = transferData; - if (!departmentId) { - logger.debug({ - msg: 'Cannot forward room. No departmentId provided', - roomId: room._id, - }); - throw new Error('error-transferring-inquiry-no-department'); - } - if (oldDepartmentId === departmentId) { - throw new Error('error-forwarding-chat-same-department'); - } - - const { userId: agentId, clientAction } = transferData; - if (agentId) { - logger.debug({ - msg: 'Forwarding room to department and user', - roomId: room._id, - departmentId, - agentId, - }); - const user = await Users.findOneOnlineAgentById( - agentId, - settings.get('Livechat_enabled_when_agent_idle'), - settings.get('Livechat_accept_chats_with_no_agents'), - {}, - ); - if (!user) { - throw new Error('error-user-is-offline'); - } - const isInDepartment = await LivechatDepartmentAgents.findOneByAgentIdAndDepartmentId(agentId, departmentId, { - projection: { _id: 1 }, - }); - if (!isInDepartment) { - throw new Error('error-user-not-belong-to-department'); - } - const { username } = user; - agent = { agentId, username }; - } - - const department = await LivechatDepartment.findOneById< - Pick - >(departmentId, { - projection: { - allowReceiveForwardOffline: 1, - fallbackForwardDepartment: 1, - name: 1, - }, - }); - - // Cases: - // 1. Routing is manual - // 2. Server is out of macs - // 3. Department allows to forward when offline and department is offline - // 4. Department is online && waiting queue is enabled - const onlineAgents = await checkOnlineAgents(departmentId); - const isWaitingQueueEnabled = settings.get('Livechat_waiting_queue'); - if ( - !RoutingManager.getConfig()?.autoAssignAgent || - !(await Omnichannel.isWithinMACLimit(room)) || - (department?.allowReceiveForwardOffline && !onlineAgents) || - (isWaitingQueueEnabled && onlineAgents) - ) { - logger.debug({ - msg: 'Room will be on department queue', - roomId: room._id, - departmentId, - departmentAllowOffline: department?.allowReceiveForwardOffline, - areAgentsOnline: onlineAgents, - isWaitingQueueEnabled, - }); - await saveTransferHistory(room, transferData); - return RoutingManager.unassignAgent(inquiry, departmentId, true, agent); - } - - // Fake the department to forward the inquiry - Case the forward process does not success - // the inquiry will stay in the same original department - inquiry.department = departmentId; - const roomTaken = await RoutingManager.delegateInquiry( - inquiry, - agent, - { - forwardingToDepartment: { oldDepartmentId }, - ...(clientAction && { clientAction }), - }, - room, - ); - if (!roomTaken) { - logger.debug({ - msg: 'Cannot forward room. Unable to delegate inquiry', - roomId: room._id, - }); - return false; - } - - const { servedBy, chatQueued } = roomTaken; - if (!chatQueued && oldServedBy && oldServedBy._id === servedBy?._id) { - if (!department?.fallbackForwardDepartment?.length) { - logger.debug({ - msg: 'Cannot forward room. Chat assigned to agent instead', - roomId: room._id, - agentId: servedBy._id, - previousAgentId: oldServedBy._id, - }); - throw new Error('error-no-agents-available-for-service-on-department'); - } - - if (!transferData.originalDepartmentName) { - transferData.originalDepartmentName = department.name; - } - // if a chat has a fallback department, attempt to redirect chat to there [EE] - const transferSuccess = !!(await callbacks.run('livechat:onTransferFailure', room, { guest, transferData, department })); - // On CE theres no callback so it will return the room - if (typeof transferSuccess !== 'boolean' || !transferSuccess) { - logger.debug({ - msg: 'Cannot forward room. Unable to delegate inquiry', - roomId: room._id, - }); - return false; - } - - return true; - } - - // Send just 1 message to the room to inform the user that the chat was transferred - if (transferData.usingFallbackDep) { - const { _id, username } = transferData.transferredBy; - await Message.saveSystemMessage( - 'livechat_transfer_history_fallback', - room._id, - '', - { _id, username }, - { - ...(transferData.transferredBy.userType === 'visitor' && { token: room.v.token }), - transferData: { - ...transferData, - prevDepartment: transferData.originalDepartmentName, - }, - }, - ); - } - - await saveTransferHistory(room, transferData); - if (oldServedBy) { - // if chat is queued then we don't ignore the new servedBy agent bcs at this - // point the chat is not assigned to him/her and it is still in the queue - await RoutingManager.removeAllRoomSubscriptions(room, !chatQueued ? servedBy : undefined); - } - - await updateChatDepartment({ rid, newDepartmentId: departmentId, oldDepartmentId }); - - if (chatQueued) { - logger.debug({ - msg: 'Forwarding successful. Marking inquiry as ready', - inquiryId: inquiry._id, - }); - await LivechatInquiry.readyInquiry(inquiry._id); - await LivechatRooms.removeAgentByRoomId(rid); - await dispatchAgentDelegated(rid); - const newInquiry = await LivechatInquiry.findOneById(inquiry._id); - if (!newInquiry) { - logger.debug({ - msg: 'Inquiry not found after forwarding', - inquiryId: inquiry._id, - }); - throw new Error('error-invalid-inquiry'); - } - - await queueInquiry(newInquiry); - logger.debug({ - msg: 'Inquiry queued successfully after forwarding', - inquiryId: inquiry._id, - }); - } - - return true; -}; - -type MakePropertyOptional = Omit & { [P in K]?: T[P] }; - -export const normalizeTransferredByData = ( - transferredBy: MakePropertyOptional, - room: IOmnichannelRoom, -): TransferByData => { - if (!transferredBy || !room) { - throw new Error('You must provide "transferredBy" and "room" params to "getTransferredByData"'); - } - const { servedBy: { _id: agentId } = {} } = room; - const { _id, username, name, userType: transferType } = transferredBy; - const userType = transferType || (_id === agentId ? 'agent' : 'user'); - return { - _id, - username, - ...(name && { name }), - userType, - }; -}; - -const parseFromIntOrStr = (value: string | number) => { - if (typeof value === 'number') { - return value; - } - return parseInt(value); -}; - -export const updateDepartmentAgents = async ( - departmentId: string, - agents: { - upsert?: (Pick & { count?: number; order?: number })[]; - remove?: Pick[]; - }, - departmentEnabled: boolean, -) => { - check(departmentId, String); - check(agents, { - upsert: Match.Maybe([ - Match.ObjectIncluding({ - agentId: String, - username: Match.Maybe(String), - count: Match.Maybe(Match.Integer), - order: Match.Maybe(Match.Integer), - }), - ]), - remove: Match.Maybe([ - Match.ObjectIncluding({ - agentId: String, - username: Match.Maybe(String), - count: Match.Maybe(Match.Integer), - order: Match.Maybe(Match.Integer), - }), - ]), - }); - - const { upsert = [], remove = [] } = agents; - - const agentsUpdated = []; - const agentsRemoved = remove.map(({ agentId }: { agentId: string }) => agentId); - const agentsAdded = []; - - if (agentsRemoved.length > 0) { - const removedIds = await LivechatDepartmentAgents.findByAgentsAndDepartmentId(agentsRemoved, departmentId, { - projection: { agentId: 1 }, - }).toArray(); - - const { deletedCount } = await LivechatDepartmentAgents.removeByIds(removedIds.map(({ _id }) => _id)); - - if (deletedCount > 0) { - removedIds.forEach(({ _id, agentId }) => { - void notifyOnLivechatDepartmentAgentChanged( - { - _id, - agentId, - departmentId, - }, - 'removed', - ); - }); - } - - callbacks.runAsync('livechat.removeAgentDepartment', { departmentId, agentsId: agentsRemoved }); - } - - for await (const agent of upsert) { - const agentFromDb = await Users.findOneById(agent.agentId, { projection: { _id: 1, username: 1 } }); - if (!agentFromDb) { - continue; - } - - const livechatDepartmentAgent = await LivechatDepartmentAgents.saveAgent({ - agentId: agent.agentId, - departmentId, - username: agentFromDb.username || '', - count: agent.count ? parseFromIntOrStr(agent.count) : 0, - order: agent.order ? parseFromIntOrStr(agent.order) : 0, - departmentEnabled, - }); - - if (livechatDepartmentAgent.upsertedId) { - void notifyOnLivechatDepartmentAgentChanged( - { - _id: livechatDepartmentAgent.upsertedId as any, - agentId: agent.agentId, - departmentId, - }, - 'inserted', - ); - } else { - agentsUpdated.push(agent.agentId); - } - - agentsAdded.push(agent.agentId); - } - - if (agentsAdded.length > 0) { - callbacks.runAsync('livechat.saveAgentDepartment', { - departmentId, - agentsId: agentsAdded, - }); - } - - if (agentsUpdated.length > 0) { - void notifyOnLivechatDepartmentAgentChangedByAgentsAndDepartmentId(agentsUpdated, departmentId); - } - - if (agentsRemoved.length > 0 || agentsAdded.length > 0) { - const numAgents = await LivechatDepartmentAgents.countByDepartmentId(departmentId); - await LivechatDepartment.updateNumAgentsById(departmentId, numAgents); - } - - return true; -}; - -export const validateEmail = (email: string) => { - if (!validatorFunc(email)) { - throw new Meteor.Error('error-invalid-email', `Invalid email ${email}`, { - function: 'Livechat.validateEmail', - email, - }); - } - return true; -}; diff --git a/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts b/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts deleted file mode 100644 index d02cf20f87262..0000000000000 --- a/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts +++ /dev/null @@ -1,395 +0,0 @@ -import { OmnichannelAnalytics } from '@rocket.chat/core-services'; -import type { IUser } from '@rocket.chat/core-typings'; -import { LivechatRooms, Users, LivechatVisitors, LivechatAgentActivity } from '@rocket.chat/models'; -import mem from 'mem'; -import moment from 'moment'; - -import { secondsToHHMMSS } from '../../../../../lib/utils/secondsToHHMMSS'; -import { settings } from '../../../../settings/server'; -import { getAnalyticsOverviewDataCachedForRealtime } from '../AnalyticsTyped'; -import { - findPercentageOfAbandonedRoomsAsync, - findAllAverageOfChatDurationTimeAsync, - findAllAverageWaitingTimeAsync, - findAllNumberOfAbandonedRoomsAsync, - findAllAverageServiceTimeAsync, -} from './departments'; - -const findAllChatsStatusAsync = async ({ start, end, departmentId = undefined }: { start: Date; end: Date; departmentId?: string }) => { - if (!start || !end) { - throw new Error('"start" and "end" must be provided'); - } - return { - open: await LivechatRooms.countAllOpenChatsBetweenDate({ start, end, departmentId }), - closed: await LivechatRooms.countAllClosedChatsBetweenDate({ start, end, departmentId }), - queued: await LivechatRooms.countAllQueuedChatsBetweenDate({ start, end, departmentId }), - onhold: await LivechatRooms.getOnHoldConversationsBetweenDate(start, end, departmentId), - }; -}; - -const getProductivityMetricsAsync = async ({ - start, - end, - departmentId = undefined, - user, -}: { - start: string; - end: string; - departmentId?: string; - user: IUser; -}) => { - if (!start || !end) { - throw new Error('"start" and "end" must be provided'); - } - const totalizers = - (await OmnichannelAnalytics.getAnalyticsOverviewData({ - daterange: { - from: start, - to: end, - }, - analyticsOptions: { - name: 'Productivity', - }, - departmentId, - utcOffset: user?.utcOffset, - language: user?.language || settings.get('Language') || 'en', - executedBy: user._id, - })) || []; - const averageWaitingTime = await findAllAverageWaitingTimeAsync({ - start: new Date(start), - end: new Date(end), - departmentId, - }); - - const totalOfWaitingTime = averageWaitingTime.departments.length; - - const sumOfWaitingTime = averageWaitingTime.departments.reduce((acc: number, serviceTime: { averageWaitingTimeInSeconds: number }) => { - acc += serviceTime.averageWaitingTimeInSeconds; - return acc; - }, 0); - const totalOfAvarageWaitingTime = totalOfWaitingTime === 0 ? 0 : sumOfWaitingTime / totalOfWaitingTime; - - return { - totalizers: [...totalizers, { title: 'Avg_of_waiting_time', value: secondsToHHMMSS(totalOfAvarageWaitingTime) }], - }; -}; - -const getAgentsProductivityMetricsAsync = async ({ - start, - end, - departmentId = undefined, - user, -}: { - start: string; - end: string; - departmentId?: string; - user: IUser; -}) => { - if (!start || !end) { - throw new Error('"start" and "end" must be provided'); - } - // TODO: check type of date - const averageOfAvailableServiceTime = ( - await LivechatAgentActivity.findAllAverageAvailableServiceTime({ - date: parseInt(moment(start).format('YYYYMMDD')) as any, - departmentId, - }) - )[0]; - const averageOfServiceTime = await findAllAverageServiceTimeAsync({ - start: new Date(start), - end: new Date(end), - departmentId, - }); - const totalizers = - (await OmnichannelAnalytics.getAnalyticsOverviewData({ - daterange: { - from: start, - to: end, - }, - analyticsOptions: { - name: 'Conversations', - }, - departmentId, - utcOffset: user.utcOffset, - language: user.language || settings.get('Language') || 'en', - executedBy: user._id, - })) || []; - - const totalOfServiceTime = averageOfServiceTime.departments.length; - - const sumOfServiceTime = averageOfServiceTime.departments.reduce( - ( - acc: number, - serviceTime: { - averageServiceTimeInSeconds: number; - }, - ) => { - acc += serviceTime.averageServiceTimeInSeconds; - return acc; - }, - 0, - ); - const totalOfAverageAvailableServiceTime = averageOfAvailableServiceTime - ? averageOfAvailableServiceTime.averageAvailableServiceTimeInSeconds - : 0; - const totalOfAverageServiceTime = totalOfServiceTime === 0 ? 0 : sumOfServiceTime / totalOfServiceTime; - - return { - totalizers: [ - ...totalizers.filter((metric: { title: string }) => metric.title === 'Busiest_time'), - { - title: 'Avg_of_available_service_time', - value: secondsToHHMMSS(totalOfAverageAvailableServiceTime), - }, - { title: 'Avg_of_service_time', value: secondsToHHMMSS(totalOfAverageServiceTime) }, - ], - }; -}; - -const getChatsMetricsAsync = async ({ start, end, departmentId = undefined }: { start: Date; end: Date; departmentId?: string }) => { - if (!start || !end) { - throw new Error('"start" and "end" must be provided'); - } - const abandonedRooms = await findAllNumberOfAbandonedRoomsAsync({ - start, - end, - departmentId, - }); - const averageOfAbandonedRooms = await findPercentageOfAbandonedRoomsAsync({ - start, - end, - departmentId, - }); - const averageOfChatDurationTime = await findAllAverageOfChatDurationTimeAsync({ - start, - end, - departmentId, - }); - - const totalOfAbandonedRooms = averageOfAbandonedRooms.departments.length; - const totalOfChatDurationTime = averageOfChatDurationTime.departments.length; - - const sumOfPercentageOfAbandonedRooms = averageOfAbandonedRooms.departments.reduce( - ( - acc: number, - abandonedRoom: { - percentageOfAbandonedChats: number; - }, - ) => { - acc += abandonedRoom.percentageOfAbandonedChats; - return acc; - }, - 0, - ); - const sumOfChatDurationTime = averageOfChatDurationTime.departments.reduce( - ( - acc: number, - chatDurationTime: { - averageChatDurationTimeInSeconds: number; - }, - ) => { - acc += chatDurationTime.averageChatDurationTimeInSeconds; - return acc; - }, - 0, - ); - const totalAbandonedRooms = abandonedRooms.departments.reduce( - ( - acc: number, - item: { - abandonedRooms: number; - }, - ) => { - acc += item.abandonedRooms; - return acc; - }, - 0, - ); - - const totalOfAverageAbandonedRooms = totalOfAbandonedRooms === 0 ? 0 : sumOfPercentageOfAbandonedRooms / totalOfAbandonedRooms; - const totalOfAverageChatDurationTime = totalOfChatDurationTime === 0 ? 0 : sumOfChatDurationTime / totalOfChatDurationTime; - - return { - totalizers: [ - { title: 'Total_abandoned_chats', value: totalAbandonedRooms }, - { title: 'Avg_of_abandoned_chats', value: `${totalOfAverageAbandonedRooms}%` }, - { - title: 'Avg_of_chat_duration_time', - value: secondsToHHMMSS(totalOfAverageChatDurationTime), - }, - ], - }; -}; - -const getConversationsMetricsAsync = async ({ - start, - end, - departmentId, - user, -}: { - start: string; - end: string; - departmentId?: string; - user: IUser; -}) => { - if (!start || !end) { - throw new Error('"start" and "end" must be provided'); - } - const totalizers = - (await getAnalyticsOverviewDataCachedForRealtime({ - daterange: { - from: start, - to: end, - }, - analyticsOptions: { - name: 'Conversations', - }, - ...(departmentId && departmentId !== 'undefined' && { departmentId }), - utcOffset: user.utcOffset, - language: user.language || settings.get('Language') || 'en', - executedBy: user._id, - })) || []; - const metrics = ['Total_conversations', 'Open_conversations', 'On_Hold_conversations', 'Total_messages']; - const visitorsCount = await LivechatVisitors.countVisitorsBetweenDate({ - start: new Date(start), - end: new Date(end), - department: departmentId, - }); - return { - totalizers: [ - ...totalizers.filter((metric: { title: string }) => metrics.includes(metric.title)), - { title: 'Total_visitors', value: visitorsCount }, - ], - }; -}; - -const findAllChatMetricsByAgentAsync = async ({ - start, - end, - departmentId = undefined, -}: { - start: Date; - end: Date; - departmentId?: string; -}) => { - if (!start || !end) { - throw new Error('"start" and "end" must be provided'); - } - const open = await LivechatRooms.countAllOpenChatsByAgentBetweenDate({ - start, - end, - departmentId, - }); - const closed = await LivechatRooms.countAllClosedChatsByAgentBetweenDate({ - start, - end, - departmentId, - }); - const onhold = await LivechatRooms.countAllOnHoldChatsByAgentBetweenDate({ - start, - end, - departmentId, - }); - - const result: Record = {}; - (open || []).forEach((agent: { chats: number; _id: string }) => { - result[agent._id] = { open: agent.chats, closed: 0, onhold: 0 }; - }); - (closed || []).forEach((agent: { _id: string; chats: number }) => { - result[agent._id] = { - open: result[agent._id] ? result[agent._id].open : 0, - closed: agent.chats, - }; - }); - - (onhold || []).forEach((agent: { _id: string; chats: number }) => { - result[agent._id] = { - ...result[agent._id], - onhold: agent.chats, - }; - }); - return result; -}; - -const findAllAgentsStatusAsync = async ({ departmentId = undefined }: { departmentId?: string }) => - (await Users.countAllAgentsStatus({ departmentId }))[0]; - -const findAllChatMetricsByDepartmentAsync = async ({ - start, - end, - departmentId = undefined, -}: { - start: Date; - end: Date; - departmentId?: string; -}) => { - if (!start || !end) { - throw new Error('"start" and "end" must be provided'); - } - const open = await LivechatRooms.countAllOpenChatsByDepartmentBetweenDate({ - start, - end, - departmentId, - }); - const closed = await LivechatRooms.countAllClosedChatsByDepartmentBetweenDate({ - start, - end, - departmentId, - }); - const result: Record = {}; - (open || []).forEach((department: { name: string; chats: number }) => { - result[department.name] = { open: department.chats, closed: 0 }; - }); - (closed || []).forEach((department: { name: string; chats: number }) => { - result[department.name] = { - open: result[department.name] ? result[department.name].open : 0, - closed: department.chats, - }; - }); - return result; -}; - -const findAllResponseTimeMetricsAsync = async ({ - start, - end, - departmentId = undefined, -}: { - start: Date; - end: Date; - departmentId?: string; -}) => { - if (!start || !end) { - throw new Error('"start" and "end" must be provided'); - } - const responseTimes = (await LivechatRooms.calculateResponseTimingsBetweenDates({ start, end, departmentId }))[0]; - const reactionTimes = (await LivechatRooms.calculateReactionTimingsBetweenDates({ start, end, departmentId }))[0]; - const durationTimings = (await LivechatRooms.calculateDurationTimingsBetweenDates({ start, end, departmentId }))[0]; - - return { - response: { - avg: responseTimes ? responseTimes.avg : 0, - longest: responseTimes ? responseTimes.longest : 0, - }, - reaction: { - avg: reactionTimes ? reactionTimes.avg : 0, - longest: reactionTimes ? reactionTimes.longest : 0, - }, - chatDuration: { - avg: durationTimings ? durationTimings.avg : 0, - longest: durationTimings ? durationTimings.longest : 0, - }, - }; -}; - -export const getConversationsMetricsAsyncCached = mem(getConversationsMetricsAsync, { maxAge: 5000, cacheKey: JSON.stringify }); -export const getAgentsProductivityMetricsAsyncCached = mem(getAgentsProductivityMetricsAsync, { maxAge: 5000, cacheKey: JSON.stringify }); -export const getChatsMetricsAsyncCached = mem(getChatsMetricsAsync, { maxAge: 5000, cacheKey: JSON.stringify }); -export const getProductivityMetricsAsyncCached = mem(getProductivityMetricsAsync, { maxAge: 5000, cacheKey: JSON.stringify }); -export const findAllChatsStatusAsyncCached = mem(findAllChatsStatusAsync, { maxAge: 5000, cacheKey: JSON.stringify }); -export const findAllChatMetricsByAgentAsyncCached = mem(findAllChatMetricsByAgentAsync, { maxAge: 5000, cacheKey: JSON.stringify }); -export const findAllAgentsStatusAsyncCached = mem(findAllAgentsStatusAsync, { maxAge: 5000, cacheKey: JSON.stringify }); -export const findAllChatMetricsByDepartmentAsyncCached = mem(findAllChatMetricsByDepartmentAsync, { - maxAge: 5000, - cacheKey: JSON.stringify, -}); -export const findAllResponseTimeMetricsAsyncCached = mem(findAllResponseTimeMetricsAsync, { maxAge: 5000, cacheKey: JSON.stringify }); diff --git a/apps/meteor/app/livechat/server/lib/analytics/departments.ts b/apps/meteor/app/livechat/server/lib/analytics/departments.ts deleted file mode 100644 index f505c6f17f9d0..0000000000000 --- a/apps/meteor/app/livechat/server/lib/analytics/departments.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { LivechatRooms, Messages } from '@rocket.chat/models'; - -import { settings } from '../../../../settings/server'; - -type Params = { - start: Date; - end: Date; - options?: any; - departmentId?: string; -}; - -export const findAllRoomsAsync = async ({ start, end, answered, departmentId, options = {} }: Params & { answered?: boolean }) => { - if (!start || !end) { - throw new Error('"start" and "end" must be provided'); - } - const total = await LivechatRooms.findAllRooms({ - start, - answered, - end, - departmentId, - onlyCount: true, - }).toArray(); - return { - departments: await LivechatRooms.findAllRooms({ - start, - answered, - end, - departmentId, - options, - }).toArray(), - total: total.length ? total[0].total : 0, - }; -}; - -export const findAllAverageOfChatDurationTimeAsync = async ({ start, end, departmentId, options = {} }: Params) => { - if (!start || !end) { - throw new Error('"start" and "end" must be provided'); - } - const total = await LivechatRooms.findAllAverageOfChatDurationTime({ - start, - end, - departmentId, - onlyCount: true, - }).toArray(); - return { - departments: await LivechatRooms.findAllAverageOfChatDurationTime({ - start, - end, - departmentId, - options, - }).toArray(), - total: total.length ? total[0].total : 0, - }; -}; - -export const findAllAverageServiceTimeAsync = async ({ start, end, departmentId, options = {} }: Params) => { - if (!start || !end) { - throw new Error('"start" and "end" must be provided'); - } - const total = await LivechatRooms.findAllAverageOfServiceTime({ - start, - end, - departmentId, - onlyCount: true, - }).toArray(); - return { - departments: await LivechatRooms.findAllAverageOfServiceTime({ - start, - end, - departmentId, - options, - }).toArray(), - total: total.length ? total[0].total : 0, - }; -}; - -export const findAllServiceTimeAsync = async ({ start, end, departmentId, options = {} }: Params) => { - if (!start || !end) { - throw new Error('"start" and "end" must be provided'); - } - const total = await LivechatRooms.findAllServiceTime({ - start, - end, - departmentId, - onlyCount: true, - }).toArray(); - return { - departments: await LivechatRooms.findAllServiceTime({ - start, - end, - departmentId, - options, - }).toArray(), - total: total.length ? total[0].total : 0, - }; -}; - -export const findAllAverageWaitingTimeAsync = async ({ start, end, departmentId, options = {} }: Params) => { - if (!start || !end) { - throw new Error('"start" and "end" must be provided'); - } - const total = await LivechatRooms.findAllAverageWaitingTime({ - start, - end, - departmentId, - onlyCount: true, - }).toArray(); - return { - departments: await LivechatRooms.findAllAverageWaitingTime({ - start, - end, - departmentId, - options, - }).toArray(), - total: total.length ? total[0].total : 0, - }; -}; - -export const findAllNumberOfTransferredRoomsAsync = async ({ start, end, departmentId, options = {} }: Params) => { - if (!start || !end) { - throw new Error('"start" and "end" must be provided'); - } - const total = await Messages.findAllNumberOfTransferredRooms({ - start, - end, - departmentId, - onlyCount: true, - }).toArray(); - return { - departments: await Messages.findAllNumberOfTransferredRooms({ - start, - end, - departmentId, - options, - }).toArray(), - total: total.length ? total[0].total : 0, - }; -}; - -export const findAllNumberOfAbandonedRoomsAsync = async ({ start, end, departmentId, options = {} }: Params) => { - if (!start || !end) { - throw new Error('"start" and "end" must be provided'); - } - const total = await ( - await LivechatRooms.findAllNumberOfAbandonedRooms({ - start, - end, - departmentId, - inactivityTimeout: settings.get('Livechat_visitor_inactivity_timeout'), - onlyCount: true, - }) - ).toArray(); - return { - departments: await ( - await LivechatRooms.findAllNumberOfAbandonedRooms({ - start, - end, - departmentId, - inactivityTimeout: settings.get('Livechat_visitor_inactivity_timeout'), - options, - }) - ).toArray(), - total: total.length ? total[0].total : 0, - }; -}; - -export const findPercentageOfAbandonedRoomsAsync = async ({ start, end, departmentId, options = {} }: Params) => { - if (!start || !end) { - throw new Error('"start" and "end" must be provided'); - } - const total = await ( - await LivechatRooms.findPercentageOfAbandonedRooms({ - start, - end, - departmentId, - inactivityTimeout: settings.get('Livechat_visitor_inactivity_timeout'), - onlyCount: true, - }) - ).toArray(); - return { - departments: await ( - await LivechatRooms.findPercentageOfAbandonedRooms({ - start, - end, - departmentId, - inactivityTimeout: settings.get('Livechat_visitor_inactivity_timeout'), - options, - }) - ).toArray(), - total: total.length ? total[0].total : 0, - }; -}; diff --git a/apps/meteor/app/livechat/server/lib/messages.ts b/apps/meteor/app/livechat/server/lib/messages.ts deleted file mode 100644 index 558aecde98f5c..0000000000000 --- a/apps/meteor/app/livechat/server/lib/messages.ts +++ /dev/null @@ -1,151 +0,0 @@ -import dns from 'node:dns'; -import * as util from 'node:util'; - -import type { ILivechatVisitor, AtLeast, IMessage, IUser, IOmnichannelRoomInfo, SelectedAgent } from '@rocket.chat/core-typings'; -import { LivechatDepartment, Messages } from '@rocket.chat/models'; - -import type { ILivechatMessage } from './localTypes'; -import { getRoom } from './rooms'; -import { showConnecting } from './utils'; -import { callbacks } from '../../../../server/lib/callbacks'; -import { deleteMessage as deleteMessageFunc } from '../../../lib/server/functions/deleteMessage'; -import { sendMessage as sendMessageFunc } from '../../../lib/server/functions/sendMessage'; -import { updateMessage as updateMessageFunc } from '../../../lib/server/functions/updateMessage'; -import * as Mailer from '../../../mailer/server/api'; -import { settings } from '../../../settings/server'; - -const dnsResolveMx = util.promisify(dns.resolveMx); - -type OfflineMessageData = { - message: string; - name: string; - email: string; - department?: string; - host?: string; -}; - -export async function sendOfflineMessage(data: OfflineMessageData) { - if (!settings.get('Livechat_display_offline_form')) { - throw new Error('error-offline-form-disabled'); - } - - const { message, name, email, department, host } = data; - - if (!email) { - throw new Error('error-invalid-email'); - } - - const emailMessage = `${message}`.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1
$2'); - - let html = '

New livechat message

'; - if (host && host !== '') { - html = html.concat(`

Sent from: ${host}

`); - } - html = html.concat(` -

Visitor name: ${name}

-

Visitor email: ${email}

-

Message:
${emailMessage}

`); - - const fromEmail = settings.get('From_Email').match(/\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b/i); - - let from: string; - if (fromEmail) { - from = fromEmail[0]; - } else { - from = settings.get('From_Email'); - } - - if (settings.get('Livechat_validate_offline_email')) { - const emailDomain = email.substr(email.lastIndexOf('@') + 1); - - try { - await dnsResolveMx(emailDomain); - } catch (e) { - throw new Meteor.Error('error-invalid-email-address'); - } - } - - // TODO Block offline form if Livechat_offline_email is undefined - // (it does not make sense to have an offline form that does nothing) - // `this.sendEmail` will throw an error if the email is invalid - // thus this breaks livechat, since the "to" email is invalid, and that returns an [invalid email] error to the livechat client - let emailTo = settings.get('Livechat_offline_email'); - if (department && department !== '') { - const dep = await LivechatDepartment.findOneByIdOrName(department, { projection: { email: 1 } }); - if (dep) { - emailTo = dep.email || emailTo; - } - } - - const fromText = `${name} - ${email} <${from}>`; - const replyTo = `${name} <${email}>`; - const subject = `Livechat offline message from ${name}: ${`${emailMessage}`.substring(0, 20)}`; - await Mailer.send({ - to: emailTo, - from: fromText, - replyTo, - subject, - html, - }); - - setImmediate(() => { - void callbacks.run('livechat.offlineMessage', data); - }); -} - -export async function updateMessage({ guest, message }: { guest: ILivechatVisitor; message: AtLeast }) { - // TODO: Remove check - check(message, Match.ObjectIncluding({ _id: String })); - - const originalMessage = await Messages.findOneById>(message._id, { projection: { u: 1 } }); - if (!originalMessage?._id) { - return; - } - - // TODO: shouldn't this happen inside updateMessageFunc? - const editAllowed = settings.get('Message_AllowEditing'); - const editOwn = originalMessage.u && originalMessage.u._id === guest._id; - - if (!editAllowed || !editOwn) { - throw new Error('error-action-not-allowed'); - } - - // TODO: Apps sends an `any` object and apparently we just check for _id being present - // while updateMessage expects AtLeast - await updateMessageFunc(message, guest as unknown as IUser); - - return true; -} - -export async function deleteMessage({ guest, message }: { guest: ILivechatVisitor; message: IMessage }) { - const deleteAllowed = settings.get('Message_AllowDeleting'); - const editOwn = message.u && message.u._id === guest._id; - - if (!deleteAllowed || !editOwn) { - throw new Error('error-action-not-allowed'); - } - - // TODO: we shouldn't do this :( - await deleteMessageFunc(message, guest as unknown as IUser); - - return true; -} - -export async function sendMessage({ - guest, - message, - roomInfo, - agent, -}: { - guest: ILivechatVisitor; - message: ILivechatMessage; - roomInfo: IOmnichannelRoomInfo; - agent?: SelectedAgent; -}) { - const { room, newRoom } = await getRoom(guest, message, roomInfo, agent); - return { - ...(await sendMessageFunc(guest, { ...message, token: guest.token, ...(guest.name && { alias: guest.name }) }, room)), - newRoom, - showConnecting: showConnecting(), - }; -} diff --git a/apps/meteor/app/livechat/server/lib/rooms.ts b/apps/meteor/app/livechat/server/lib/rooms.ts deleted file mode 100644 index 90755eb5bf002..0000000000000 --- a/apps/meteor/app/livechat/server/lib/rooms.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { AppEvents, Apps } from '@rocket.chat/apps'; -import { Omnichannel } from '@rocket.chat/core-services'; -import type { - ILivechatVisitor, - IMessage, - IOmnichannelRoomInfo, - SelectedAgent, - IOmnichannelRoomExtraData, - IOmnichannelRoom, - TransferData, -} from '@rocket.chat/core-typings'; -import { isOmnichannelRoom } from '@rocket.chat/core-typings'; -import { - LivechatRooms, - LivechatContacts, - Messages, - LivechatCustomField, - LivechatInquiry, - Rooms, - Subscriptions, - Users, - ReadReceipts, - ReadReceiptsArchive, -} from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { normalizeTransferredByData } from './Helper'; -import { QueueManager } from './QueueManager'; -import { RoutingManager } from './RoutingManager'; -import { Visitors } from './Visitors'; -import { getRequiredDepartment } from './departmentsLib'; -import { checkDefaultAgentOnNewRoom } from './hooks'; -import { livechatLogger } from './logger'; -import { saveTransferHistory } from './transfer'; -import { trim } from '../../../../lib/utils/stringUtils'; -import { callbacks } from '../../../../server/lib/callbacks'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { - notifyOnLivechatInquiryChangedByRoom, - notifyOnSubscriptionChangedByRoomId, - notifyOnRoomChangedById, - notifyOnLivechatInquiryChanged, - notifyOnSubscriptionChanged, - notifyOnRoomChanged, -} from '../../../lib/server/lib/notifyListener'; -import { settings } from '../../../settings/server'; -import { i18n } from '../../../utils/lib/i18n'; - -export async function getRoom( - guest: ILivechatVisitor, - message: Pick, - roomInfo: IOmnichannelRoomInfo, - agent?: SelectedAgent, - extraData?: IOmnichannelRoomExtraData, -) { - if (!settings.get('Livechat_enabled')) { - throw new Meteor.Error('error-omnichannel-is-disabled'); - } - livechatLogger.debug({ msg: 'Attempting to find or create a room for visitor', visitorId: guest._id }); - const room = await LivechatRooms.findOneById(message.rid); - - if (room?.v._id && (await LivechatContacts.isChannelBlocked(Visitors.makeVisitorAssociation(room.v._id, room.source)))) { - throw new Error('error-contact-channel-blocked'); - } - - if (!room?.open) { - livechatLogger.debug({ msg: 'Last room for visitor closed. Creating new one', visitorId: guest._id }); - } - - if (!room?.open) { - return { - room: await createRoom({ visitor: guest, message: message.msg, roomInfo, agent, extraData }), - newRoom: true, - }; - } - - if (room.v.token !== guest.token) { - livechatLogger.debug({ msg: 'Visitor trying to access another visitor room', visitorId: guest._id }); - throw new Meteor.Error('cannot-access-room'); - } - - return { room, newRoom: false }; -} - -export async function createRoom({ - visitor, - message, - rid, - roomInfo, - agent, - extraData, -}: { - visitor: ILivechatVisitor; - message?: string; - rid?: string; - roomInfo: IOmnichannelRoomInfo; - agent?: SelectedAgent; - extraData?: IOmnichannelRoomExtraData; -}) { - if (!settings.get('Livechat_enabled')) { - throw new Meteor.Error('error-omnichannel-is-disabled'); - } - - if (await LivechatContacts.isChannelBlocked(Visitors.makeVisitorAssociation(visitor._id, roomInfo.source))) { - throw new Error('error-contact-channel-blocked'); - } - - const defaultAgent = await checkDefaultAgentOnNewRoom(agent, { - visitorId: visitor._id, - source: roomInfo.source, - }); - - // if no department selected verify if there is at least one active and pick the first - if (!defaultAgent && !visitor.department) { - const department = await getRequiredDepartment(); - livechatLogger.debug({ msg: 'No department or default agent selected for visitor', visitorId: visitor._id }); - - if (department) { - livechatLogger.debug({ msg: 'Assigning visitor to department', visitorId: visitor._id, departmentId: department._id }); - visitor.department = department._id; - } - } - - // delegate room creation to QueueManager - livechatLogger.debug({ msg: 'Calling QueueManager to request a room for visitor', visitorId: visitor._id }); - - const room = await QueueManager.requestRoom({ - guest: visitor, - message, - rid, - roomInfo, - agent: defaultAgent, - extraData, - }); - - livechatLogger.debug({ msg: 'Room obtained for visitor', visitorId: visitor._id, roomId: room._id }); - - await Messages.setRoomIdByToken(visitor.token, room._id); - - return room; -} - -export async function saveRoomInfo( - roomData: { - _id: string; - topic?: string; - tags?: string[]; - livechatData?: { [k: string]: string }; - // For priority and SLA, if the value is blank (ie ""), then system will remove the priority or SLA from the room - priorityId?: string; - slaId?: string; - }, - guestData?: { - _id: string; - name?: string; - email?: string; - phone?: string; - livechatData?: { [k: string]: string }; - }, - userId?: string, -) { - livechatLogger.debug({ msg: 'Saving room information', roomId: roomData._id }); - const { livechatData = {} } = roomData; - const customFields: Record = {}; - - if ((!userId || (await hasPermissionAsync(userId, 'edit-livechat-room-customfields'))) && Object.keys(livechatData).length) { - const fields = LivechatCustomField.findByScope('room'); - for await (const field of fields) { - if (!livechatData.hasOwnProperty(field._id)) { - continue; - } - const value = trim(livechatData[field._id]); - if (value !== '' && field.regexp !== undefined && field.regexp !== '') { - const regexp = new RegExp(field.regexp); - if (!regexp.test(value)) { - throw new Meteor.Error(i18n.t('error-invalid-custom-field-value', { field: field.label })); - } - } - customFields[field._id] = value; - } - roomData.livechatData = customFields; - livechatLogger.debug({ - msg: 'About to update custom fields on room', - roomId: roomData._id, - customFieldCount: Object.keys(customFields).length, - }); - } - - await LivechatRooms.saveRoomById(roomData); - - setImmediate(() => { - void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomSaved, roomData._id); - }); - - if (guestData?.name?.trim().length) { - const { _id: rid } = roomData; - const { name } = guestData; - - const responses = await Promise.all([ - Rooms.setFnameById(rid, name), - LivechatInquiry.setNameByRoomId(rid, name), - Subscriptions.updateDisplayNameByRoomId(rid, name), - ]); - - if (responses[1]?.modifiedCount) { - void notifyOnLivechatInquiryChangedByRoom(rid, 'updated', { name }); - } - - if (responses[2]?.modifiedCount) { - await notifyOnSubscriptionChangedByRoomId(rid); - } - } - - void notifyOnRoomChangedById(roomData._id); - - return true; -} - -export async function returnRoomAsInquiry(room: IOmnichannelRoom, departmentId?: string, overrideTransferData: Partial = {}) { - livechatLogger.debug({ msg: 'Transferring room to queue', scope: departmentId ? 'department' : undefined, room }); - if (!room.open) { - throw new Meteor.Error('room-closed', 'Room closed'); - } - - if (room.onHold) { - throw new Meteor.Error('error-room-onHold'); - } - - if (!(await Omnichannel.isWithinMACLimit(room))) { - throw new Meteor.Error('error-mac-limit-reached'); - } - - if (!room.servedBy) { - return false; - } - - const user = await Users.findOneById(room.servedBy._id); - if (!user?._id) { - throw new Meteor.Error('error-invalid-user'); - } - - const inquiry = await LivechatInquiry.findOne({ rid: room._id }); - if (!inquiry) { - return false; - } - - // update inquiry's last message with room's last message to correctly display in the queue - // because we stop updating the inquiry when it's been taken - if (room.lastMessage) { - await LivechatInquiry.setLastMessageById(inquiry._id, room.lastMessage); - } - - const transferredBy = normalizeTransferredByData(user, room); - livechatLogger.debug({ msg: 'Transferring room by user', roomId: room._id, transferredBy: transferredBy._id }); - const transferData = { scope: 'queue' as const, departmentId, transferredBy, ...overrideTransferData }; - try { - await saveTransferHistory(room, transferData); - await RoutingManager.unassignAgent(inquiry, departmentId); - } catch (err) { - livechatLogger.error({ err }); - throw new Meteor.Error('error-returning-inquiry'); - } - - callbacks.runAsync('livechat:afterReturnRoomAsInquiry', { room }); - - return true; -} - -export async function removeOmnichannelRoom(rid: string) { - livechatLogger.debug({ msg: 'Deleting room', roomId: rid }); - check(rid, String); - const room = await LivechatRooms.findOneById(rid); - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room'); - } - - if (!isOmnichannelRoom(room)) { - throw new Meteor.Error('error-this-is-not-a-livechat-room'); - } - - if (room.open) { - throw new Meteor.Error('error-room-is-not-closed'); - } - - const inquiry = await LivechatInquiry.findOneByRoomId(rid); - - const result = await Promise.allSettled([ - Messages.removeByRoomId(rid), - ReadReceipts.removeByRoomId(rid), - Subscriptions.removeByRoomId(rid, { - async onTrash(doc) { - void notifyOnSubscriptionChanged(doc, 'removed'); - }, - }), - LivechatInquiry.removeByRoomId(rid), - LivechatRooms.removeById(rid), - ReadReceiptsArchive.removeByRoomId(rid), - ]); - - if (result[3]?.status === 'fulfilled' && result[3].value?.deletedCount && inquiry) { - void notifyOnLivechatInquiryChanged(inquiry, 'removed'); - } - if (result[4]?.status === 'fulfilled' && result[4].value?.deletedCount) { - void notifyOnRoomChanged(room, 'removed'); - } - - for (const r of result) { - if (r.status === 'rejected') { - livechatLogger.error({ msg: 'Error removing room', roomId: rid, err: r.reason }); - throw new Meteor.Error('error-removing-room', 'Error removing room'); - } - } -} diff --git a/apps/meteor/app/livechat/server/lib/settings.ts b/apps/meteor/app/livechat/server/lib/settings.ts deleted file mode 100644 index f4a917b966d7a..0000000000000 --- a/apps/meteor/app/livechat/server/lib/settings.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { OmnichannelSortingMechanismSettingType } from '@rocket.chat/core-typings'; - -import { showConnecting } from './utils'; -import { settings } from '../../../settings/server'; - -export const getInquirySortMechanismSetting = (): OmnichannelSortingMechanismSettingType => - settings.get('Omnichannel_sorting_mechanism') || OmnichannelSortingMechanismSettingType.Timestamp; - -export async function getInitSettings() { - const validSettings = [ - 'Livechat_title', - 'Livechat_title_color', - 'Livechat_enable_message_character_limit', - 'Livechat_message_character_limit', - 'Message_MaxAllowedSize', - 'Livechat_enabled', - 'Livechat_registration_form', - 'Livechat_allow_switching_departments', - 'Livechat_offline_title', - 'Livechat_offline_title_color', - 'Livechat_offline_message', - 'Livechat_offline_success_message', - 'Livechat_offline_form_unavailable', - 'Livechat_display_offline_form', - 'Omnichannel_call_provider', - 'Language', - 'Livechat_enable_transcript', - 'Livechat_transcript_message', - 'Livechat_fileupload_enabled', - 'FileUpload_Enabled', - 'Livechat_conversation_finished_message', - 'Livechat_conversation_finished_text', - 'Livechat_name_field_registration_form', - 'Livechat_email_field_registration_form', - 'Livechat_registration_form_message', - 'Livechat_force_accept_data_processing_consent', - 'Livechat_data_processing_consent_text', - 'Livechat_show_agent_info', - 'Livechat_clear_local_storage_when_chat_ended', - 'Livechat_history_monitor_type', - 'Livechat_hide_system_messages', - 'Livechat_widget_position', - 'Livechat_background', - 'Assets_livechat_widget_logo', - 'Livechat_hide_watermark', - 'Omnichannel_allow_visitors_to_close_conversation', - 'Livechat_hide_expand_chat', - ] as const; - - type SettingTypes = (typeof validSettings)[number] | 'Livechat_Show_Connecting'; - - const rcSettings = validSettings.reduce>((acc, setting) => { - acc[setting] = settings.get(setting); - return acc; - }, {} as any); - - rcSettings.Livechat_Show_Connecting = showConnecting(); - - return rcSettings; -} diff --git a/apps/meteor/app/livechat/server/lib/transfer.ts b/apps/meteor/app/livechat/server/lib/transfer.ts deleted file mode 100644 index 61d8af806c4ca..0000000000000 --- a/apps/meteor/app/livechat/server/lib/transfer.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Message } from '@rocket.chat/core-services'; -import type { ILivechatDepartment, ILivechatVisitor, IOmnichannelRoom, TransferData } from '@rocket.chat/core-typings'; -import { Users, LivechatRooms, LivechatVisitors, LivechatDepartment } from '@rocket.chat/models'; - -import { normalizeTransferredByData } from './Helper'; -import { RoutingManager } from './RoutingManager'; -import { livechatLogger } from './logger'; - -export async function saveTransferHistory(room: IOmnichannelRoom, transferData: TransferData) { - const { departmentId: previousDepartment } = room; - const { department: nextDepartment, transferredBy, transferredTo, scope, comment } = transferData; - - check( - transferredBy, - Match.ObjectIncluding({ - _id: String, - username: String, - name: Match.Maybe(String), - userType: String, - }), - ); - - const { _id, username } = transferredBy; - const scopeData = scope || (nextDepartment ? 'department' : 'agent'); - livechatLogger.info({ msg: 'Storing new chat transfer', roomId: room._id, transferredBy: _id, scope: scopeData }); - - const transferMessage = { - ...(transferData.transferredBy.userType === 'visitor' && { token: room.v.token }), - transferData: { - transferredBy, - ts: new Date(), - scope: scopeData, - comment, - ...(previousDepartment && { previousDepartment }), - ...(nextDepartment && { nextDepartment }), - ...(transferredTo && { transferredTo }), - }, - }; - - await Message.saveSystemMessageAndNotifyUser('livechat_transfer_history', room._id, '', { _id, username }, transferMessage); -} - -export async function forwardOpenChats(userId: string) { - livechatLogger.debug({ msg: 'Transferring open chats for user', userId }); - const user = await Users.findOneById(userId); - if (!user) { - throw new Error('error-invalid-user'); - } - - const { _id, username, name } = user; - for await (const room of LivechatRooms.findOpenByAgent(userId)) { - const guest = await LivechatVisitors.findOneEnabledById(room.v._id); - if (!guest) { - continue; - } - - const transferredBy = normalizeTransferredByData({ _id, username, name }, room); - await transfer(room, guest, { - transferredBy, - departmentId: guest.department, - }); - } -} - -export async function transfer(room: IOmnichannelRoom, guest: ILivechatVisitor, transferData: TransferData) { - livechatLogger.debug({ msg: 'Transferring room', roomId: room._id, transferredBy: transferData?.transferredBy?._id }); - if (room.onHold) { - throw new Error('error-room-onHold'); - } - - if (transferData.departmentId) { - const department = await LivechatDepartment.findOneById>(transferData.departmentId, { - projection: { name: 1 }, - }); - if (!department) { - throw new Error('error-invalid-department'); - } - - transferData.department = department; - livechatLogger.debug({ msg: 'Transferring room to department', roomId: room._id, departmentId: transferData.department?._id }); - } - - return RoutingManager.transferRoom(room, guest, transferData); -} diff --git a/apps/meteor/app/livechat/server/lib/utils.ts b/apps/meteor/app/livechat/server/lib/utils.ts deleted file mode 100644 index 45e5d58e1dc4b..0000000000000 --- a/apps/meteor/app/livechat/server/lib/utils.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; -import type { ILivechatAgent, IUser } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; -import type { Filter } from 'mongodb'; - -import { RoutingManager } from './RoutingManager'; -import type { AKeyOf } from './localTypes'; -import { callbacks } from '../../../../server/lib/callbacks'; -import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; -import { businessHourManager } from '../business-hour'; - -export function showConnecting() { - return RoutingManager.getConfig()?.showConnecting || false; -} - -export async function setUserStatusLivechat(userId: string, status: ILivechatAgentStatus) { - const user = await Users.setLivechatStatus(userId, status); - // TODO: shouldnt this callback run if the modified count is > 0 too? - callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); - - if (user.modifiedCount > 0) { - void notifyOnUserChange({ - id: userId, - clientAction: 'updated', - diff: { - statusLivechat: status, - livechatStatusSystemModified: false, - }, - }); - } - - return user; -} - -export async function setUserStatusLivechatIf( - userId: string, - status: ILivechatAgentStatus, - condition?: Filter, - fields?: AKeyOf, -) { - const result = await Users.setLivechatStatusIf(userId, status, condition, fields); - - if (result.modifiedCount > 0) { - void notifyOnUserChange({ - id: userId, - clientAction: 'updated', - diff: { ...fields, statusLivechat: status }, - }); - } - - // TODO: shouldnt this callback run if the modified count is > 0 too? - callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); - return result; -} - -export async function allowAgentChangeServiceStatus(statusLivechat: ILivechatAgentStatus, agentId: string) { - if (statusLivechat !== ILivechatAgentStatus.AVAILABLE) { - return true; - } - - return businessHourManager.allowAgentChangeServiceStatus(agentId); -} diff --git a/apps/meteor/app/livechat/server/lib/webhooks.ts b/apps/meteor/app/livechat/server/lib/webhooks.ts deleted file mode 100644 index de49a6bb0cae7..0000000000000 --- a/apps/meteor/app/livechat/server/lib/webhooks.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { serverFetch as fetch } from '@rocket.chat/server-fetch'; -import type { Response } from '@rocket.chat/server-fetch'; - -import { webhooksLogger } from './logger'; -import { metrics } from '../../../metrics/server'; -import { settings } from '../../../settings/server'; - -const isRetryable = (status: number): boolean => status >= 500 || status === 429; - -export async function sendRequest( - postData: { - type: string; - [key: string]: any; - }, - attempts = 5, - cb?: (response: Response) => Promise, -) { - const timeout = settings.get('Livechat_http_timeout'); - const secretToken = settings.get('Livechat_secret_token'); - const webhookUrl = settings.get('Livechat_webhookUrl'); - try { - webhooksLogger.debug({ msg: 'Sending webhook request', postData }); - const result = await fetch(webhookUrl, { - method: 'POST', - headers: { - ...(secretToken && { 'X-RocketChat-Livechat-Token': secretToken }), - }, - body: postData, - timeout, - // SECURITY: Webhooks can only be configured by users with enough privileges. It's ok to disable this check here. - ignoreSsrfValidation: true, - size: 10 * 1024 * 1024, - }); - - if (result.status === 200) { - metrics.totalLivechatWebhooksSuccess.inc(); - metrics.totalLivechatWebhooksSuccessTotal.inc(); - await cb?.(result); - return result; - } - - if (!isRetryable(result.status)) { - webhooksLogger.error({ - msg: 'Non-retryable error response from webhook', - webhookUrl, - status: result.status, - response: await result.text(), - }); - metrics.totalLivechatWebhooksFailures.inc(); - metrics.totalLivechatWebhooksFailuresTotal.inc(); - return; - } - - metrics.totalLivechatWebhooksFailures.inc(); - metrics.totalLivechatWebhooksFailuresTotal.inc(); - throw new Error(await result.text()); - } catch (err) { - const retryAfter = timeout * 4; - webhooksLogger.debug({ - msg: 'Error response on retry', - attempt: 6 - attempts, - err, - newAttemptAfterSeconds: retryAfter / 1000, - webhookUrl, - }); - const remainingAttempts = attempts - 1; - // try 5 times after 20 seconds each - if (!remainingAttempts) { - webhooksLogger.error({ msg: 'Omnichannel webhook call failed. Max attempts reached' }); - return; - } - - setTimeout(async () => { - await sendRequest(postData, remainingAttempts, cb); - }, retryAfter); - } -} diff --git a/apps/meteor/app/livechat/server/livechat.ts b/apps/meteor/app/livechat/server/livechat.ts deleted file mode 100644 index 70dd67257650d..0000000000000 --- a/apps/meteor/app/livechat/server/livechat.ts +++ /dev/null @@ -1,73 +0,0 @@ -import url from 'node:url'; - -import jsdom from 'jsdom'; -import mem from 'mem'; -import { WebApp } from 'meteor/webapp'; - -import { settings } from '../../settings/server'; -import { addServerUrlToIndex } from '../lib/Assets'; - -const indexHtmlWithServerURL = await Assets.getTextAsync('livechat/index.html'); - -function parseExtraAttributes(widgetData: string): string { - const liveChatAdditionalScripts = settings.get('Livechat_AdditionalWidgetScripts'); - const additionalClass = settings.get('Livechat_WidgetLayoutClasses'); - - if (liveChatAdditionalScripts == null || additionalClass == null) { - return widgetData; - } - - const domParser = new jsdom.JSDOM(widgetData); - const doc = domParser.window.document; - const head = doc.querySelector('head'); - const body = doc.querySelector('body'); - - liveChatAdditionalScripts.split(',').forEach((script) => { - const scriptElement = doc.createElement('script'); - scriptElement.src = script; - body?.appendChild(scriptElement); - }); - - additionalClass.split(',').forEach((css) => { - const linkElement = doc.createElement('link'); - linkElement.rel = 'stylesheet'; - linkElement.href = css; - head?.appendChild(linkElement); - }); - - return doc.documentElement.innerHTML; -} - -const memoizedParseExtraAttributes = mem(parseExtraAttributes, { - maxAge: process.env.TEST_MODE === 'true' || process.env.TEST_MODE === 'api' ? 1 : 60000, -}); - -WebApp.connectHandlers.use('/livechat', (req, res, next) => { - if (!req.url) { - return next(); - } - - const reqUrl = url.parse(req.url); - if (reqUrl.pathname !== '/') { - return next(); - } - - res.setHeader('content-type', 'text/html; charset=utf-8'); - const domainWhiteListSetting = settings.get('Livechat_AllowedDomainsList'); - let domainWhiteList = []; - if (req.headers.referer && domainWhiteListSetting.trim()) { - domainWhiteList = domainWhiteListSetting.split(',').map((domain) => domain.trim()); - const referer = url.parse(req.headers.referer); - if (referer.host && !domainWhiteList.includes(referer.host)) { - res.setHeader('Content-Security-Policy', "frame-ancestors 'none'"); - return next(); - } - res.setHeader('Content-Security-Policy', `frame-ancestors ${referer.protocol}//${referer.host}`); - } else { - // TODO need to remove inline scripts from this route to be able to enable CSP here as well - res.removeHeader('Content-Security-Policy'); - } - - res.write(memoizedParseExtraAttributes(addServerUrlToIndex(indexHtmlWithServerURL || ''))); - res.end(); -}); diff --git a/apps/meteor/app/livechat/server/methods/saveBusinessHour.ts b/apps/meteor/app/livechat/server/methods/saveBusinessHour.ts deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/apps/meteor/app/livechat/server/startup.ts b/apps/meteor/app/livechat/server/startup.ts deleted file mode 100644 index 1ba15a5e0baeb..0000000000000 --- a/apps/meteor/app/livechat/server/startup.ts +++ /dev/null @@ -1,148 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { ILivechatAgentStatus, isOmnichannelRoom } from '@rocket.chat/core-typings'; -import { Logger } from '@rocket.chat/logger'; -import { LivechatContacts, LivechatRooms } from '@rocket.chat/models'; -import { registerGuest } from '@rocket.chat/omni-core'; -import { validateEmail, wrapExceptions } from '@rocket.chat/tools'; -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; - -import { businessHourManager } from './business-hour'; -import { createDefaultBusinessHourIfNotExists } from './business-hour/Helper'; -import { setUserStatusLivechatIf } from './lib/utils'; -import { LivechatAgentActivityMonitor } from './statistics/LivechatAgentActivityMonitor'; -import { callbacks } from '../../../server/lib/callbacks'; -import { beforeLeaveRoomCallback } from '../../../server/lib/callbacks/beforeLeaveRoomCallback'; -import { i18n } from '../../../server/lib/i18n'; -import { roomCoordinator } from '../../../server/lib/rooms/roomCoordinator'; -import { maybeMigrateLivechatRoom } from '../../api/server/lib/maybeMigrateLivechatRoom'; -import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; -import { notifyOnUserChange } from '../../lib/server/lib/notifyListener'; -import { settings } from '../../settings/server'; -import './roomAccessValidator.internalService'; -import { ContactMerger, type FieldAndValue } from './lib/contacts/ContactMerger'; - -const logger = new Logger('LivechatStartup'); - -// TODO this patch is temporary because `ContactMerger` still a lot of dependencies, so it is not suitable to be moved to omni-core package -// TODO add tests covering the ContactMerger usage -registerGuest.patch(async (originalFn, newData, options) => { - const visitor = await originalFn(newData, options); - if (!visitor) { - return null; - } - - const { name, phone, email, username } = newData; - - const validatedEmail = - email && - wrapExceptions(() => { - const trimmedEmail = email.trim().toLowerCase(); - validateEmail(trimmedEmail); - return trimmedEmail; - }).suppress(); - - const fields = [ - { type: 'name', value: name }, - { type: 'phone', value: phone?.number }, - { type: 'email', value: validatedEmail }, - { type: 'username', value: username || visitor.username }, - ].filter((field) => Boolean(field.value)) as FieldAndValue[]; - - if (!fields.length) { - return null; - } - - // If a visitor was updated who already had contacts, load up the contacts and update that information as well - const contacts = await LivechatContacts.findAllByVisitorId(visitor._id).toArray(); - for await (const contact of contacts) { - await ContactMerger.mergeFieldsIntoContact({ - fields, - contact, - conflictHandlingMode: contact.unknown ? 'overwrite' : 'conflict', - }); - } - - return visitor; -}); - -Meteor.startup(async () => { - roomCoordinator.setRoomFind('l', async (id) => maybeMigrateLivechatRoom(await LivechatRooms.findOneById(id))); - - beforeLeaveRoomCallback.add( - (user, room) => { - if (!isOmnichannelRoom(room)) { - return; - } - throw new Meteor.Error( - i18n.t('You_cant_leave_a_livechat_room_Please_use_the_close_button', { - lng: user.language || settings.get('Language') || 'en', - }), - ); - }, - callbacks.priority.LOW, - 'cant-leave-omnichannel-room', - ); - - callbacks.add( - 'beforeJoinRoom', - async (user, room) => { - if (isOmnichannelRoom(room) && !(await hasPermissionAsync(user._id, 'view-l-room'))) { - throw new Meteor.Error('error-user-is-not-agent', 'User is not an Omnichannel Agent', { - method: 'beforeJoinRoom', - }); - } - - return user; - }, - callbacks.priority.LOW, - 'cant-join-omnichannel-room', - ); - - const monitor = new LivechatAgentActivityMonitor(); - - settings.watch('Troubleshoot_Disable_Livechat_Activity_Monitor', async (value) => { - if (value) { - return monitor.stop(); - } - - await monitor.start(); - }); - await createDefaultBusinessHourIfNotExists(); - - settings.watch( - 'Livechat_enable_business_hours', - async (value) => { - logger.debug({ msg: 'Starting business hour manager', enabled: value }); - if (value) { - await businessHourManager.startManager(); - return; - } - await businessHourManager.stopManager(); - }, - process.env.TEST_MODE === 'true' || process.env.TEST_MODE === 'api' - ? { - debounce: 10, - } - : undefined, - ); - - // Remove when accounts.onLogout is async - Accounts.onLogout(({ user }: { user?: IUser }) => { - if (!user?.roles?.includes('livechat-agent') || user?.roles?.includes('bot')) { - return; - } - - void setUserStatusLivechatIf(user._id, ILivechatAgentStatus.NOT_AVAILABLE, {}, { livechatStatusSystemModified: true }).catch(); - - // TODO: Shouldn't this notifier be the same as the one inside setUserStatusLivechatIf? - void notifyOnUserChange({ - id: user._id, - clientAction: 'updated', - diff: { - statusLivechat: ILivechatAgentStatus.NOT_AVAILABLE, - livechatStatusSystemModified: true, - }, - }); - }); -}); diff --git a/apps/meteor/app/logger/README.md b/apps/meteor/app/logger/README.md deleted file mode 100644 index b812f649c7cb7..0000000000000 --- a/apps/meteor/app/logger/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# Logger - -Constructor -```javascript -new Logger(name, options); -``` - -options -* **sections**: An object of sections -* **methods**: An object of new methods - -Example -```javascript -const logger = new Logger('LDAP', { - sections: { - connection: 'Connection', - bind: 'Bind', - search: 'Search', - auth: 'Auth' - } -}); -``` - -Usage -```javascript -logger.info('connecting'); -// LDAP โž” info connecting - -logger.connection.info('connecting'); -// LDAP โž” Connection.info connecting -``` - -Sections have all avaliable methods methods, the default methods are: -```javascript -debug: - name: 'debug' - color: 'blue' - level: 2 -log: - name: 'info' - color: 'blue' - level: 1 -info: - name: 'info' - color: 'blue' - level: 1 -success: - name: 'info' - color: 'green' - level: 1 -warn: - name: 'warn' - color: 'magenta' - level: 1 -error: - name: 'error' - color: 'red' - level: 0 -``` - -The method **error** will always log the file and line of method execution - - -# LoggerManager (singleton) - -You can enable, disable, or set the log level for all logs here. -You can show the origin package and file/line from all logs here. - -```javascript -LoggerManager.enabled = false; -LoggerManager.showPackage = false; -LoggerManager.showFileAndLine = false; -LoggerManager.logLevel = 0; -``` - -The **LoggerManager** starts disabled, you should enable in some point, if you want to print all logs queued while disabled use: -```javascript -LoggerManager.enable(true); -``` diff --git a/apps/meteor/app/mailer/server/api.ts b/apps/meteor/app/mailer/server/api.ts deleted file mode 100644 index 8fc98f884cb5c..0000000000000 --- a/apps/meteor/app/mailer/server/api.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { AppEvents, Apps } from '@rocket.chat/apps'; -import type { ISetting } from '@rocket.chat/core-typings'; -import { Settings } from '@rocket.chat/models'; -import { escapeHTML } from '@rocket.chat/string-helpers'; -import { validateEmail } from '@rocket.chat/tools'; -import juice from 'juice'; -import { Email } from 'meteor/email'; -import { Meteor } from 'meteor/meteor'; -import { stripHtml } from 'string-strip-html'; -import _ from 'underscore'; - -import { replaceVariables } from './replaceVariables'; -import { strLeft, strRightBack } from '../../../lib/utils/stringUtils'; -import { i18n } from '../../../server/lib/i18n'; -import { notifyOnSettingChanged } from '../../lib/server/lib/notifyListener'; -import { settings } from '../../settings/server'; - -let contentHeader: string | undefined; -let contentFooter: string | undefined; -let body: string | undefined; - -// define server language for email translations -// @TODO: change TAPi18n.__ function to use the server language by default -let lng = 'en'; -settings.watch('Language', (value) => { - lng = value || 'en'; -}); - -export const replacekey = (str: string, key: string, value = ''): string => - str.replace(new RegExp(`(\\[${key}\\]|__${key}__)`, 'igm'), value); - -export const translate = (str: string): string => replaceVariables(str, (_match, key) => i18n.t(key, { lng })); - -export const replace = (str: string, data: { [key: string]: unknown } = {}): string => { - if (!str) { - return ''; - } - - const options = { - Site_Name: settings.get('Site_Name'), - Site_URL: settings.get('Site_Url'), - Site_URL_Slash: settings.get('Site_Url')?.replace(/\/?$/, '/'), - ...(data.name - ? { - fname: strLeft(String(data.name), ' '), - lname: strRightBack(String(data.name), ' '), - } - : {}), - ...data, - }; - - return Object.entries(options).reduce((ret, [key, value]) => replacekey(ret, key, value), translate(str)); -}; - -const nonEscapeKeys = ['room_path']; - -export const replaceEscaped = (str: string, data: { [key: string]: unknown } = {}): string => { - const siteName = settings.get('Site_Name'); - const siteUrl = settings.get('Site_Url'); - - return replace(str, { - Site_Name: siteName ? escapeHTML(siteName) : undefined, - Site_Url: siteUrl ? escapeHTML(siteUrl) : undefined, - ...Object.entries(data).reduce<{ [key: string]: string }>((ret, [key, value]) => { - if (value !== undefined && value !== null) { - ret[key] = nonEscapeKeys.includes(key) ? String(value) : escapeHTML(String(value)); - } - return ret; - }, {}), - }); -}; - -export const wrap = (html: string, data: { [key: string]: unknown } = {}): string => { - if (settings.get('email_plain_text_only')) { - return replace(html, data); - } - - if (!body) { - throw new Error('error-email-body-not-initialized'); - } - - return replaceEscaped(body.replace('{{body}}', html), data); -}; - -export const inlinecss = (html: string): string => { - const css = settings.get('email_style'); - return css ? juice.inlineContent(html, css) : html; -}; - -export const getTemplate = (template: ISetting['_id'], fn: (html: string) => void, escape = true): void => { - let html = ''; - - settings.watch(template, (value) => { - html = value || ''; - fn(escape ? inlinecss(html) : html); - }); - - settings.watch('email_style', () => { - fn(escape ? inlinecss(html) : html); - }); -}; - -export const getTemplateWrapped = (template: ISetting['_id'], fn: (html: string) => void): void => { - let html = ''; - const wrapInlineCSS = _.debounce(() => fn(wrap(inlinecss(html))), 100); - - settings.watch('Email_Header', () => html && wrapInlineCSS()); - settings.watch('Email_Footer', () => html && wrapInlineCSS()); - settings.watch('email_style', () => html && wrapInlineCSS()); - settings.watch(template, (value) => { - html = value || ''; - return html && wrapInlineCSS(); - }); -}; - -settings.watchMultiple(['Email_Header', 'Email_Footer'], () => { - getTemplate( - 'Email_Header', - (value) => { - contentHeader = replace(value || ''); - body = inlinecss(`${contentHeader} {{body}} ${contentFooter}`); - }, - false, - ); - - getTemplate( - 'Email_Footer', - (value) => { - contentFooter = replace(value || ''); - body = inlinecss(`${contentHeader} {{body}} ${contentFooter}`); - }, - false, - ); - - body = inlinecss(`${contentHeader} {{body}} ${contentFooter}`); -}); - -export const checkAddressFormat = (adresses: string | string[]): boolean => - ([] as string[]).concat(adresses).every((address) => validateEmail(address)); - -export const sendNoWrap = async ({ - to, - from, - replyTo, - subject, - html, - text, - headers, -}: { - to: string | string[]; - from: string; - replyTo?: string; - subject: string; - html?: string; - text?: string; - headers?: string; -}) => { - if (!checkAddressFormat(to)) { - throw new Meteor.Error('invalid email'); - } - - if (!text) { - text = html ? stripHtml(html).result : undefined; - } - - if (settings.get('email_plain_text_only')) { - html = undefined; - } - - const value = await Settings.incrementValueById('Triggered_Emails_Count', 1, { returnDocument: 'after' }); - if (value) { - void notifyOnSettingChanged(value); - } - - const email = { to, from, replyTo, subject, html, text, headers }; - - const eventResult = await Apps.self?.triggerEvent(AppEvents.IPreEmailSent, { email }); - - setImmediate(() => Email.sendAsync(eventResult || email).catch((e) => console.error(e))); -}; - -export const send = async ({ - to, - from, - replyTo, - subject, - html, - text, - data, - headers, -}: { - to: string | string[]; - from: string; - replyTo?: string; - subject: string; - html?: string; - text?: string; - headers?: string; - data?: { [key: string]: unknown }; -}): Promise => - sendNoWrap({ - to, - from, - replyTo, - subject: replace(subject, data), - text: (text && replace(text, data)) || (html && stripHtml(replace(html, data)).result) || undefined, - html: html ? wrap(html, data) : undefined, - headers, - }); - -// Needed because of https://github.com/microsoft/TypeScript/issues/36931 -type Assert = (input: string, func: string) => asserts input; -export const checkAddressFormatAndThrow: Assert = (from: string, func: string): asserts from => { - if (checkAddressFormat(from)) { - return; - } - - throw new Meteor.Error('error-invalid-from-address', 'Invalid from address', { - function: func, - }); -}; - -export const getHeader = (): string | undefined => contentHeader; - -export const getFooter = (): string | undefined => contentFooter; diff --git a/apps/meteor/app/markdown/server/index.ts b/apps/meteor/app/markdown/server/index.ts deleted file mode 100644 index 4642c5fcbe9a1..0000000000000 --- a/apps/meteor/app/markdown/server/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { callbacks } from '../../../server/lib/callbacks'; -import { createMarkdownMessageRenderer, createMarkdownNotificationRenderer } from '../lib/markdown'; - -export { Markdown } from '../lib/markdown'; - -Meteor.startup(() => { - Tracker.autorun(() => { - const options = { - rootUrl: Meteor.absoluteUrl(), - }; - - const renderMessage = createMarkdownMessageRenderer(options); - callbacks.add('renderMessage', renderMessage, callbacks.priority.HIGH, 'markdown'); - }); - - const renderNotification = createMarkdownNotificationRenderer(); - callbacks.add('renderNotification', renderNotification, callbacks.priority.HIGH, 'filter-markdown'); -}); diff --git a/apps/meteor/app/mentions/server/index.ts b/apps/meteor/app/mentions/server/index.ts deleted file mode 100644 index b16d62185a64e..0000000000000 --- a/apps/meteor/app/mentions/server/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './getMentionedTeamMembers'; -import './methods/getUserMentionsByChannel'; diff --git a/apps/meteor/app/message-mark-as-unread/server/index.ts b/apps/meteor/app/message-mark-as-unread/server/index.ts deleted file mode 100644 index d9ad8ff7357a0..0000000000000 --- a/apps/meteor/app/message-mark-as-unread/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './unreadMessages'; diff --git a/apps/meteor/app/message-pin/server/index.ts b/apps/meteor/app/message-pin/server/index.ts deleted file mode 100644 index 19a4497a35322..0000000000000 --- a/apps/meteor/app/message-pin/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './pinMessage'; diff --git a/apps/meteor/app/message-star/server/index.ts b/apps/meteor/app/message-star/server/index.ts deleted file mode 100644 index cff8c3818b0e4..0000000000000 --- a/apps/meteor/app/message-star/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './starMessage'; diff --git a/apps/meteor/app/meteor-accounts-saml/server/definition/ISAMLAssertion.ts b/apps/meteor/app/meteor-accounts-saml/server/definition/ISAMLAssertion.ts deleted file mode 100644 index 1744afe7b3800..0000000000000 --- a/apps/meteor/app/meteor-accounts-saml/server/definition/ISAMLAssertion.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ISAMLAssertion { - assertion: Element | Document; - xml: string; -} diff --git a/apps/meteor/app/meteor-accounts-saml/server/index.ts b/apps/meteor/app/meteor-accounts-saml/server/index.ts deleted file mode 100644 index 8da4b6cc9c9b9..0000000000000 --- a/apps/meteor/app/meteor-accounts-saml/server/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import './startup'; -import './loginHandler'; -import './listener'; -import './methods/samlLogout'; -import './methods/addSamlService'; diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts deleted file mode 100644 index 0316045acb5c8..0000000000000 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts +++ /dev/null @@ -1,369 +0,0 @@ -import type { SAMLConfiguration } from '@rocket.chat/core-typings'; -import { LoginServiceConfiguration } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { SAMLUtils } from './Utils'; -import { - defaultAuthnContextTemplate, - defaultAuthRequestTemplate, - defaultLogoutResponseTemplate, - defaultLogoutRequestTemplate, - defaultNameIDTemplate, - defaultIdentifierFormat, - defaultAuthnContext, - defaultMetadataTemplate, - defaultMetadataCertificateTemplate, -} from './constants'; -import { SystemLogger } from '../../../../server/lib/logger/system'; -import { - notifyOnLoginServiceConfigurationChanged, - notifyOnLoginServiceConfigurationChangedByService, -} from '../../../lib/server/lib/notifyListener'; -import { settings, settingsRegistry } from '../../../settings/server'; -import type { IServiceProviderOptions } from '../definition/IServiceProviderOptions'; - -const getSamlConfigs = function (service: string): SAMLConfiguration { - const configs: SAMLConfiguration = { - buttonLabelText: settings.get(`${service}_button_label_text`), - buttonLabelColor: settings.get(`${service}_button_label_color`), - buttonColor: settings.get(`${service}_button_color`), - clientConfig: { - provider: settings.get(`${service}_provider`), - }, - entryPoint: settings.get(`${service}_entry_point`), - idpSLORedirectURL: settings.get(`${service}_idp_slo_redirect_url`), - usernameNormalize: settings.get(`${service}_username_normalize`), - immutableProperty: settings.get(`${service}_immutable_property`), - generateUsername: settings.get(`${service}_generate_username`), - debug: settings.get(`${service}_debug`), - nameOverwrite: settings.get(`${service}_name_overwrite`), - mailOverwrite: settings.get(`${service}_mail_overwrite`), - issuer: settings.get(`${service}_issuer`), - logoutBehaviour: settings.get(`${service}_logout_behaviour`), - defaultUserRole: settings.get(`${service}_default_user_role`), - secret: { - privateKey: settings.get(`${service}_private_key`), - publicCert: settings.get(`${service}_public_cert`), - // People often overlook the instruction to remove the header and footer of the certificate on this specific setting, so let's do it for them. - cert: SAMLUtils.normalizeCert((settings.get(`${service}_cert`) as string) || ''), - algorithm: settings.get(`${service}_signature_algorithm`) || 'SHA1', - }, - signatureValidationType: settings.get(`${service}_signature_validation_type`), - validateLogoutRequestSignature: settings.get(`${service}_validate_logout_request_signature`), - validateLogoutResponseSignature: settings.get(`${service}_validate_logout_response_signature`), - userDataFieldMap: settings.get(`${service}_user_data_fieldmap`), - allowedClockDrift: settings.get(`${service}_allowed_clock_drift`), - customAuthnContext: defaultAuthnContext, - authnContextComparison: 'exact', - identifierFormat: defaultIdentifierFormat, - nameIDPolicyTemplate: defaultNameIDTemplate, - authnContextTemplate: defaultAuthnContextTemplate, - authRequestTemplate: defaultAuthRequestTemplate, - logoutResponseTemplate: defaultLogoutResponseTemplate, - logoutRequestTemplate: defaultLogoutRequestTemplate, - metadataCertificateTemplate: defaultMetadataCertificateTemplate, - metadataTemplate: defaultMetadataTemplate, - channelsAttributeUpdate: settings.get(`${service}_channels_update`), - includePrivateChannelsInUpdate: settings.get(`${service}_include_private_channels_update`), - }; - - SAMLUtils.events.emit('loadConfigs', service, configs); - return configs; -}; - -const isValidConfiguration = function (key: string, samlConfigs: SAMLConfiguration): boolean { - const needsCert = samlConfigs.signatureValidationType !== 'None'; - - if (!samlConfigs.secret.cert && needsCert) { - SAMLUtils.log({ msg: 'SAML Configuration missing Custom Certificate setting', key }); - return false; - } - - return true; -}; - -const configureSamlService = function (samlConfigs: Record): IServiceProviderOptions { - let privateCert = null; - let privateKey = null; - const rawCustomAuthnContext = samlConfigs.customAuthnContext; - const normalizedCustomAuthnContext = typeof rawCustomAuthnContext === 'string' ? rawCustomAuthnContext.trim() : rawCustomAuthnContext; - const customAuthnContext = typeof normalizedCustomAuthnContext === 'string' ? normalizedCustomAuthnContext : defaultAuthnContext; - - if (samlConfigs.secret.privateKey && samlConfigs.secret.publicCert) { - privateKey = samlConfigs.secret.privateKey; - privateCert = samlConfigs.secret.publicCert; - } else if (samlConfigs.secret.privateKey || samlConfigs.secret.publicCert) { - SAMLUtils.error('SAML Service: You must specify both cert and key files.'); - } - - SAMLUtils.updateGlobalSettings(samlConfigs); - - return { - provider: samlConfigs.clientConfig.provider, - entryPoint: samlConfigs.entryPoint, - idpSLORedirectURL: samlConfigs.idpSLORedirectURL, - issuer: samlConfigs.issuer, - cert: samlConfigs.secret.cert, - privateCert, - privateKey, - signatureAlgorithm: samlConfigs.secret.algorithm, - customAuthnContext, - authnContextComparison: samlConfigs.authnContextComparison, - defaultUserRole: samlConfigs.defaultUserRole, - allowedClockDrift: parseInt(samlConfigs.allowedClockDrift) || 0, - signatureValidationType: samlConfigs.signatureValidationType, - validateLogoutRequestSignature: samlConfigs.validateLogoutRequestSignature, - validateLogoutResponseSignature: samlConfigs.validateLogoutResponseSignature, - identifierFormat: samlConfigs.identifierFormat, - nameIDPolicyTemplate: samlConfigs.nameIDPolicyTemplate, - authnContextTemplate: samlConfigs.authnContextTemplate, - authRequestTemplate: samlConfigs.authRequestTemplate, - logoutResponseTemplate: samlConfigs.logoutResponseTemplate, - logoutRequestTemplate: samlConfigs.logoutRequestTemplate, - metadataCertificateTemplate: samlConfigs.metadataCertificateTemplate, - metadataTemplate: samlConfigs.metadataTemplate, - callbackUrl: Meteor.absoluteUrl(`_saml/validate/${samlConfigs.clientConfig.provider}`), - }; -}; - -export const loadSamlServiceProviders = async function (): Promise { - const serviceName = 'saml'; - const services = settings.getByRegexp(/^(SAML_Custom_)[a-z]+$/i); - - if (!services) { - return SAMLUtils.setServiceProvidersList([]); - } - - const providers = ( - await Promise.all( - services.map(async ([key, value]) => { - if (value === true) { - const samlConfigs = getSamlConfigs(key); - - if (isValidConfiguration(key, samlConfigs)) { - SAMLUtils.log({ msg: 'Loading SAML Provider', key }); - await LoginServiceConfiguration.createOrUpdateService(serviceName, samlConfigs); - void notifyOnLoginServiceConfigurationChangedByService(serviceName); - return configureSamlService(samlConfigs); - } - - SAMLUtils.logger?.warn({ msg: 'SAML Provider not loaded due to invalid configuration', key }); - } - - const service = await LoginServiceConfiguration.findOneByService(serviceName, { projection: { _id: 1 } }); - if (!service?._id) { - return false; - } - - const { deletedCount } = await LoginServiceConfiguration.removeService(service._id); - if (!deletedCount) { - return false; - } - - void notifyOnLoginServiceConfigurationChanged({ _id: service._id }, 'removed'); - - return false; - }), - ) - ).filter((e) => e) as IServiceProviderOptions[]; - - SAMLUtils.setServiceProvidersList(providers); -}; - -export const addSamlService = function (name: string): void { - SystemLogger.warn({ - msg: 'Adding SAML service is deprecated', - serviceName: name, - }); -}; - -export const addSettings = async function (name: string): Promise { - await settingsRegistry.addGroup('SAML', async function () { - await this.with( - { - tab: 'SAML_Connection', - }, - async function () { - await this.add(`SAML_Custom_${name}`, false, { - type: 'boolean', - i18nLabel: 'Accounts_OAuth_Custom_Enable', - public: true, - }); - await this.add(`SAML_Custom_${name}_provider`, 'provider-name', { - type: 'string', - i18nLabel: 'SAML_Custom_Provider', - public: true, - }); - await this.add(`SAML_Custom_${name}_entry_point`, 'https://example.com/simplesaml/saml2/idp/SSOService.php', { - type: 'string', - i18nLabel: 'SAML_Custom_Entry_point', - }); - await this.add(`SAML_Custom_${name}_idp_slo_redirect_url`, 'https://example.com/simplesaml/saml2/idp/SingleLogoutService.php', { - type: 'string', - i18nLabel: 'SAML_Custom_IDP_SLO_Redirect_URL', - public: true, - }); - await this.add(`SAML_Custom_${name}_issuer`, 'https://your-rocket-chat/_saml/metadata/provider-name', { - type: 'string', - i18nLabel: 'SAML_Custom_Issuer', - }); - await this.add(`SAML_Custom_${name}_debug`, false, { - type: 'boolean', - i18nLabel: 'SAML_Custom_Debug', - }); - - await this.section('SAML_Section_2_Certificate', async function () { - await this.add(`SAML_Custom_${name}_cert`, '', { - type: 'string', - i18nLabel: 'SAML_Custom_Cert', - multiline: true, - secret: true, - }); - await this.add(`SAML_Custom_${name}_public_cert`, '', { - type: 'string', - multiline: true, - i18nLabel: 'SAML_Custom_Public_Cert', - }); - await this.add(`SAML_Custom_${name}_signature_validation_type`, 'All', { - type: 'select', - values: [ - { key: 'None', i18nLabel: 'SAML_Custom_signature_validation_none' }, - { key: 'Response', i18nLabel: 'SAML_Custom_signature_validation_response' }, - { key: 'Assertion', i18nLabel: 'SAML_Custom_signature_validation_assertion' }, - { key: 'Either', i18nLabel: 'SAML_Custom_signature_validation_either' }, - { key: 'All', i18nLabel: 'SAML_Custom_signature_validation_all' }, - ], - i18nLabel: 'SAML_Custom_signature_validation_type', - i18nDescription: 'SAML_Custom_signature_validation_type_description', - }); - await this.add(`SAML_Custom_${name}_validate_logout_request_signature`, true, { - type: 'boolean', - i18nLabel: 'SAML_Custom_validate_logout_request_signature', - }); - await this.add(`SAML_Custom_${name}_validate_logout_response_signature`, true, { - type: 'boolean', - i18nLabel: 'SAML_Custom_validate_logout_response_signature', - }); - await this.add(`SAML_Custom_${name}_private_key`, '', { - type: 'string', - multiline: true, - i18nLabel: 'SAML_Custom_Private_Key', - secret: true, - }); - await this.add(`SAML_Custom_${name}_signature_algorithm`, 'SHA1', { - type: 'select', - values: [ - { key: 'SHA1', i18nLabel: 'SHA1' }, - { key: 'SHA256', i18nLabel: 'SHA256' }, - { key: 'SHA384', i18nLabel: 'SHA384' }, - { key: 'SHA512', i18nLabel: 'SHA512' }, - ], - i18nLabel: 'SAML_Custom_Signature_Algorithm', - i18nDescription: 'SAML_Custom_Signature_Algorithm_description', - secret: true, - }); - }); - }, - ); - - await this.with( - { - tab: 'SAML_General', - }, - async function () { - await this.section('SAML_Section_1_User_Interface', async function () { - await this.add(`SAML_Custom_${name}_button_label_text`, 'SAML', { - type: 'string', - i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Text', - }); - await this.add(`SAML_Custom_${name}_button_label_color`, '#FFFFFF', { - type: 'string', - i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Color', - alert: 'OAuth_button_colors_alert', - }); - await this.add(`SAML_Custom_${name}_button_color`, '#1d74f5', { - type: 'string', - i18nLabel: 'Accounts_OAuth_Custom_Button_Color', - alert: 'OAuth_button_colors_alert', - }); - }); - - await this.section('SAML_Section_3_Behavior', async function () { - // Settings to customize behavior - await this.add(`SAML_Custom_${name}_generate_username`, false, { - type: 'boolean', - i18nLabel: 'SAML_Custom_Generate_Username', - }); - await this.add(`SAML_Custom_${name}_username_normalize`, 'None', { - type: 'select', - values: [ - { key: 'None', i18nLabel: 'SAML_Custom_Username_Normalize_None' }, - { key: 'Lowercase', i18nLabel: 'SAML_Custom_Username_Normalize_Lowercase' }, - ], - i18nLabel: 'SAML_Custom_Username_Normalize', - }); - await this.add(`SAML_Custom_${name}_immutable_property`, 'EMail', { - type: 'select', - values: [ - { key: 'Username', i18nLabel: 'SAML_Custom_Immutable_Property_Username' }, - { key: 'EMail', i18nLabel: 'SAML_Custom_Immutable_Property_EMail' }, - ], - i18nLabel: 'SAML_Custom_Immutable_Property', - }); - await this.add(`SAML_Custom_${name}_name_overwrite`, false, { - type: 'boolean', - i18nLabel: 'SAML_Custom_name_overwrite', - }); - await this.add(`SAML_Custom_${name}_mail_overwrite`, false, { - type: 'boolean', - i18nLabel: 'SAML_Custom_mail_overwrite', - }); - await this.add(`SAML_Custom_${name}_logout_behaviour`, 'SAML', { - type: 'select', - values: [ - { key: 'SAML', i18nLabel: 'SAML_Custom_Logout_Behaviour_Terminate_SAML_Session' }, - { key: 'Local', i18nLabel: 'SAML_Custom_Logout_Behaviour_End_Only_RocketChat' }, - ], - i18nLabel: 'SAML_Custom_Logout_Behaviour', - public: true, - }); - await this.add(`SAML_Custom_${name}_channels_update`, false, { - type: 'boolean', - i18nLabel: 'SAML_Custom_channels_update', - i18nDescription: 'SAML_Custom_channels_update_description', - }); - await this.add(`SAML_Custom_${name}_include_private_channels_update`, false, { - type: 'boolean', - i18nLabel: 'SAML_Custom_include_private_channels_update', - i18nDescription: 'SAML_Custom_include_private_channels_update_description', - }); - - await this.add(`SAML_Custom_${name}_default_user_role`, 'user', { - type: 'string', - i18nLabel: 'SAML_Default_User_Role', - i18nDescription: 'SAML_Default_User_Role_Description', - }); - - await this.add(`SAML_Custom_${name}_allowed_clock_drift`, 0, { - type: 'int', - invalidValue: 0, - i18nLabel: 'SAML_Allowed_Clock_Drift', - i18nDescription: 'SAML_Allowed_Clock_Drift_Description', - }); - }); - - await this.section('SAML_Section_5_Mapping', async function () { - // Data Mapping Settings - await this.add(`SAML_Custom_${name}_user_data_fieldmap`, '{"username":"username", "email":"email", "name": "cn"}', { - type: 'string', - i18nLabel: 'SAML_Custom_user_data_fieldmap', - i18nDescription: 'SAML_Custom_user_data_fieldmap_description', - multiline: true, - }); - }); - }, - ); - - SAMLUtils.events.emit('addSettings', name); - }); -}; diff --git a/apps/meteor/app/meteor-accounts-saml/server/loginHandler.ts b/apps/meteor/app/meteor-accounts-saml/server/loginHandler.ts deleted file mode 100644 index 8b2059489931c..0000000000000 --- a/apps/meteor/app/meteor-accounts-saml/server/loginHandler.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { CredentialTokens } from '@rocket.chat/models'; -import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; - -import { SAML } from './lib/SAML'; -import { SAMLUtils } from './lib/Utils'; -import { i18n } from '../../../server/lib/i18n'; -import { SystemLogger } from '../../../server/lib/logger/system'; - -const makeError = (message: string): Record => ({ - type: 'saml', - error: new Meteor.Error(Accounts.LoginCancelledError.numericError, message), -}); - -Accounts.registerLoginHandler('saml', async (loginRequest) => { - if ( - !loginRequest.saml || - !loginRequest.credentialToken || - typeof loginRequest.credentialToken !== 'string' || - SAMLUtils.serviceProviders.length === 0 - ) { - return undefined; - } - - const loginResult = await SAML.retrieveCredential(loginRequest.credentialToken); - - await CredentialTokens.removeById(loginRequest.credentialToken); - SAMLUtils.log({ msg: 'RESULT', loginResult }); - - if (!loginResult) { - return makeError('No matching login attempt found'); - } - - if (!loginResult.profile) { - return makeError('No profile information found'); - } - - try { - const userObject = SAMLUtils.mapProfileToUserObject(loginResult.profile); - const updatedUser = await SAML.insertOrUpdateSAMLUser(userObject); - SAMLUtils.events.emit('updateCustomFields', loginResult, updatedUser); - - return updatedUser; - } catch (err: any) { - SystemLogger.error({ err }); - - let message = err.toString(); - let errorCode = ''; - - if (err instanceof Meteor.Error) { - errorCode = (err.error || err.message) as string; - } else if (err instanceof Error) { - errorCode = err.message; - } - - if (errorCode) { - const localizedMessage = i18n.t(errorCode); - if (localizedMessage && localizedMessage !== errorCode) { - message = localizedMessage; - } - } - - return makeError(message); - } -}); diff --git a/apps/meteor/app/meteor-accounts-saml/server/startup.ts b/apps/meteor/app/meteor-accounts-saml/server/startup.ts deleted file mode 100644 index 721edce08d463..0000000000000 --- a/apps/meteor/app/meteor-accounts-saml/server/startup.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Logger } from '@rocket.chat/logger'; -import debounce from 'lodash.debounce'; -import { Meteor } from 'meteor/meteor'; - -import { SAMLUtils } from './lib/Utils'; -import { loadSamlServiceProviders, addSettings } from './lib/settings'; -import { settings } from '../../settings/server'; - -const logger = new Logger('steffo:meteor-accounts-saml'); -SAMLUtils.setLoggerInstance(logger); - -Meteor.startup(async () => { - await addSettings('Default'); -}); - -settings.watchByRegex(/^SAML_.+/, debounce(loadSamlServiceProviders, 2000)); diff --git a/apps/meteor/app/nextcloud/server/addWebdavServer.ts b/apps/meteor/app/nextcloud/server/addWebdavServer.ts deleted file mode 100644 index 6d3ea80f6d178..0000000000000 --- a/apps/meteor/app/nextcloud/server/addWebdavServer.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { callbacks } from '../../../server/lib/callbacks'; -import { SystemLogger } from '../../../server/lib/logger/system'; -import { settings } from '../../settings/server'; -import { addWebdavAccountByToken } from '../../webdav/server/methods/addWebdavAccount'; - -Meteor.startup(() => { - settings.watch('Webdav_Integration_Enabled', (value) => { - if (value) { - return callbacks.add( - 'afterValidateLogin', - async (login) => { - const { user } = login; - const { services } = user; - if (!services?.nextcloud) { - return; - } - const token = { - token_type: 'Bearer', - access_token: services.nextcloud.accessToken, - refresh_token: services.nextcloud.refreshToken, - }; - const data = { - name: 'Nextcloud', - serverURL: `${services.nextcloud.serverURL}/remote.php/webdav/`, - token, - }; - try { - await addWebdavAccountByToken(user._id, data); - } catch (err) { - SystemLogger.error({ err }); - } - }, - callbacks.priority.MEDIUM, - 'add-webdav-server', - ); - } - callbacks.remove('afterValidateLogin', 'add-webdav-server'); - }); -}); diff --git a/apps/meteor/app/nextcloud/server/index.ts b/apps/meteor/app/nextcloud/server/index.ts deleted file mode 100644 index 732ad0a2f4aa0..0000000000000 --- a/apps/meteor/app/nextcloud/server/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './lib'; -import './addWebdavServer'; diff --git a/apps/meteor/app/nextcloud/server/lib.ts b/apps/meteor/app/nextcloud/server/lib.ts deleted file mode 100644 index 28cf52da57a17..0000000000000 --- a/apps/meteor/app/nextcloud/server/lib.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { OauthConfig } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; - -import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; -import { settings } from '../../settings/server'; - -const config: OauthConfig = { - serverURL: '', - tokenPath: '/index.php/apps/oauth2/api/v1/token', - tokenSentVia: 'header', - authorizePath: '/index.php/apps/oauth2/authorize', - identityPath: '/ocs/v2.php/cloud/user?format=json', - scope: 'openid', - addAutopublishFields: { - forLoggedInUser: ['services.nextcloud'], - forOtherUsers: ['services.nextcloud.name'], - }, -}; - -const Nextcloud = new CustomOAuth('nextcloud', config); - -const fillServerURL = _.debounce((): void => { - const nextcloudURL = settings.get('Accounts_OAuth_Nextcloud_URL'); - if (!nextcloudURL) { - if (nextcloudURL === undefined) { - return fillServerURL(); - } - return; - } - config.serverURL = nextcloudURL.trim().replace(/\/*$/, ''); - return Nextcloud.configure(config); -}, 1000); - -Meteor.startup(() => { - settings.watch('Accounts_OAuth_Nextcloud_URL', () => fillServerURL()); -}); diff --git a/apps/meteor/app/oauth2-server-config/.gitignore b/apps/meteor/app/oauth2-server-config/.gitignore deleted file mode 100644 index 677a6fc26373d..0000000000000 --- a/apps/meteor/app/oauth2-server-config/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.build* diff --git a/apps/meteor/app/oauth2-server-config/server/index.ts b/apps/meteor/app/oauth2-server-config/server/index.ts deleted file mode 100644 index 3914cac5eaadc..0000000000000 --- a/apps/meteor/app/oauth2-server-config/server/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import './oauth/oauth2-server'; -import './admin/functions/addOAuthApp'; -import './admin/methods/updateOAuthApp'; -import './admin/methods/deleteOAuthApp'; diff --git a/apps/meteor/app/push-notifications/server/index.ts b/apps/meteor/app/push-notifications/server/index.ts deleted file mode 100644 index c42819a559039..0000000000000 --- a/apps/meteor/app/push-notifications/server/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import './methods/saveNotificationSettings'; -import PushNotification from './lib/PushNotification'; - -export { PushNotification }; diff --git a/apps/meteor/app/push/server/index.ts b/apps/meteor/app/push/server/index.ts deleted file mode 100644 index 4cfcaa2da26f7..0000000000000 --- a/apps/meteor/app/push/server/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import './methods'; - -export { Push } from './push'; diff --git a/apps/meteor/app/push/server/methods.ts b/apps/meteor/app/push/server/methods.ts deleted file mode 100644 index 43514d0f14735..0000000000000 --- a/apps/meteor/app/push/server/methods.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Push } from '@rocket.chat/core-services'; -import type { IPushToken } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { PushToken } from '@rocket.chat/models'; -import { Accounts } from 'meteor/accounts-base'; -import { Match, check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { logger } from './logger'; -import { _matchToken } from './push'; -import { methodDeprecationLogger } from '../../lib/server/lib/deprecationWarningLogger'; - -type PushUpdateOptions = { - id?: string; - token: IPushToken['token']; - authToken: string; - appName: string; - userId: string | null; - metadata?: Record; -}; -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'raix:push-update'(options: PushUpdateOptions): Promise>; - 'raix:push-setuser'(options: { id: string; userId: string }): Promise; - } -} - -Meteor.methods({ - async 'raix:push-update'(options) { - methodDeprecationLogger.method('raix:push-update', '9.0.0', '/v1/push.token'); - logger.debug({ msg: 'Got push token from app', options }); - - check(options, { - id: Match.Optional(String), - token: _matchToken, - authToken: String, - appName: String, - userId: Match.OneOf(String, null), - metadata: Match.Optional(Object), - }); - - // The if user id is set then user id should match on client and connection - if (!this.userId || (options.userId && options.userId !== this.userId)) { - throw new Meteor.Error(403, 'Forbidden access'); - } - - // Retain old behavior: if id is not specified but userId is explicitly set, then update the user's first token - if (!options.id && options.userId) { - const firstDoc = await PushToken.findFirstByUserId(options.userId, { projection: { _id: 1 } }); - if (firstDoc) { - options.id = firstDoc._id; - } - } - - const authToken = Accounts._hashLoginToken(options.authToken); - - return Push.registerPushToken({ - ...(options.id && { _id: options.id }), - token: options.token, - appName: options.appName, - authToken, - userId: this.userId, - ...(options.metadata && { metadata: options.metadata }), - }); - }, - // Deprecated - async 'raix:push-setuser'(id) { - check(id, String); - if (!this.userId) { - throw new Meteor.Error(403, 'Forbidden access'); - } - - logger.debug({ msg: 'Setting userId for app', userId: this.userId, appId: id }); - const found = await PushToken.updateOne({ _id: id }, { $set: { userId: this.userId } }); - - return !!found; - }, -}); diff --git a/apps/meteor/app/push/server/push.ts b/apps/meteor/app/push/server/push.ts deleted file mode 100644 index 63b33832c19ec..0000000000000 --- a/apps/meteor/app/push/server/push.ts +++ /dev/null @@ -1,514 +0,0 @@ -import type { IPushToken, RequiredField, Optional, IPushNotificationConfig } from '@rocket.chat/core-typings'; -import { PushToken } from '@rocket.chat/models'; -import { ajv } from '@rocket.chat/rest-typings'; -import type { ExtendedFetchOptions } from '@rocket.chat/server-fetch'; -import { serverFetch as fetch } from '@rocket.chat/server-fetch'; -import { pick, truncateString } from '@rocket.chat/tools'; -import { JWT } from 'google-auth-library'; -import { Match, check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { initAPN, sendAPN } from './apn'; -import type { PushOptions, PendingPushNotification } from './definition'; -import { sendFCM } from './fcm'; -import { logger } from './logger'; -import { settings } from '../../settings/server'; - -export const _matchToken = Match.OneOf({ apn: String }, { gcm: String }); - -const PUSH_TITLE_LIMIT = 65; -const PUSH_MESSAGE_BODY_LIMIT = 240; -const PUSH_GATEWAY_MAX_RETRIES = 5; - -type FCMCredentials = { - type: string; - project_id: string; - private_key_id: string; - private_key: string; - client_email: string; - client_id: string; - auth_uri: string; - token_uri: string; - auth_provider_x509_cert_url: string; - client_x509_cert_url: string; - universe_domain: string; -}; - -export const FCMCredentialsValidationSchema = { - type: 'object', - properties: { - type: { - type: 'string', - }, - project_id: { - type: 'string', - }, - private_key_id: { - type: 'string', - }, - private_key: { - type: 'string', - }, - client_email: { - type: 'string', - }, - client_id: { - type: 'string', - }, - auth_uri: { - type: 'string', - }, - token_uri: { - type: 'string', - }, - auth_provider_x509_cert_url: { - type: 'string', - }, - client_x509_cert_url: { - type: 'string', - }, - universe_domain: { - type: 'string', - }, - }, - required: ['client_email', 'project_id', 'private_key_id', 'private_key'], -}; - -export const isFCMCredentials = ajv.compile(FCMCredentialsValidationSchema); - -// This type must match the type defined in the push gateway -type GatewayNotification = { - uniqueId: string; - from?: string; - title?: string; - text?: string; - badge?: number; - sound?: string; - notId?: number; - contentAvailable?: 1 | 0; - forceStart?: number; - topic?: string; - apn?: { - from?: string; - title?: string; - text?: string; - badge?: number; - sound?: string; - notId?: number; - category?: string; - expirationSeconds?: number; - }; - gcm?: { - from?: string; - title?: string; - text?: string; - image?: string; - style?: string; - summaryText?: string; - picture?: string; - badge?: number; - sound?: string; - notId?: number; - actions?: any[]; - }; - query?: { - userId: any; - }; - token?: IPushToken['token']; - tokens?: IPushToken['token'][]; - payload?: Record; - delayUntil?: Date; - createdAt: Date; - createdBy?: string; -}; - -export type NativeNotificationParameters = { - userTokens: string | string[]; - notification: PendingPushNotification; - _removeToken: (token: string) => void; - options: RequiredField; -}; - -class PushClass { - options: PushOptions = { - uniqueId: '', - }; - - isConfigured = false; - - public configure(options: PushOptions): void { - this.options = { - sendTimeout: 60000, // Timeout period for notification send - ...options, - }; - // https://npmjs.org/package/apn - - // After requesting the certificate from Apple, export your private key as - // a .p12 file anddownload the .cer file from the iOS Provisioning Portal. - - // gateway.push.apple.com, port 2195 - // gateway.sandbox.push.apple.com, port 2195 - - // Now, in the directory containing cert.cer and key.p12 execute the - // following commands to generate your .pem files: - // $ openssl x509 -in cert.cer -inform DER -outform PEM -out cert.pem - // $ openssl pkcs12 -in key.p12 -out key.pem -nodes - - // Block multiple calls - if (this.isConfigured) { - throw new Error('Configure should not be called more than once!'); - } - - this.isConfigured = true; - - logger.debug({ msg: 'Configure', options: this.options }); - - if (this.options.apn) { - initAPN({ options: this.options as RequiredField, absoluteUrl: Meteor.absoluteUrl() }); - } - } - - private removeToken(token: string): void { - void PushToken.removeOrUnsetByTokenString(token).catch((err) => { - logger.error({ msg: 'Failed to remove push token', err }); - }); - } - - private shouldUseGateway(): boolean { - return Boolean(!!this.options.gateways && settings.get('Register_Server') && settings.get('Cloud_Service_Agree_PrivacyTerms')); - } - - private async sendNotificationNative( - app: IPushToken, - notification: PendingPushNotification, - countApn: string[], - countGcm: string[], - ): Promise { - logger.debug({ msg: 'send to token', token: app.token }); - - if ('apn' in app.token && app.token.apn) { - const userToken = notification.useVoipToken ? app.voipToken : app.token.apn; - const topic = notification.useVoipToken ? `${app.appName}.voip` : app.appName; - - // Send to APN - if (this.options.apn && userToken) { - countApn.push(app._id); - sendAPN({ userToken, notification: { topic, ...notification }, _removeToken: this.removeToken }); - } - } else if ('gcm' in app.token && app.token.gcm) { - countGcm.push(app._id); - - // override this.options.gcm.apiKey with the oauth2 token - const { projectId, token } = await this.getNativeNotificationAuthorizationCredentials(); - const sendGCMOptions = { - ...this.options, - gcm: { - ...this.options.gcm, - apiKey: token, - projectNumber: projectId, - }, - }; - - sendFCM({ - userTokens: app.token.gcm, - notification, - _removeToken: this.removeToken, - options: sendGCMOptions as RequiredField, - }); - } else { - throw new Error('send got a faulty query'); - } - } - - private async getNativeNotificationAuthorizationCredentials(): Promise<{ token: string; projectId: string }> { - const credentialsString = settings.get('Push_google_api_credentials'); - if (!credentialsString.trim()) { - throw new Error('Push_google_api_credentials is not set'); - } - - try { - const credentials = JSON.parse(credentialsString); - if (!isFCMCredentials(credentials)) { - throw new Error('Push_google_api_credentials is not in the correct format'); - } - - const client = new JWT({ - email: credentials.client_email, - key: credentials.private_key, - keyId: credentials.private_key_id, - scopes: 'https://www.googleapis.com/auth/firebase.messaging', - }); - - await client.authorize(); - - return { - token: client.credentials.access_token as string, - projectId: credentials.project_id, - }; - } catch (error) { - logger.error({ msg: 'Error getting FCM token', err: error }); - throw new Error('Error getting FCM token'); - } - } - - private async sendGatewayPush( - gateway: string, - service: 'apn' | 'gcm', - token: string, - notification: Optional, - retryOptions: { tries: number; maxRetries: number } = { tries: 0, maxRetries: PUSH_GATEWAY_MAX_RETRIES }, - ): Promise { - notification.uniqueId = this.options.uniqueId; - - const options = { - // SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check. - ignoreSsrfValidation: true, - method: 'POST', - body: { - token, - options: notification, - }, - ...(token && this.options.getAuthorization && { headers: { Authorization: await this.options.getAuthorization() } }), - } as ExtendedFetchOptions; - - const result = await fetch(`${gateway}/push/${service}/send`, options); - const response = await result.text(); - - if (result.status === 406) { - logger.info({ msg: 'removing push token', token }); - this.removeToken(token); - return; - } - - if (result.status === 422) { - logger.info({ msg: 'gateway rejected push notification. not retrying.', response }); - return; - } - - if (result.status === 401) { - logger.warn({ msg: 'authorization failed when sending push to gateway. not retrying.', response }); - return; - } - - if (result.ok) { - return; - } - - const { tries, maxRetries } = retryOptions; - - logger.error({ msg: 'Error sending push to gateway', tries, err: response }); - - if (tries < maxRetries) { - // [1, 2, 4, 8, 16] minutes (total 31) - const ms = 60000 * Math.pow(2, tries); - - logger.log({ msg: 'Retrying push to gateway', tries: tries + 1, in: ms }); - - setTimeout(() => this.sendGatewayPush(gateway, service, token, notification, { tries: tries + 1, maxRetries }), ms); - } - } - - private getGatewayNotificationData(notification: PendingPushNotification): Omit { - // Gateway currently accepts every attribute from the PendingPushNotification type, except for the priority and useVoipToken - // If new attributes are added to the PendingPushNotification type, they'll need to be removed here as well. - const { priority: _priority, useVoipToken: _useVoipToken, ...notifData } = notification; - - return { - ...notifData, - }; - } - - private async sendNotificationGateway( - app: IPushToken, - notification: PendingPushNotification, - countApn: string[], - countGcm: string[], - ): Promise { - if (!this.options.gateways) { - return; - } - - const gatewayNotification = this.getGatewayNotificationData(notification); - const retryOptions = { - tries: 0, - maxRetries: notification.useVoipToken ? 0 : PUSH_GATEWAY_MAX_RETRIES, - }; - - for (const gateway of this.options.gateways) { - logger.debug({ msg: 'send to token', token: app.token }); - - if ('apn' in app.token && app.token.apn) { - const token = notification.useVoipToken ? app.voipToken : app.token.apn; - const topic = notification.useVoipToken ? `${app.appName}.voip` : app.appName; - - if (token) { - countApn.push(app._id); - return this.sendGatewayPush(gateway, 'apn', token, { topic, ...gatewayNotification }, retryOptions); - } - } - - if ('gcm' in app.token && app.token.gcm) { - countGcm.push(app._id); - return this.sendGatewayPush(gateway, 'gcm', app.token.gcm, gatewayNotification, retryOptions); - } - } - } - - private async sendNotification( - notification: PendingPushNotification, - options: { skipTokenId?: IPushToken['_id'] } = {}, - ): Promise<{ apn: string[]; gcm: string[] }> { - logger.debug({ msg: 'Sending notification', notification }); - - const countApn: string[] = []; - const countGcm: string[] = []; - - if (notification.from && notification.from !== String(notification.from)) { - throw new Error('Push.send: option "from" not a string'); - } - if (notification.title && notification.title !== String(notification.title)) { - throw new Error('Push.send: option "title" not a string'); - } - if (notification.text && notification.text !== String(notification.text)) { - throw new Error('Push.send: option "text" not a string'); - } - - logger.debug({ - msg: 'send message to userId', - title: notification.title, - userId: notification.userId, - }); - - const appTokens = options.skipTokenId - ? PushToken.findTokensByUserIdExceptId(notification.userId, options.skipTokenId) - : PushToken.findAllTokensByUserId(notification.userId); - - for await (const app of appTokens) { - logger.debug({ msg: 'send to token', token: app.token }); - - if (this.shouldUseGateway()) { - await this.sendNotificationGateway(app, notification, countApn, countGcm); - continue; - } - - await this.sendNotificationNative(app, notification, countApn, countGcm); - } - - if (settings.get('Log_Level') === '2') { - logger.debug({ - msg: 'Sent message to apps', - title: notification.title, - iosApps: countApn.length, - androidApps: countGcm.length, - }); - - // Add some verbosity about the send result, making sure the developer - // understands what just happened. - if (!countApn.length && !countGcm.length) { - if ((await PushToken.estimatedDocumentCount()) === 0) { - logger.debug('GUIDE: The "AppsTokens" is empty - No clients have registered on the server yet...'); - } - } else if (!countApn.length) { - if ((await PushToken.countApnTokens()) === 0) { - logger.debug('GUIDE: The "AppsTokens" - No APN clients have registered on the server yet...'); - } - } else if (!countGcm.length) { - if ((await PushToken.countGcmTokens()) === 0) { - logger.debug('GUIDE: The "AppsTokens" - No GCM clients have registered on the server yet...'); - } - } - } - - return { - apn: countApn, - gcm: countGcm, - }; - } - - // This is a general function to validate that the data added to notifications - // is in the correct format. If not this function will throw errors - private _validateDocument(notification: PendingPushNotification): void { - // Check the general notification - check(notification, { - from: Match.Optional(String), - title: Match.Optional(String), - text: Match.Optional(String), - sent: Match.Optional(Boolean), - sending: Match.Optional(Match.Integer), - badge: Match.Optional(Match.Integer), - sound: Match.Optional(String), - notId: Match.Optional(Match.Integer), - contentAvailable: Match.Optional(Match.Integer), - apn: Match.Optional({ - category: Match.Optional(String), - expirationSeconds: Match.Optional(Match.Integer), - }), - gcm: Match.Optional({ - image: Match.Optional(String), - style: Match.Optional(String), - }), - userId: String, - payload: Match.Optional(Object), - createdAt: Date, - createdBy: Match.OneOf(String, null), - priority: Match.Optional(Match.Integer), - useVoipToken: Match.Optional(Boolean), - }); - - if (!notification.userId) { - throw new Error('No userId found'); - } - } - - private hasApnOptions(options: IPushNotificationConfig): options is RequiredField { - return Match.test(options.apn, Object); - } - - private hasGcmOptions(options: IPushNotificationConfig): options is RequiredField { - return Match.test(options.gcm, Object); - } - - public async send(options: IPushNotificationConfig) { - const notification: PendingPushNotification = { - createdAt: new Date(), - // createdBy is no longer used, but the gateway still expects it - createdBy: '', - sent: false, - sending: 0, - ...(options.title && { title: truncateString(options.title, PUSH_TITLE_LIMIT) }), - ...(options.text && { text: truncateString(options.text, PUSH_MESSAGE_BODY_LIMIT) }), - - ...pick(options, 'from', 'userId', 'payload', 'badge', 'sound', 'notId', 'priority', 'useVoipToken'), - - ...(this.hasApnOptions(options) - ? { - apn: { - ...pick(options.apn, 'category', 'expirationSeconds'), - }, - } - : {}), - ...(this.hasGcmOptions(options) - ? { - gcm: { - ...pick(options.gcm, 'image', 'style'), - }, - } - : {}), - }; - - // Validate the notification - this._validateDocument(notification); - - try { - await this.sendNotification(notification, { skipTokenId: options.skipTokenId }); - } catch (error: any) { - logger.debug({ - msg: 'Could not send notification to user', - userId: notification.userId, - err: error, - }); - } - } -} - -export const Push = new PushClass(); diff --git a/apps/meteor/app/reactions/server/index.ts b/apps/meteor/app/reactions/server/index.ts deleted file mode 100644 index 8c5644023dbc8..0000000000000 --- a/apps/meteor/app/reactions/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './setReaction'; diff --git a/apps/meteor/app/retention-policy/README.md b/apps/meteor/app/retention-policy/README.md deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/apps/meteor/app/search/server/events/index.ts b/apps/meteor/app/search/server/events/index.ts deleted file mode 100644 index 30ad122312772..0000000000000 --- a/apps/meteor/app/search/server/events/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { IMessage } from '@rocket.chat/core-typings'; - -import { callbacks } from '../../../../server/lib/callbacks'; -import { settings } from '../../../settings/server'; -import { searchProviderService } from '../service'; -import { EventService } from './EventService'; - -export const searchEventService = new EventService(); - -/** - * Listen to message changes via Hooks - */ -function afterSaveMessage(m: IMessage) { - searchEventService.promoteEvent('message.save', m._id, m); - return m; -} - -function afterDeleteMessage(m: IMessage) { - searchEventService.promoteEvent('message.delete', m._id); - return m; -} - -settings.watch('Search.Provider', () => { - if (searchProviderService.activeProvider?.on) { - callbacks.add('afterSaveMessage', afterSaveMessage, callbacks.priority.MEDIUM, 'search-events'); - callbacks.add('afterDeleteMessage', afterDeleteMessage, callbacks.priority.MEDIUM, 'search-events-delete'); - return; - } - - callbacks.remove('afterSaveMessage', 'search-events'); - callbacks.remove('afterDeleteMessage', 'search-events-delete'); -}); diff --git a/apps/meteor/app/search/server/index.ts b/apps/meteor/app/search/server/index.ts deleted file mode 100644 index 762dc8ab112b2..0000000000000 --- a/apps/meteor/app/search/server/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import './model/SearchProvider'; -import './events'; -import './search.internalService'; -import './register'; -import './startup'; -import './service'; -import './methods'; diff --git a/apps/meteor/app/search/server/methods.ts b/apps/meteor/app/search/server/methods.ts deleted file mode 100644 index ad8c9641f5238..0000000000000 --- a/apps/meteor/app/search/server/methods.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { IMessageSearchProvider, IMessageSearchSuggestion, IRoom, IUser } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { SearchLogger } from './logger/logger'; -import type { IRawSearchResult, ISearchResult } from './model/ISearchResult'; -import { searchProviderService, validationService } from './service'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - 'rocketchatSearch.getProvider'(): IMessageSearchProvider | undefined; - 'rocketchatSearch.search'(text: string, context: { uid?: IUser['_id']; rid: IRoom['_id'] }, payload: unknown): Promise; - 'rocketchatSearch.suggest'( - text: string, - context: { uid?: IUser['_id']; rid: IRoom['_id'] }, - payload: unknown, - ): Promise; - } -} - -Meteor.methods({ - /** - * Get the current provider with key, description, resultTemplate, suggestionItemTemplate and settings (as Map) - */ - 'rocketchatSearch.getProvider'() { - const provider = searchProviderService.activeProvider; - if (!provider) { - return undefined; - } - - return { - key: provider.key, - description: provider.i18nDescription, - icon: provider.iconName, - resultTemplate: provider.resultTemplate, - supportsSuggestions: provider.supportsSuggestions, - suggestionItemTemplate: provider.suggestionItemTemplate, - settings: Object.fromEntries(Object.values(provider.settingsAsMap).map((setting) => [setting.key, setting.value] as const)), - }; - }, - /** - * Search using the current search provider and check if results are valid for the user. The search result has - * the format `{messages:{start:0,numFound:1,docs:[{...}]},users:{...},rooms:{...}}` - * @param text the search text - * @param context the context (uid, rid) - * @param payload custom payload (e.g. for paging) - */ - async 'rocketchatSearch.search'(text, context, payload) { - payload = payload !== null ? payload : undefined; // TODO is this cleanup necessary? - - if (!searchProviderService.activeProvider) { - throw new Error('Provider currently not active'); - } - - SearchLogger.debug({ msg: 'search', text, context, payload }); - - const userId = Meteor.userId(); - if (!userId) { - throw new Error('User not logged in'); - } - - return new Promise((resolve, reject) => { - void searchProviderService.activeProvider?.search(userId, text, context, payload, (error, data) => { - if (error) { - return reject(error); - } - - return resolve(data); - }); - }).then((result) => validationService.validateSearchResult(result)); - }, - - async 'rocketchatSearch.suggest'(text, context, payload) { - payload ??= undefined; // TODO is this cleanup necessary? - - if (!searchProviderService.activeProvider) { - throw new Error('Provider currently not active'); - } - - SearchLogger.debug({ msg: 'suggest', text, context, payload }); - - return new Promise((resolve, reject) => { - searchProviderService.activeProvider?.suggest(text, context, payload, (error, data) => { - if (error) { - reject(error); - return; - } - - resolve(data); - }); - }); - }, -}); diff --git a/apps/meteor/app/settings/server/index.ts b/apps/meteor/app/settings/server/index.ts deleted file mode 100644 index 30a8c9fd6ba27..0000000000000 --- a/apps/meteor/app/settings/server/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable react-hooks/rules-of-hooks */ -import { Settings } from '@rocket.chat/models'; - -import { use } from './Middleware'; -import { SettingsRegistry } from './SettingsRegistry'; -import { settings } from './cached'; -import { initializeSettings } from './startup'; -import './applyMiddlewares'; - -export { SettingsEvents } from './SettingsRegistry'; - -export { settings }; - -export const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings }); - -settingsRegistry.add = use(settingsRegistry.add, async (context, next) => { - return next(...context) as any; -}); - -settingsRegistry.addGroup = use(settingsRegistry.addGroup, async (context, next) => { - return next(...context) as any; -}); - -await initializeSettings({ model: Settings, settings }); diff --git a/apps/meteor/app/settings/server/startup.ts b/apps/meteor/app/settings/server/startup.ts deleted file mode 100644 index 5c0f4cdee5fe6..0000000000000 --- a/apps/meteor/app/settings/server/startup.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { ISetting } from '@rocket.chat/core-typings'; -import type { Settings } from '@rocket.chat/models'; - -import type { ICachedSettings } from './CachedSettings'; - -// eslint-disable-next-line @typescript-eslint/naming-convention -export async function initializeSettings({ model, settings }: { model: typeof Settings; settings: ICachedSettings }): Promise { - await model.find().forEach((record: ISetting) => { - settings.set(record); - }); - - settings.initialized(); -} diff --git a/apps/meteor/app/slashcommands-archiveroom/server/server.ts b/apps/meteor/app/slashcommands-archiveroom/server/server.ts deleted file mode 100644 index f1b33c1022bb6..0000000000000 --- a/apps/meteor/app/slashcommands-archiveroom/server/server.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { api } from '@rocket.chat/core-services'; -import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { isRegisterUser } from '@rocket.chat/core-typings'; -import { Users, Rooms } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { RoomMemberActions } from '../../../definition/IRoomTypeConfig'; -import { i18n } from '../../../server/lib/i18n'; -import { roomCoordinator } from '../../../server/lib/rooms/roomCoordinator'; -import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; -import { archiveRoom } from '../../lib/server/functions/archiveRoom'; -import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/server/slashCommand'; - -slashCommands.add({ - command: 'archive', - callback: async function Archive({ params, message, userId }: SlashCommandCallbackParams<'archive'>): Promise { - let channel = params.trim(); - - let room; - - if (channel === '') { - room = await Rooms.findOneById(message.rid); - if (room?.name) { - channel = room.name; - } - } else { - channel = channel.replace('#', ''); - room = await Rooms.findOneByName(channel); - } - - if (!userId) { - return; - } - - const user = await Users.findOneById(userId, { projection: { username: 1, name: 1 } }); - if (!user || !isRegisterUser(user)) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'archiveRoom' }); - } - - if (!room) { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Channel_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [channel], - lng: settings.get('Language') || 'en', - }), - }); - return; - } - - if (!(await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.ARCHIVE, userId))) { - throw new Meteor.Error('error-room-type-not-archivable', `Room type: ${room.t} can not be archived`); - } - - if (!(await hasPermissionAsync(userId, 'archive-room', room._id))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized'); - } - - if (room.archived) { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Duplicate_archived_channel_name', { - postProcess: 'sprintf', - sprintf: [channel], - lng: settings.get('Language') || 'en', - }), - }); - return; - } - - await archiveRoom(room._id, user); - - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Channel_Archived', { - postProcess: 'sprintf', - sprintf: [channel], - lng: settings.get('Language') || 'en', - }), - }); - }, - options: { - description: 'Archive', - params: '#channel', - permission: 'archive-room', - }, -}); diff --git a/apps/meteor/app/slashcommands-ban/server/ban.ts b/apps/meteor/app/slashcommands-ban/server/ban.ts deleted file mode 100644 index 3ca43a3b5f4db..0000000000000 --- a/apps/meteor/app/slashcommands-ban/server/ban.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { api } from '@rocket.chat/core-services'; -import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; - -import { banUserFromRoomMethod } from '../../../server/lib/banUserFromRoom'; -import { i18n } from '../../../server/lib/i18n'; -import { sanitizeUsername } from '../../lib/server/methods/addUsersToRoom'; -import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/server/slashCommand'; - -slashCommands.add({ - command: 'ban', - callback: async ({ params, message, userId }: SlashCommandCallbackParams<'ban'>): Promise => { - const username = sanitizeUsername(params.trim()); - - if (!username) { - return; - } - - const user = await Users.findOneByUsernameIgnoringCase(username); - if (!user) { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Username_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [username], - lng: settings.get('Language') || 'en', - }), - }); - return; - } - - await banUserFromRoomMethod(userId, { rid: message.rid, username }); - }, - options: { - description: 'Ban_user_from_room', - params: '@username', - permission: 'ban-user', - }, -}); diff --git a/apps/meteor/app/slashcommands-ban/server/unban.ts b/apps/meteor/app/slashcommands-ban/server/unban.ts deleted file mode 100644 index 4cb52047b651f..0000000000000 --- a/apps/meteor/app/slashcommands-ban/server/unban.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { api } from '@rocket.chat/core-services'; -import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; - -import { i18n } from '../../../server/lib/i18n'; -import { unbanUserFromRoom } from '../../../server/lib/unbanUserFromRoom'; -import { sanitizeUsername } from '../../lib/server/methods/addUsersToRoom'; -import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/server/slashCommand'; - -slashCommands.add({ - command: 'unban', - callback: async ({ params, message, userId }: SlashCommandCallbackParams<'unban'>): Promise => { - const username = sanitizeUsername(params.trim()); - - if (!username) { - return; - } - - const user = await Users.findOneByUsernameIgnoringCase(username); - if (!user) { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Username_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [username], - lng: settings.get('Language') || 'en', - }), - }); - return; - } - - await unbanUserFromRoom(userId, { rid: message.rid, username }); - }, - options: { - description: 'Unban_user_from_room', - params: '@username', - permission: 'ban-user', - }, -}); diff --git a/apps/meteor/app/slashcommands-create/server/server.ts b/apps/meteor/app/slashcommands-create/server/server.ts deleted file mode 100644 index 6abee71c56fdd..0000000000000 --- a/apps/meteor/app/slashcommands-create/server/server.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { api } from '@rocket.chat/core-services'; -import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { Rooms, Users } from '@rocket.chat/models'; - -import { i18n } from '../../../server/lib/i18n'; -import { createChannelMethod } from '../../lib/server/methods/createChannel'; -import { createPrivateGroupMethod } from '../../lib/server/methods/createPrivateGroup'; -import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/server/slashCommand'; - -slashCommands.add({ - command: 'create', - callback: async function Create({ params, message, userId }: SlashCommandCallbackParams<'create'>): Promise { - function getParams(str: string): string[] { - const regex = /(--(\w+))+/g; - const result = []; - let m; - while ((m = regex.exec(str)) !== null) { - if (m.index === regex.lastIndex) { - regex.lastIndex++; - } - result.push(m[2]); - } - return result; - } - - const regexp = new RegExp(settings.get('UTF8_Channel_Names_Validation') as string); - - const channel = regexp.exec(params.trim()); - - if (!channel) { - return; - } - - const channelStr: string = channel ? channel[0] : ''; - if (channelStr === '') { - return; - } - - const room = await Rooms.findOneByName(channelStr); - if (room != null) { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Channel_already_exist', { - postProcess: 'sprintf', - sprintf: [channelStr], - lng: settings.get('Language') || 'en', - }), - }); - return; - } - - if (getParams(params).indexOf('private') > -1) { - const user = await Users.findOneById(userId); - if (!user) { - return; - } - await createPrivateGroupMethod(user, channelStr, []); - return; - } - - await createChannelMethod(userId, channelStr, []); - }, - options: { - description: 'Create_A_New_Channel', - params: '#channel', - permission: ['create-c', 'create-p'], - }, -}); diff --git a/apps/meteor/app/slashcommands-help/server/server.ts b/apps/meteor/app/slashcommands-help/server/server.ts deleted file mode 100644 index 4d826996b43f6..0000000000000 --- a/apps/meteor/app/slashcommands-help/server/server.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { api } from '@rocket.chat/core-services'; -import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; - -import { i18n } from '../../../server/lib/i18n'; -import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/server/slashCommand'; - -/* - * Help is a named function that will replace /help commands - * @param {Object} message - The message object - */ - -interface IHelpCommand { - key: string; - command: string; -} - -slashCommands.add({ - command: 'help', - callback: async function Help({ message, userId }: SlashCommandCallbackParams<'help'>): Promise { - const user = await Users.findOneById(userId); - - const keys: IHelpCommand[] = [ - { - key: 'Open_channel_user_search', - command: 'Command (or Ctrl) + p OR Command (or Ctrl) + k', - }, - { - key: 'Mark_all_as_read', - command: 'Shift (or Ctrl) + ESC', - }, - { - key: 'Edit_previous_message', - command: 'Up Arrow', - }, - { - key: 'Move_beginning_message', - command: 'Command (or Alt) + Left Arrow', - }, - { - key: 'Move_beginning_message', - command: 'Command (or Alt) + Up Arrow', - }, - { - key: 'Move_end_message', - command: 'Command (or Alt) + Right Arrow', - }, - { - key: 'Move_end_message', - command: 'Command (or Alt) + Down Arrow', - }, - { - key: 'New_line_message_compose_input', - command: 'Shift + Enter', - }, - ]; - let msg = ''; - keys.forEach((key) => { - msg = `${msg}\n${i18n.t(key.key, { - postProcess: 'sprintf', - sprintf: [key.command], - lng: user?.language || settings.get('language') || 'en', - })}`; - }); - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg, - ...(message.tmid && { tmid: message.tmid }), - }); - }, - options: { - description: 'Show_the_keyboard_shortcut_list', - }, -}); diff --git a/apps/meteor/app/slashcommands-invite/server/server.ts b/apps/meteor/app/slashcommands-invite/server/server.ts deleted file mode 100644 index 9fd22b7b54ce5..0000000000000 --- a/apps/meteor/app/slashcommands-invite/server/server.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { api, FederationMatrix, isMeteorError } from '@rocket.chat/core-services'; -import type { IUser, SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { isBannedSubscription } from '@rocket.chat/core-typings'; -import { validateFederatedUsername } from '@rocket.chat/federation-matrix'; -import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { i18n } from '../../../server/lib/i18n'; -import { FederationActions } from '../../../server/services/room/hooks/BeforeFederationActions'; -import { addUsersToRoomMethod, sanitizeUsername } from '../../lib/server/methods/addUsersToRoom'; -import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/server/slashCommand'; - -// Type guards for the error -function isStringError(error: unknown): error is { error: string } { - return typeof (error as any)?.error === 'string'; -} - -/* - * Invite is a named function that will replace /invite commands - * @param {Object} message - The message object - */ -slashCommands.add({ - command: 'invite', - callback: async ({ params, message, userId }: SlashCommandCallbackParams<'invite'>): Promise => { - let usernames = params - .split(/[\s,]/) - .map((username) => sanitizeUsername(username)) - .filter((a) => a !== ''); - if (usernames.length === 0) { - return; - } - - // Get room information for federation check - const room = await Rooms.findOneById(message.rid); - if (!room) { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('error-invalid-room', { lng: settings.get('Language') || 'en' }), - }); - return; - } - - // Ensure federated users exist locally before looking them up - const federatedUsernames = usernames.filter((u) => validateFederatedUsername(u)) as string[]; - if (federatedUsernames.length > 0) { - if (FederationActions.shouldPerformFederationAction(room)) { - await FederationMatrix.ensureFederatedUsersExistLocally(federatedUsernames); - } else { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('You_cannot_add_external_users_to_non_federated_room', { lng: settings.get('Language') || 'en' }), - }); - // These federated users shouldn't be invited and we already broadcasted the error message - usernames = usernames.filter((username) => { - return !federatedUsernames.includes(username); - }); - if (usernames.length === 0) { - return; - } - } - } - - const users = await Users.findByUsernames(usernames).toArray(); - if (users.length === 0) { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('User_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [usernames.join(' @')], - lng: settings.get('Language') || 'en', - }), - }); - return; - } - - const usersFiltered: IUser[] = []; - - for await (const user of users) { - const subscription = await Subscriptions.findOneByRoomIdAndUserId(message.rid, user._id, { - projection: { _id: 1, status: 1 }, - }); - if (subscription == null || isBannedSubscription(subscription)) { - usersFiltered.push(user); - continue; - } - const usernameStr = user.username as string; - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Username_is_already_in_here', { - postProcess: 'sprintf', - sprintf: [usernameStr], - lng: settings.get('Language') || 'en', - }), - }); - } - - const inviter = await Users.findOneById(userId); - - if (!inviter) { - throw new Meteor.Error('error-user-not-found', 'Inviter not found', { - method: 'slashcommand-invite', - }); - } - - await Promise.all( - usersFiltered.map(async (user) => { - try { - // TODO: Refactor this to return an error if some user fails to be added - return await addUsersToRoomMethod( - userId, - { - rid: message.rid, - users: [user.username || ''], - }, - inviter, - ); - } catch (e: unknown) { - if (isMeteorError(e)) { - if (e.error === 'error-only-compliant-users-can-be-added-to-abac-rooms') { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t(e.error, { lng: settings.get('Language') || 'en' }), - }); - } else { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t(e.message, { lng: settings.get('Language') || 'en' }), - }); - } - return; - } - - if (isStringError(e)) { - const { error } = e; - if (error === 'error-federated-users-in-non-federated-rooms') { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('You_cannot_add_external_users_to_non_federated_room', { lng: settings.get('Language') || 'en' }), - }); - } else if (error === 'cant-invite-for-direct-room') { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Cannot_invite_users_to_direct_rooms', { lng: settings.get('Language') || 'en' }), - }); - } else { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t(error, { lng: settings.get('Language') || 'en' }), - }); - } - } - } - }), - ); - }, - options: { - description: 'Invite_user_to_join_channel', - params: '@username', - permission: 'add-user-to-joined-room', - }, -}); diff --git a/apps/meteor/app/slashcommands-inviteall/server/server.ts b/apps/meteor/app/slashcommands-inviteall/server/server.ts deleted file mode 100644 index 0ad7cebe6ecfb..0000000000000 --- a/apps/meteor/app/slashcommands-inviteall/server/server.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Invite is a named function that will replace /invite commands - * @param {Object} message - The message object - */ - -import { api } from '@rocket.chat/core-services'; -import type { ISubscription, SlashCommand, SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; -import { isTruthy } from '@rocket.chat/tools'; -import { Meteor } from 'meteor/meteor'; - -import { i18n } from '../../../server/lib/i18n'; -import { canAccessRoomAsync } from '../../authorization/server'; -import { addUsersToRoomMethod } from '../../lib/server/methods/addUsersToRoom'; -import { createChannelMethod } from '../../lib/server/methods/createChannel'; -import { createPrivateGroupMethod } from '../../lib/server/methods/createPrivateGroup'; -import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/server/slashCommand'; - -function inviteAll(type: T): SlashCommand['callback'] { - return async function inviteAll({ command, params, message, userId }: SlashCommandCallbackParams): Promise { - if (!/invite\-all-(to|from)/.test(command)) { - return; - } - - let channel = params.trim(); - if (channel === '') { - return; - } - - channel = channel.replace('#', ''); - - if (!channel) { - return; - } - if (!userId) { - return; - } - - const user = await Users.findOneById(userId); - if (!user) { - return; - } - const lng = user?.language || settings.get('Language') || 'en'; - - const baseChannel = type === 'to' ? await Rooms.findOneById(message.rid) : await Rooms.findOneByName(channel); - const targetChannel = type === 'from' ? await Rooms.findOneById(message.rid) : await Rooms.findOneByName(channel); - - if (!baseChannel) { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Channel_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [channel], - lng, - }), - }); - return; - } - - if (!(await canAccessRoomAsync(baseChannel, user))) { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Room_not_exist_or_not_permission', { lng }), - }); - return; - } - - try { - const APIsettings = settings.get('API_User_Limit'); - if (!APIsettings) { - return; - } - if ((await Subscriptions.countByRoomIdWhenUsernameExists(baseChannel._id)) > APIsettings) { - throw new Meteor.Error('error-user-limit-exceeded', 'User Limit Exceeded', { - method: 'addAllToRoom', - }); - } - - const cursor = Subscriptions.findByRoomIdWhenUsernameExists(baseChannel._id, { - projection: { 'u.username': 1 }, - }); - const users = (await cursor.toArray()).map((s: ISubscription) => s.u.username).filter(isTruthy); - - if (!targetChannel && ['c', 'p'].indexOf(baseChannel.t) > -1) { - baseChannel.t === 'c' ? await createChannelMethod(userId, channel, users) : await createPrivateGroupMethod(user, channel, users); - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Channel_created', { - postProcess: 'sprintf', - sprintf: [channel], - lng, - }), - }); - } else { - await addUsersToRoomMethod(userId, { - rid: targetChannel?._id ?? '', - users, - }); - } - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Users_added', { lng }), - }); - } catch (e: any) { - const msg = e.error === 'cant-invite-for-direct-room' ? 'Cannot_invite_users_to_direct_rooms' : e.error; - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t(msg, { lng }), - }); - } - }; -} - -slashCommands.add({ - command: 'invite-all-to', - callback: inviteAll('to'), - options: { - description: 'Invite_user_to_join_channel_all_to', - params: '#room', - permission: ['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room'], - }, -}); -slashCommands.add({ - command: 'invite-all-from', - callback: inviteAll('from'), - options: { - description: 'Invite_user_to_join_channel_all_from', - params: '#room', - permission: 'add-user-to-joined-room', - }, -}); -module.exports = inviteAll; diff --git a/apps/meteor/app/slashcommands-join/server/server.ts b/apps/meteor/app/slashcommands-join/server/server.ts deleted file mode 100644 index 4d7b3fe001f7b..0000000000000 --- a/apps/meteor/app/slashcommands-join/server/server.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { api, Room } from '@rocket.chat/core-services'; -import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { i18n } from '../../../server/lib/i18n'; -import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/server/slashCommand'; - -slashCommands.add({ - command: 'join', - callback: async ({ params, message, userId }: SlashCommandCallbackParams<'join'>): Promise => { - let channel = params.trim(); - if (channel === '') { - return; - } - - if (!userId) { - return; - } - - channel = channel.replace('#', ''); - - const room = await Rooms.findOneByNameAndType(channel, 'c'); - if (!room) { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Channel_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [channel], - lng: settings.get('Language') || 'en', - }), - }); - return; - } - - const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, userId, { - projection: { _id: 1 }, - }); - - if (subscription) { - throw new Meteor.Error('error-user-already-in-room', 'You are already in the channel', { - method: 'slashCommands', - }); - } - - const user = await Users.findOneById(userId); - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'slashCommands', - }); - } - await Room.join({ room, user }); - }, - options: { - description: 'Join_the_given_channel', - params: '#channel', - permission: 'view-c-room', - }, -}); diff --git a/apps/meteor/app/slashcommands-kick/server/server.ts b/apps/meteor/app/slashcommands-kick/server/server.ts deleted file mode 100644 index f29a65a764e7c..0000000000000 --- a/apps/meteor/app/slashcommands-kick/server/server.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Kick is a named function that will replace /kick commands -import { api } from '@rocket.chat/core-services'; -import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; - -import { i18n } from '../../../server/lib/i18n'; -import { removeUserFromRoomMethod } from '../../../server/methods/removeUserFromRoom'; -import { sanitizeUsername } from '../../lib/server/methods/addUsersToRoom'; -import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/server/slashCommand'; - -slashCommands.add({ - command: 'kick', - callback: async ({ params, message, userId }: SlashCommandCallbackParams<'kick'>): Promise => { - const username = sanitizeUsername(params.trim()); - if (username === '') { - return; - } - const user = await Users.findOneById(userId); - const lng = user?.language || settings.get('Language') || 'en'; - - const kickedUser = await Users.findOneByUsernameIgnoringCase(username); - - if (kickedUser == null) { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Username_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [username], - lng, - }), - }); - return; - } - - const { rid } = message; - - await removeUserFromRoomMethod(userId, { rid, username }); - }, - options: { - description: 'Remove_someone_from_room', - params: '@username', - permission: 'remove-user', - }, -}); diff --git a/apps/meteor/app/slashcommands-msg/server/server.ts b/apps/meteor/app/slashcommands-msg/server/server.ts deleted file mode 100644 index e757938106eb4..0000000000000 --- a/apps/meteor/app/slashcommands-msg/server/server.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { api } from '@rocket.chat/core-services'; -import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; -import { Random } from '@rocket.chat/random'; - -import { i18n } from '../../../server/lib/i18n'; -import { createDirectMessage } from '../../../server/methods/createDirectMessage'; -import { executeSendMessage } from '../../lib/server/methods/sendMessage'; -import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/server/slashCommand'; - -/* - * Msg is a named function that will replace /msg commands - */ - -slashCommands.add({ - command: 'msg', - callback: async function Msg({ params, message: item, userId }: SlashCommandCallbackParams<'msg'>): Promise { - const trimmedParams = params.trim(); - const separator = trimmedParams.indexOf(' '); - if (separator === -1) { - void api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: i18n.t('Username_and_message_must_not_be_empty', { lng: settings.get('Language') || 'en' }), - }); - return; - } - const message = trimmedParams.slice(separator + 1); - const targetUsernameOrig = trimmedParams.slice(0, separator); - const targetUsername = targetUsernameOrig.replace('@', ''); - const targetUser = await Users.findOneByUsernameIgnoringCase(targetUsername); - if (targetUser == null) { - const user = await Users.findOneById(userId, { projection: { language: 1 } }); - void api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: i18n.t('Username_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [targetUsernameOrig], - lng: user?.language || settings.get('Language') || 'en', - }), - }); - return; - } - const { rid } = await createDirectMessage([targetUsername], userId); - const msgObject = { - _id: Random.id(), - rid, - msg: message, - }; - await executeSendMessage(userId, msgObject); - }, - options: { - description: 'Direct_message_someone', - params: '@username ', - permission: 'create-d', - }, -}); diff --git a/apps/meteor/app/slashcommands-topic/server/topic.ts b/apps/meteor/app/slashcommands-topic/server/topic.ts deleted file mode 100644 index c1fa6ea283b78..0000000000000 --- a/apps/meteor/app/slashcommands-topic/server/topic.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; - -import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; -import { saveRoomSettings } from '../../channel-settings/server/methods/saveRoomSettings'; -import { slashCommands } from '../../utils/server/slashCommand'; - -slashCommands.add({ - command: 'topic', - callback: async ({ params, message, userId }: SlashCommandCallbackParams<'topic'>): Promise => { - if (userId && (await hasPermissionAsync(userId, 'edit-room', message.rid))) { - await saveRoomSettings(userId, message.rid, 'roomTopic', params); - } - }, - options: { - description: 'Slash_Topic_Description', - params: 'Slash_Topic_Params', - permission: 'edit-room', - }, -}); diff --git a/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts b/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts deleted file mode 100644 index 4c0c44269d2fa..0000000000000 --- a/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { api } from '@rocket.chat/core-services'; -import { isRegisterUser } from '@rocket.chat/core-typings'; -import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { Users, Rooms } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { RoomMemberActions } from '../../../definition/IRoomTypeConfig'; -import { i18n } from '../../../server/lib/i18n'; -import { roomCoordinator } from '../../../server/lib/rooms/roomCoordinator'; -import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; -import { unarchiveRoom } from '../../lib/server/functions/unarchiveRoom'; -import { settings } from '../../settings/server'; -import { slashCommands } from '../../utils/server/slashCommand'; - -slashCommands.add({ - command: 'unarchive', - callback: async function Unarchive({ params, message, userId }: SlashCommandCallbackParams<'unarchive'>): Promise { - let channel = params.trim(); - let room; - - if (channel === '') { - room = await Rooms.findOneById(message.rid); - if (room?.name) { - channel = room.name; - } - } else { - channel = channel.replace('#', ''); - room = await Rooms.findOneByName(channel); - } - - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'archiveRoom' }); - } - - const user = await Users.findOneById(userId, { projection: { username: 1, name: 1 } }); - if (!user || !isRegisterUser(user)) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'archiveRoom' }); - } - - if (!room) { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Channel_doesnt_exist', { - postProcess: 'sprintf', - sprintf: [channel], - lng: settings.get('Language') || 'en', - }), - }); - return; - } - - if (!(await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.ARCHIVE, userId))) { - throw new Meteor.Error('error-room-type-not-unarchivable', `Room type: ${room.t} can not be unarchived`); - } - - if (!(await hasPermissionAsync(userId, 'unarchive-room', room._id))) { - throw new Meteor.Error('error-not-authorized', 'Not authorized'); - } - - if (!room.archived) { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Channel_already_Unarchived', { - postProcess: 'sprintf', - sprintf: [channel], - lng: settings.get('Language') || 'en', - }), - }); - return; - } - - await unarchiveRoom(room._id, user); - - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('Channel_Unarchived', { - postProcess: 'sprintf', - sprintf: [channel], - lng: settings.get('Language') || 'en', - }), - }); - }, - options: { - description: 'Unarchive', - params: '#channel', - permission: 'unarchive-room', - }, -}); diff --git a/apps/meteor/app/smarsh-connector/server/startup.ts b/apps/meteor/app/smarsh-connector/server/startup.ts deleted file mode 100644 index edd4be4570824..0000000000000 --- a/apps/meteor/app/smarsh-connector/server/startup.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { cronJobs } from '@rocket.chat/cron'; - -import { generateEml } from './functions/generateEml'; -import { smarshIntervalValuesToCronMap } from '../../../server/settings/smarsh'; -import { settings } from '../../settings/server'; - -const smarshJobName = 'Smarsh EML Connector'; - -settings.watchMultiple( - ['Smarsh_Enabled', 'Smarsh_Email', 'From_Email', 'Smarsh_Interval'], - async function __addSmarshSyncedCronJobDebounced() { - if (await cronJobs.has(smarshJobName)) { - await cronJobs.remove(smarshJobName); - } - - if (settings.get('Smarsh_Enabled') && settings.get('Smarsh_Email') !== '' && settings.get('From_Email') !== '') { - const cronInterval = smarshIntervalValuesToCronMap[settings.get('Smarsh_Interval')]; - await cronJobs.add(smarshJobName, cronInterval, async () => generateEml()); - } - }, -); diff --git a/apps/meteor/app/statistics/server/functions/getStatistics.ts b/apps/meteor/app/statistics/server/functions/getStatistics.ts deleted file mode 100644 index 58e3f0ca84741..0000000000000 --- a/apps/meteor/app/statistics/server/functions/getStatistics.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { IStats } from '@rocket.chat/core-typings'; -import { Statistics } from '@rocket.chat/models'; -import type { FindOptions, SchemaMember } from 'mongodb'; - -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; - -type GetStatisticsParams = { - userId: string; - query?: Record; - pagination: { - offset: number; - count?: number; - sort?: FindOptions['sort']; - fields?: SchemaMember; - }; -}; - -type GetStatisticsReturn = { statistics: IStats[]; count: number; offset: number; total: number }; - -export async function getStatistics({ - userId, - query = {}, - pagination: { offset, count, sort, fields }, -}: GetStatisticsParams): Promise { - if (!(await hasPermissionAsync(userId, 'view-statistics'))) { - throw new Error('error-not-allowed'); - } - - const { cursor, totalCount } = Statistics.findPaginated(query, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - projection: fields, - }); - - const [statistics, total] = await Promise.all([cursor.toArray(), totalCount]); - - return { - statistics, - count: statistics.length, - offset, - total, - }; -} diff --git a/apps/meteor/app/statistics/server/index.ts b/apps/meteor/app/statistics/server/index.ts deleted file mode 100644 index 15020aea451a6..0000000000000 --- a/apps/meteor/app/statistics/server/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import './methods/getStatistics'; -import './startup/monitor'; -import './functions/slashCommandsStats'; - -export { statistics } from './lib/statistics'; -export { getLastStatistics } from './functions/getLastStatistics'; -export { getStatistics } from './functions/getStatistics'; -export { updateCounter } from './functions/updateStatsCounter'; diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts deleted file mode 100644 index 8c810acaad70b..0000000000000 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ /dev/null @@ -1,604 +0,0 @@ -import { log } from 'console'; -import os from 'node:os'; - -import { Analytics, Team, VideoConf, Presence } from '@rocket.chat/core-services'; -import type { IRoom, IStats, ISetting } from '@rocket.chat/core-typings'; -import { UserStatus } from '@rocket.chat/core-typings'; -import { License } from '@rocket.chat/license'; -import { - NotificationQueue, - Rooms, - Statistics, - Sessions, - Integrations, - Invites, - Uploads, - LivechatDepartment, - LivechatVisitors, - EmailInbox, - LivechatBusinessHours, - Messages, - Roles as RolesRaw, - InstanceStatus, - Settings, - LivechatTrigger, - LivechatCustomField, - Subscriptions, - Users, - LivechatRooms, - AbacAttributes, -} from '@rocket.chat/models'; -import { MongoInternals } from 'meteor/mongo'; -import moment from 'moment'; - -import { getAppsStatistics } from './getAppsStatistics'; -import { getContactVerificationStatistics } from './getContactVerificationStatistics'; -import { getStatistics as getEnterpriseStatistics } from './getEEStatistics'; -import { getImporterStatistics } from './getImporterStatistics'; -import { getServicesStatistics } from './getServicesStatistics'; -import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; -import { isRunningMs } from '../../../../server/lib/isRunningMs'; -import { getControl } from '../../../../server/lib/migrations'; -import { getSettingsStatistics } from '../../../../server/lib/statistics/getSettingsStatistics'; -import { getMatrixFederationStatistics } from '../../../../server/services/federation/infrastructure/rocket-chat/adapters/Statistics'; -import { settings } from '../../../settings/server'; -import { Info } from '../../../utils/rocketchat.info'; -import { getMongoInfo } from '../../../utils/server/functions/getMongoInfo'; - -const getUserLanguages = async (totalUsers: number): Promise<{ [key: string]: number }> => { - const result = await Users.getUserLanguages(); - - const languages: { [key: string]: number } = { - none: totalUsers, - }; - - result.forEach(({ _id, total }: { _id: string; total: number }) => { - if (!_id) { - return; - } - languages[_id] = total; - languages.none -= total; - }); - - return languages; -}; - -const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; - -export const statistics = { - get: async (): Promise => { - const readPreference = readSecondaryPreferred(db); - - const statistics = {} as IStats; - const statsPms = []; - - // Setup Wizard - const [organizationType, industry, size, country, language, serverType, registerServer] = await Promise.all([ - settings.get('Organization_Type'), - settings.get('Industry'), - settings.get('Size'), - settings.get('Country'), - settings.get('Language'), - settings.get('Server_Type'), - settings.get('Register_Server'), - ]); - - statistics.wizard = { - organizationType, - industry, - size, - country, - language, - serverType, - registerServer, - }; - - // Version - const uniqueID = await Settings.findOne>('uniqueID', { projection: { createdAt: 1 } }); - statistics.uniqueId = settings.get('uniqueID'); - if (uniqueID) { - statistics.installedAt = uniqueID.createdAt.toISOString(); - } - - statistics.deploymentFingerprintHash = settings.get('Deployment_FingerPrint_Hash'); - statistics.deploymentFingerprintVerified = settings.get('Deployment_FingerPrint_Verified'); - - if (Info) { - statistics.version = Info.version; - statistics.tag = Info.tag; - statistics.branch = Info.branch; - } - - // User statistics - statistics.totalUsers = await Users.estimatedDocumentCount(); - statistics.activeUsers = await Users.getActiveLocalUserCount(); - statistics.activeGuests = await Users.getActiveLocalGuestCount(); - statistics.nonActiveUsers = await Users.countDocuments({ active: false }); - statistics.appUsers = await Users.countDocuments({ type: 'app' }); - statistics.onlineUsers = await Users.countDocuments({ status: UserStatus.ONLINE }); - statistics.awayUsers = await Users.countDocuments({ status: UserStatus.AWAY }); - statistics.busyUsers = await Users.countDocuments({ status: UserStatus.BUSY }); - statistics.totalConnectedUsers = statistics.onlineUsers + statistics.awayUsers; - statistics.offlineUsers = statistics.totalUsers - statistics.onlineUsers - statistics.awayUsers - statistics.busyUsers; - statsPms.push( - getUserLanguages(statistics.totalUsers).then((total) => { - statistics.userLanguages = total; - }), - ); - - // Room statistics - statistics.totalRooms = await Rooms.estimatedDocumentCount(); - statistics.totalChannels = await Rooms.countByType('c'); - statistics.totalPrivateGroups = await Rooms.countByType('p'); - statistics.totalDirect = await Rooms.countByType('d'); - statistics.totalLivechat = await Rooms.countByType('l'); - statistics.totalDiscussions = await Rooms.countDiscussions(); - statistics.totalThreads = await Messages.countThreads(); - - // livechat visitors - statistics.totalLivechatVisitors = await LivechatVisitors.estimatedDocumentCount(); - - // livechat agents - statistics.totalLivechatAgents = await Users.countAgents(); - statistics.totalLivechatManagers = await Users.countDocuments({ roles: 'livechat-manager' }); - - // livechat enabled - statistics.livechatEnabled = settings.get('Livechat_enabled'); - - // Count and types of omnichannel rooms - statsPms.push( - Rooms.allRoomSourcesCount() - .toArray() - .then((roomSources) => { - statistics.omnichannelSources = roomSources.map(({ _id: { id, alias, type }, count }) => ({ - id, - alias, - type, - count, - })); - }), - ); - - // Number of livechat rooms with department - statsPms.push( - LivechatRooms.countLivechatRoomsWithDepartment().then((count) => { - statistics.totalLivechatRoomsWithDepartment = count; - }), - ); - - // Number of departments - statsPms.push( - LivechatDepartment.estimatedDocumentCount().then((count) => { - statistics.departments = count; - }), - ); - - // Number of archived departments - statsPms.push( - LivechatDepartment.countArchived().then((count) => { - statistics.archivedDepartments = count; - }), - ); - - // Workspace allows dpeartment removal - statistics.isDepartmentRemovalEnabled = settings.get('Omnichannel_enable_department_removal'); - - // Number of triggers - statsPms.push( - LivechatTrigger.estimatedDocumentCount().then((count) => { - statistics.totalTriggers = count; - }), - ); - - // Number of custom fields - statsPms.push((statistics.totalCustomFields = await LivechatCustomField.estimatedDocumentCount())); - - // Number of public custom fields - statsPms.push((statistics.totalLivechatPublicCustomFields = await LivechatCustomField.countDocuments({ public: true }))); - - // Livechat Automatic forwarding feature enabled - statistics.livechatAutomaticForwardingUnansweredChats = settings.get('Livechat_auto_transfer_chat_timeout') !== 0; - - // Type of routing algorithm used on omnichannel - statistics.routingAlgorithm = settings.get('Livechat_Routing_Method') || ''; - - // is on-hold active - statistics.onHoldEnabled = settings.get('Livechat_allow_manual_on_hold'); - - // Number of Email Inboxes - statsPms.push( - EmailInbox.estimatedDocumentCount().then((count) => { - statistics.emailInboxes = count; - }), - ); - - statsPms.push( - LivechatBusinessHours.estimatedDocumentCount().then((count) => { - statistics.BusinessHours = { - // Number of Business Hours - total: count, - // Business Hours strategy - strategy: settings.get('Livechat_enable_business_hours') || '', - }; - }), - ); - - // Type of routing algorithm used on omnichannel - statistics.routingAlgorithm = settings.get('Livechat_Routing_Method'); - - // is on-hold active - statistics.onHoldEnabled = settings.get('Livechat_allow_manual_on_hold'); - - // Last-Chatted Agent Preferred (enabled/disabled) - statistics.lastChattedAgentPreferred = settings.get('Livechat_last_chatted_agent_routing'); - - // Assign new conversations to the contact manager (enabled/disabled) - statistics.assignNewConversationsToContactManager = settings.get('Omnichannel_contact_manager_routing'); - - // How to handle Visitor Abandonment setting - statistics.visitorAbandonment = settings.get('Livechat_abandoned_rooms_action'); - - // Amount of chats placed on hold - statsPms.push( - Messages.countRoomsWithMessageType('omnichannel_placed_chat_on_hold', { readPreference }).then((total) => { - statistics.chatsOnHold = total; - }), - ); - - const defaultValue = { contactsCount: 0, conversationsCount: 0, sources: [] }; - const billablePeriod = moment.utc().format('YYYY-MM'); - statsPms.push( - LivechatRooms.getMACStatisticsForPeriod(billablePeriod).then(([result]) => { - statistics.omnichannelContactsBySource = result || defaultValue; - }), - ); - - const monthAgo = moment.utc().subtract(30, 'days').toDate(); - const today = moment.utc().toDate(); - statsPms.push( - LivechatRooms.getMACStatisticsBetweenDates(monthAgo, today).then(([result]) => { - statistics.uniqueContactsOfLastMonth = result || defaultValue; - }), - ); - - const weekAgo = moment.utc().subtract(7, 'days').toDate(); - statsPms.push( - LivechatRooms.getMACStatisticsBetweenDates(weekAgo, today).then(([result]) => { - statistics.uniqueContactsOfLastWeek = result || defaultValue; - }), - ); - - const yesterday = moment.utc().subtract(1, 'days').toDate(); - statsPms.push( - LivechatRooms.getMACStatisticsBetweenDates(yesterday, today).then(([result]) => { - statistics.uniqueContactsOfYesterday = result || defaultValue; - }), - ); - - // Message statistics - const channels = await Rooms.findByType('c', { projection: { msgs: 1, prid: 1 } }).toArray(); - const totalChannelDiscussionsMessages = channels.reduce(function _countChannelDiscussionsMessages(num: number, room: IRoom) { - return num + (room.prid ? room.msgs : 0); - }, 0); - statistics.totalChannelMessages = - channels.reduce(function _countChannelMessages(num: number, room: IRoom) { - return num + room.msgs; - }, 0) - totalChannelDiscussionsMessages; - - const privateGroups = await Rooms.findByType('p', { projection: { msgs: 1, prid: 1 } }).toArray(); - const totalPrivateGroupsDiscussionsMessages = privateGroups.reduce(function _countPrivateGroupsDiscussionsMessages( - num: number, - room: IRoom, - ) { - return num + (room.prid ? room.msgs : 0); - }, 0); - statistics.totalPrivateGroupMessages = - privateGroups.reduce(function _countPrivateGroupMessages(num: number, room: IRoom) { - return num + room.msgs; - }, 0) - totalPrivateGroupsDiscussionsMessages; - - statistics.totalDiscussionsMessages = totalPrivateGroupsDiscussionsMessages + totalChannelDiscussionsMessages; - - statistics.totalDirectMessages = (await Rooms.findByType('d', { projection: { msgs: 1 } }).toArray()).reduce( - function _countDirectMessages(num: number, room: IRoom) { - return num + room.msgs; - }, - 0, - ); - statistics.totalLivechatMessages = (await Rooms.findByType('l', { projection: { msgs: 1 } }).toArray()).reduce( - function _countLivechatMessages(num: number, room: IRoom) { - return num + room.msgs; - }, - 0, - ); - statistics.totalMessages = - statistics.totalChannelMessages + - statistics.totalPrivateGroupMessages + - statistics.totalDiscussionsMessages + - statistics.totalDirectMessages + - statistics.totalLivechatMessages; - - statistics.lastLogin = (await Users.getLastLogin())?.toString() || ''; - statistics.lastMessageSentAt = await Messages.getLastTimestamp(); - statistics.lastSeenSubscription = (await Subscriptions.getLastSeen())?.toString() || ''; - - statistics.os = { - type: os.type(), - platform: os.platform(), - arch: os.arch(), - release: os.release(), - uptime: os.uptime(), - loadavg: os.loadavg(), - totalmem: os.totalmem(), - freemem: os.freemem(), - cpus: os.cpus(), - }; - - statistics.process = { - nodeVersion: process.version, - pid: process.pid, - uptime: process.uptime(), - }; - - statistics.deploy = { - method: process.env.DEPLOY_METHOD || 'tar', - platform: process.env.DEPLOY_PLATFORM || 'selfinstall', - }; - - statistics.readReceiptsEnabled = settings.get('Message_Read_Receipt_Enabled'); - statistics.readReceiptsDetailed = settings.get('Message_Read_Receipt_Store_Users'); - - statistics.enterpriseReady = true; - statsPms.push( - Uploads.estimatedDocumentCount().then((count) => { - statistics.uploadsTotal = count; - }), - ); - statsPms.push( - Uploads.col - .aggregate( - [ - { - $group: { _id: 'total', total: { $sum: '$size' } }, - }, - ], - { readPreference }, - ) - .toArray() - .then((agg) => { - const [result] = agg; - statistics.uploadsTotalSize = result ? (result as any).total : 0; - }), - ); - - statistics.fileStoreType = settings.get('FileUpload_Storage_Type'); - - statistics.migration = await getControl(); - statsPms.push( - InstanceStatus.countDocuments({ _updatedAt: { $gt: new Date(Date.now() - process.uptime() * 1000 - 2000) } }).then((count) => { - statistics.instanceCount = count; - }), - ); - - const { mongoVersion, mongoStorageEngine } = await getMongoInfo(); - statistics.msEnabled = isRunningMs(); - statistics.oplogEnabled = false; - statistics.mongoVersion = mongoVersion; - statistics.mongoStorageEngine = mongoStorageEngine || ''; - - statsPms.push( - Sessions.getUniqueUsersOfYesterday().then((result) => { - statistics.uniqueUsersOfYesterday = result; - }), - ); - statsPms.push( - Sessions.getUniqueUsersOfLastWeek().then((result) => { - statistics.uniqueUsersOfLastWeek = result; - }), - ); - statsPms.push( - Sessions.getUniqueUsersOfLastMonth().then((result) => { - statistics.uniqueUsersOfLastMonth = result; - }), - ); - statsPms.push( - Sessions.getUniqueDevicesOfYesterday().then((result) => { - statistics.uniqueDevicesOfYesterday = result; - }), - ); - statsPms.push( - Sessions.getUniqueDevicesOfLastWeek().then((result) => { - statistics.uniqueDevicesOfLastWeek = result; - }), - ); - statsPms.push( - Sessions.getUniqueDevicesOfLastMonth().then((result) => { - statistics.uniqueDevicesOfLastMonth = result; - }), - ); - statsPms.push( - Sessions.getUniqueOSOfYesterday().then((result) => { - statistics.uniqueOSOfYesterday = result; - }), - ); - statsPms.push( - Sessions.getUniqueOSOfLastWeek().then((result) => { - statistics.uniqueOSOfLastWeek = result; - }), - ); - statsPms.push( - Sessions.getUniqueOSOfLastMonth().then((result) => { - statistics.uniqueOSOfLastMonth = result; - }), - ); - - statistics.apps = await getAppsStatistics(); - statistics.services = await getServicesStatistics(); - statistics.importer = getImporterStatistics(); - statistics.videoConf = await VideoConf.getStatistics(); - statistics.contactVerification = await getContactVerificationStatistics(); - - // If getSettingsStatistics() returns an error, save as empty object. - statsPms.push( - getSettingsStatistics().then((res) => { - const settingsStatisticsObject = res || {}; - statistics.settings = settingsStatisticsObject; - }), - ); - - statsPms.push( - Integrations.find( - {}, - { - projection: { - _id: 0, - type: 1, - enabled: 1, - scriptEnabled: 1, - }, - readPreference, - }, - ) - .toArray() - .then((found) => { - const integrations = found; - - statistics.integrations = { - totalIntegrations: integrations.length, - totalIncoming: integrations.filter((integration) => integration.type === 'webhook-incoming').length, - totalIncomingActive: integrations.filter( - (integration) => integration.enabled === true && integration.type === 'webhook-incoming', - ).length, - totalOutgoing: integrations.filter((integration) => integration.type === 'webhook-outgoing').length, - totalOutgoingActive: integrations.filter( - (integration) => integration.enabled === true && integration.type === 'webhook-outgoing', - ).length, - totalWithScriptEnabled: integrations.filter((integration) => integration.scriptEnabled === true).length, - }; - }), - ); - - statsPms.push( - NotificationQueue.estimatedDocumentCount().then((count) => { - statistics.pushQueue = count; - }), - ); - - statsPms.push( - getEnterpriseStatistics().then((result) => { - statistics.enterprise = result; - }), - ); - - statsPms.push( - Team.getStatistics().then((result) => { - statistics.teams = result; - }), - ); - - statsPms.push(Analytics.resetSeatRequestCount()); - - // TODO: Is that the best way to do this? maybe we should use a promise.all() - - statistics.dashboardCount = settings.get('Engagement_Dashboard_Load_Count'); - statistics.messageAuditApply = settings.get('Message_Auditing_Apply_Count'); - statistics.messageAuditLoad = settings.get('Message_Auditing_Panel_Load_Count'); - statistics.joinJitsiButton = settings.get('Jitsi_Click_To_Join_Count'); - statistics.slashCommandsJitsi = settings.get('Jitsi_Start_SlashCommands_Count'); - statistics.totalBroadcastRooms = await Rooms.countByBroadcast({ readPreference }); - statistics.totalTriggeredEmails = settings.get('Triggered_Emails_Count'); - statistics.totalRoomsWithStarred = await Messages.countRoomsWithStarredMessages({ readPreference }); - statistics.totalRoomsWithPinned = await Messages.countRoomsWithPinnedMessages({ readPreference }); - statistics.totalUserTOTP = await Users.countActiveUsersTOTPEnable({ readPreference }); - statistics.totalUserEmail2fa = await Users.countActiveUsersEmail2faEnable({ readPreference }); - statistics.totalPinned = await Messages.countPinned({ readPreference }); - statistics.totalStarred = await Messages.countStarred({ readPreference }); - statistics.totalLinkInvitation = await Invites.estimatedDocumentCount(); - statistics.totalLinkInvitationUses = await Invites.countUses(); - statistics.totalEmailInvitation = settings.get('Invitation_Email_Count'); - statistics.totalE2ERooms = await Rooms.countByE2E({ readPreference }); - statistics.logoChange = Object.keys(settings.get('Assets_logo') || {}).includes('url'); - statistics.showHomeButton = settings.get('Layout_Show_Home_Button'); - statistics.totalEncryptedMessages = await Messages.countByType('e2e', { readPreference }); - statistics.totalManuallyAddedUsers = settings.get('Manual_Entry_User_Count'); - statistics.totalSubscriptionRoles = await RolesRaw.countByScope('Subscriptions', { readPreference }); - statistics.totalUserRoles = await RolesRaw.countByScope('Users', { readPreference }); - statistics.totalCustomRoles = await RolesRaw.countCustomRoles({ readPreference }); - statistics.totalWebRTCCalls = settings.get('WebRTC_Calls_Count'); - statistics.uncaughtExceptionsCount = settings.get('Uncaught_Exceptions_Count'); - - const defaultGateway = (await Settings.findOneById('Push_gateway', { projection: { packageValue: 1 } }))?.packageValue; - - // Push notification stats - // one bit for each of the following: - const pushEnabled = settings.get('Push_enable') ? 1 : 0; - const pushGatewayEnabled = settings.get('Push_enable_gateway') ? 2 : 0; - const pushGatewayChanged = settings.get('Push_gateway') !== defaultGateway ? 4 : 0; - - statistics.push = pushEnabled | pushGatewayEnabled | pushGatewayChanged; - statistics.pushSecured = settings.get('Push_request_content_from_server'); - - const defaultHomeTitle = (await Settings.findOneById('Layout_Home_Title'))?.packageValue; - statistics.homeTitleChanged = settings.get('Layout_Home_Title') !== defaultHomeTitle; - - const defaultHomeBody = (await Settings.findOneById('Layout_Home_Body'))?.packageValue; - statistics.homeBodyChanged = settings.get('Layout_Home_Body') !== defaultHomeBody; - - const defaultCustomCSS = (await Settings.findOneById('theme-custom-css'))?.packageValue; - statistics.customCSSChanged = settings.get('theme-custom-css') !== defaultCustomCSS; - - const defaultOnLogoutCustomScript = (await Settings.findOneById('Custom_Script_On_Logout'))?.packageValue; - statistics.onLogoutCustomScriptChanged = settings.get('Custom_Script_On_Logout') !== defaultOnLogoutCustomScript; - - const defaultLoggedOutCustomScript = (await Settings.findOneById('Custom_Script_Logged_Out'))?.packageValue; - statistics.loggedOutCustomScriptChanged = settings.get('Custom_Script_Logged_Out') !== defaultLoggedOutCustomScript; - - const defaultLoggedInCustomScript = (await Settings.findOneById('Custom_Script_Logged_In'))?.packageValue; - statistics.loggedInCustomScriptChanged = settings.get('Custom_Script_Logged_In') !== defaultLoggedInCustomScript; - - try { - statistics.dailyPeakConnections = await Presence.getPeakConnections(true); - } catch { - statistics.dailyPeakConnections = 0; - } - - const peak = await Statistics.findMonthlyPeakConnections(); - statistics.maxMonthlyPeakConnections = Math.max(statistics.dailyPeakConnections, peak?.dailyPeakConnections || 0); - - statistics.matrixFederation = await getMatrixFederationStatistics(); - - // ABAC stats - if (License.hasModule('abac')) { - statistics.abacEnabled = settings.get('ABAC_Enabled'); - statsPms.push( - AbacAttributes.estimatedDocumentCount().then((result) => { - statistics.abacTotalAttributes = result; - }), - ); - statsPms.push( - AbacAttributes.countTotalValues().then((result) => { - statistics.abacTotalAttributeValues = result; - }), - ); - statsPms.push( - Rooms.countAbacEnabled().then((result) => { - statistics.abacRoomsEnrolled = result; - }), - ); - } - - statistics.allowUnsafeQueryAndFieldsApiParamsEnabled = process.env.ALLOW_UNSAFE_QUERY_AND_FIELDS_API_PARAMS?.toUpperCase() === 'TRUE'; - - await Promise.all(statsPms).catch(log); - - return statistics; - }, - async save(): Promise { - const rcStatistics = await statistics.get(); - rcStatistics.createdAt = new Date(); - const { insertedId } = await Statistics.insertOne(rcStatistics); - rcStatistics._id = insertedId; - - return rcStatistics; - }, -}; diff --git a/apps/meteor/app/statistics/server/methods/getStatistics.ts b/apps/meteor/app/statistics/server/methods/getStatistics.ts deleted file mode 100644 index 1b383bdc54536..0000000000000 --- a/apps/meteor/app/statistics/server/methods/getStatistics.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { IStats } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Meteor } from 'meteor/meteor'; - -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { getLastStatistics } from '../functions/getLastStatistics'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - getStatistics(refresh?: boolean): IStats; - } -} - -Meteor.methods({ - async getStatistics(refresh) { - methodDeprecationLogger.method('getStatistics', '9.0.0', '/v1/statistics'); - const uid = Meteor.userId(); - if (!uid) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getStatistics' }); - } - return getLastStatistics({ - userId: uid, - refresh, - }); - }, -}); diff --git a/apps/meteor/app/theme/client/imports/components/emoji.css b/apps/meteor/app/theme/client/imports/components/emoji.css index 58668075584d0..d0a78e0296531 100644 --- a/apps/meteor/app/theme/client/imports/components/emoji.css +++ b/apps/meteor/app/theme/client/imports/components/emoji.css @@ -1,17 +1,33 @@ +/* Global emoji styles */ .emoji { - position: relative; - display: inline-block; - overflow: hidden; - width: 1.375rem; - height: 1.375rem; - margin: 0 0.15em; - vertical-align: middle; - white-space: nowrap; - text-indent: 100%; - background-repeat: no-repeat; - background-position: center; - background-size: contain; - font-size: inherit; - line-height: normal; - image-rendering: auto; + font-size: 1.375rem; + line-height: 1.5rem; + + /* custom emojis */ + &--custom { + height: 1.375rem; + width: 1.375rem; + overflow: hidden; + position: relative; + text-indent: 100%; + vertical-align: bottom; + display: inline-block; + margin: 0 .15em; + background-position: 50%; + background-repeat: no-repeat; + background-size: contain; + } +} + +/* emoji picker */ +.rcx-button .emoji { + font-size: 1.375rem; + line-height: 1.5rem; +} + +.rcx-message { + /* emojis on messages */ + &__emoji { + font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', 'Twemoji Mozilla', 'Android Emoji', sans-serif; + } } diff --git a/apps/meteor/app/theme/server/server.ts b/apps/meteor/app/theme/server/server.ts deleted file mode 100644 index 34b71e38a303d..0000000000000 --- a/apps/meteor/app/theme/server/server.ts +++ /dev/null @@ -1,43 +0,0 @@ -import crypto from 'node:crypto'; - -import { Settings } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; -import { WebApp } from 'meteor/webapp'; - -import { settings } from '../../settings/server'; -import { addStyle } from '../../ui-master/server/inject'; - -settings.watch('theme-custom-css', (value) => { - if (!value || typeof value !== 'string') { - addStyle('css-theme', ''); - return; - } - addStyle('css-theme', value); -}); - -// TODO: Add a migration to remove this setting from the database -Meteor.startup(async () => { - await Settings.deleteMany({ _id: /theme-color/ }); - await Settings.deleteOne({ _id: /theme-font/ }); - await Settings.deleteOne({ _id: 'css' }); -}); - -WebApp.rawConnectHandlers.use((req, res, next) => { - const path = req.url?.split('?')[0]; - const prefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ''; - - if (path !== `${prefix}/theme.css`) { - next(); - return; - } - - const style = settings.get('theme-custom-css'); - if (typeof style !== 'string') { - throw new Error('Invalid theme-custom-css setting'); - } - - res.setHeader('Content-Type', 'text/css; charset=UTF-8'); - res.setHeader('Content-Length', style.length); - res.setHeader('ETag', `"${crypto.createHash('sha1').update(style).digest('hex')}"`); - res.end(style, 'utf-8'); -}); diff --git a/apps/meteor/app/threads/server/hooks/aftersavemessage.ts b/apps/meteor/app/threads/server/hooks/aftersavemessage.ts deleted file mode 100644 index fd63a30d64c69..0000000000000 --- a/apps/meteor/app/threads/server/hooks/aftersavemessage.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; -import { isEditedMessage } from '@rocket.chat/core-typings'; -import { Messages } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { callbacks } from '../../../../server/lib/callbacks'; -import type { SendMessageOptions } from '../../../lib/server/functions/sendMessage'; -import { notifyOnMessageChange } from '../../../lib/server/lib/notifyListener'; -import { updateThreadUsersSubscriptions, getMentions } from '../../../lib/server/lib/notifyUsersOnMessage'; -import { sendMessageNotifications } from '../../../lib/server/lib/sendNotificationsOnMessage'; -import { settings } from '../../../settings/server'; -import { reply } from '../functions'; - -async function notifyUsersOnReply(message: IMessage, replies: IUser['_id'][]) { - // skips this callback if the message was edited - if (isEditedMessage(message)) { - return message; - } - - await updateThreadUsersSubscriptions(message, replies); - - return message; -} - -async function metaData(message: IMessage, parentMessage: IMessage, followers: string[]) { - await reply({ tmid: message.tmid }, message, parentMessage, followers); - - return message; -} - -const notification = async (message: IMessage, room: IRoom, replies: string[]) => { - // skips this callback if the message was edited - if (isEditedMessage(message)) { - return message; - } - - // will send a notification to everyone who replied/followed the thread except the owner of the message - await sendMessageNotifications(message, room, replies); - - return message; -}; - -export async function processThreads(message: IMessage, room: IRoom, options?: SendMessageOptions) { - if (!message.tmid) { - return message; - } - - const parentMessage = await Messages.findOneById(message.tmid); - if (!parentMessage) { - return message; - } - - const { mentionIds } = await getMentions(message); - - const replies = [ - ...new Set([ - ...((!parentMessage.tcount ? [parentMessage.u._id] : parentMessage.replies) || []), - ...(!parentMessage.tcount && room.t === 'd' && room.uids ? room.uids : []), - ...mentionIds, - ]), - ].filter((userId) => userId !== message.u._id); - - await notifyUsersOnReply(message, replies); - await metaData(message, parentMessage, replies); - if (!options?.skipNotifications) { - await notification(message, room, replies); - } - void notifyOnMessageChange({ - id: message.tmid, - }); - - return message; -} - -Meteor.startup(() => { - settings.watch('Threads_enabled', (value) => { - if (!value) { - callbacks.remove('afterSaveMessage', 'threads-after-save-message'); - return; - } - callbacks.add( - 'afterSaveMessage', - async (message, { room, options }) => { - return processThreads(message, room, options); - }, - callbacks.priority.LOW, - 'threads-after-save-message', - ); - }); -}); diff --git a/apps/meteor/app/threads/server/hooks/index.ts b/apps/meteor/app/threads/server/hooks/index.ts deleted file mode 100644 index b1be2eb6f38af..0000000000000 --- a/apps/meteor/app/threads/server/hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './aftersavemessage'; diff --git a/apps/meteor/app/threads/server/index.ts b/apps/meteor/app/threads/server/index.ts deleted file mode 100644 index 214190032e0d4..0000000000000 --- a/apps/meteor/app/threads/server/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './hooks'; -import './methods'; diff --git a/apps/meteor/app/threads/server/methods/index.ts b/apps/meteor/app/threads/server/methods/index.ts deleted file mode 100644 index 4e6ed3003b11f..0000000000000 --- a/apps/meteor/app/threads/server/methods/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import './followMessage'; -import './getThreadMessages'; -import './getThreadsList'; -import './unfollowMessage'; diff --git a/apps/meteor/app/token-login/server/index.ts b/apps/meteor/app/token-login/server/index.ts deleted file mode 100644 index 3cb9f1aac0b55..0000000000000 --- a/apps/meteor/app/token-login/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './login_token_server'; diff --git a/apps/meteor/app/ui-master/server/index.ts b/apps/meteor/app/ui-master/server/index.ts deleted file mode 100644 index 24a673b3b9b85..0000000000000 --- a/apps/meteor/app/ui-master/server/index.ts +++ /dev/null @@ -1,170 +0,0 @@ -import type { ISettingColor } from '@rocket.chat/core-typings'; -import { Settings } from '@rocket.chat/models'; -import { escapeHTML } from '@rocket.chat/string-helpers'; -import { Meteor } from 'meteor/meteor'; -import { Inject } from 'meteor/meteorhacks:inject-initial'; -import { Tracker } from 'meteor/tracker'; - -import { applyHeadInjections, headInjections, injectIntoBody, injectIntoHead } from './inject'; -import { getMessageMaxParseLength } from '../../../lib/getMessageMaxParseLength'; -import { withDebouncing } from '../../../lib/utils/highOrderFunctions'; -import { settings } from '../../settings/server'; -import { getURL } from '../../utils/server/getURL'; - -import './scripts'; - -export * from './inject'; - -Meteor.startup(() => { - Tracker.autorun(() => { - const injections = Object.values(headInjections.all()).filter((injection): injection is NonNullable => !!injection); - Inject.rawModHtml('headInjections', applyHeadInjections(injections)); - }); - - settings.watch('Default_Referrer_Policy', (value) => { - if (!value) { - return injectIntoHead('noreferrer', ''); - } - - injectIntoHead('noreferrer', ``); - }); - - settings.watch('Use_RC_SDK', (value) => { - injectIntoHead('Use_RC_SDK', ``); - }); - - if (process.env.DISABLE_ANIMATION) { - injectIntoHead( - 'disable-animation', - ` - - `, - ); - } - - settings.watch('Assets_SvgFavicon_Enable', (value) => { - const standardFavicons = ` - - `; - - if (value) { - injectIntoHead( - 'Assets_SvgFavicon_Enable', - `${standardFavicons} - `, - ); - } else { - injectIntoHead('Assets_SvgFavicon_Enable', standardFavicons); - } - }); - - settings.watch('theme-color-sidebar-background', (value) => { - const escapedValue = escapeHTML(value); - injectIntoHead( - 'theme-color-sidebar-background', - ``, - ); - }); - - settings.watch('Site_Name', (value = 'Rocket.Chat') => { - const escapedValue = escapeHTML(value); - injectIntoHead( - 'Site_Name', - `${escapedValue}` + - `` + - ``, - ); - }); - - settings.watch('Meta_language', (value = '') => { - const escapedValue = escapeHTML(value); - injectIntoHead( - 'Meta_language', - ``, - ); - }); - - settings.watch('Meta_robots', (value = '') => { - const escapedValue = escapeHTML(value); - injectIntoHead('Meta_robots', ``); - }); - - settings.watch('Meta_msvalidate01', (value = '') => { - const escapedValue = escapeHTML(value); - injectIntoHead('Meta_msvalidate01', ``); - }); - - settings.watch('Meta_google-site-verification', (value = '') => { - const escapedValue = escapeHTML(value); - injectIntoHead('Meta_google-site-verification', ``); - }); - - settings.watch('Meta_fb_app_id', (value = '') => { - const escapedValue = escapeHTML(value); - injectIntoHead('Meta_fb_app_id', ``); - }); - - settings.watch('Meta_custom', (value = '') => { - injectIntoHead('Meta_custom', value); - }); - - const baseUrl = ((prefix) => { - if (!prefix) { - return '/'; - } - - prefix = prefix.trim(); - - if (!prefix) { - return '/'; - } - - return /\/$/.test(prefix) ? prefix : `${prefix}/`; - })(__meteor_runtime_config__.ROOT_URL_PATH_PREFIX); - - injectIntoHead('base', ``); - - const escapedMessageMaxParseLength = escapeHTML(String(getMessageMaxParseLength())); - injectIntoHead('MESSAGE_MAX_PARSE_LENGTH', ``); -}); - -const renderDynamicCssList = withDebouncing({ wait: 500 })(async () => { - // const variables = RocketChat.models.Settings.findOne({_id:'theme-custom-variables'}, {fields: { value: 1}}); - const colors = await Settings.find({ _id: /theme-color-rc/i }, { projection: { value: 1, editor: 1 } }).toArray(); - const css = colors - .filter((color): color is ISettingColor => !!color?.value) - .map(({ _id, value, editor }) => { - if (editor === 'expression') { - return `--${_id.replace('theme-color-', '')}: var(--${value});`; - } - return `--${_id.replace('theme-color-', '')}: ${value};`; - }) - .join('\n'); - injectIntoBody('dynamic-variables', ``); -}); - -await renderDynamicCssList(); - -settings.watchByRegex(/theme-color-rc/i, renderDynamicCssList); - -injectIntoBody( - 'react-root', - ` - -
- -
-`, -); diff --git a/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx b/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx index 58edd971d1e1c..963580143aca8 100644 --- a/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx +++ b/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { isValidLink } from '../../../../client/views/room/MessageList/lib/isValidLink'; -type AddLinkComposerActionModalProps = { +export type AddLinkComposerActionModalProps = { selectedText?: string; onConfirm: (url: string, text: string) => void; onClose: () => void; diff --git a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.spec.ts b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.spec.ts new file mode 100644 index 0000000000000..ccb42a88a94a7 --- /dev/null +++ b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.spec.ts @@ -0,0 +1,61 @@ +import { createComposerAPI } from './createComposerAPI'; + +jest.mock('../../../../client/lib/chats/uploads', () => ({ + createUploadsAPI: () => ({}), +})); + +const setupComposer = (initialValue: string, cursor: { start: number; end: number }) => { + const input = document.createElement('textarea'); + document.body.appendChild(input); + + const composer = createComposerAPI(input, jest.fn(), '', Number.MAX_SAFE_INTEGER, { current: null }, { rid: 'GENERAL' }); + + input.value = initialValue; + input.setSelectionRange(cursor.start, cursor.end); + + return { composer, input }; +}; + +describe('ChatMessages Composer API - replaceText', () => { + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('should place the cursor right after the mention when inserting at the start of the message', () => { + const { composer, input } = setupComposer('@jhello', { start: 2, end: 2 }); + + composer.replaceText('@john ', { start: 0, end: 2 }); + + expect(input.value).toBe('@john hello'); + expect(input.selectionStart).toBe('@john '.length); + expect(input.selectionEnd).toBe('@john '.length); + }); + + it('should place the cursor right after the mention when inserting in the middle of the message', () => { + const { composer, input } = setupComposer('hi @jthere', { start: 5, end: 5 }); + + composer.replaceText('@john ', { start: 3, end: 5 }); + + expect(input.value).toBe('hi @john there'); + expect(input.selectionStart).toBe('hi @john '.length); + expect(input.selectionEnd).toBe('hi @john '.length); + }); + + it('should place the cursor right after the mention when inserting at the end of the message', () => { + const { composer, input } = setupComposer('hello @j', { start: 8, end: 8 }); + + composer.replaceText('@john ', { start: 6, end: 8 }); + + expect(input.value).toBe('hello @john '); + expect(input.selectionStart).toBe('hello @john '.length); + expect(input.selectionEnd).toBe('hello @john '.length); + }); + + it('should keep the cursor collapsed right after the inserted text', () => { + const { composer, input } = setupComposer('@jhello', { start: 2, end: 2 }); + + composer.replaceText('@john ', { start: 0, end: 2 }); + + expect(input.selectionStart).toBe(input.selectionEnd); + }); +}); diff --git a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts index 2018abfa5fa91..b54885d2d6c47 100644 --- a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts +++ b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts @@ -288,8 +288,6 @@ export const createComposerAPI = ( // Gets the text that is connected to the cursor and replaces it with the given text const replaceText = (text: string, selection: { readonly start: number; readonly end: number }): void => { - const { selectionStart, selectionEnd } = input; - // Selects the text that is connected to the cursor input.setSelectionRange(selection.start ?? 0, selection.end ?? text.length); const textAreaTxt = input.value; @@ -298,11 +296,9 @@ export const createComposerAPI = ( input.value = textAreaTxt.substring(0, selection.start) + text + textAreaTxt.substring(selection.end); } - input.selectionStart = selectionStart + text.length; - input.selectionEnd = selectionStart + text.length; - if (selectionStart !== selectionEnd) { - input.selectionStart = selectionStart; - } + const cursorPosition = (selection.start ?? 0) + text.length; + input.selectionStart = cursorPosition; + input.selectionEnd = cursorPosition; triggerEvent(input, 'input'); triggerEvent(input, 'change'); diff --git a/apps/meteor/app/ui-utils/server/index.ts b/apps/meteor/app/ui-utils/server/index.ts deleted file mode 100644 index f2d89d22fcc40..0000000000000 --- a/apps/meteor/app/ui-utils/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Message } from './Message'; diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index a6febf3fdfef9..5bf8cf8693eb6 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -1,4 +1,4 @@ -import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { DeepWritable, IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { isVideoConfMessage } from '@rocket.chat/core-typings'; import type { IActionManager } from '@rocket.chat/ui-contexts'; @@ -18,12 +18,6 @@ import { uploadFiles } from '../../../../client/lib/chats/flows/uploadFiles'; import { ReadStateManager } from '../../../../client/lib/chats/readStateManager'; import { setHighlightMessage } from '../../../../client/views/room/MessageList/providers/messageHighlightSubscription'; -type DeepWritable = T extends (...args: any) => any - ? T - : { - -readonly [P in keyof T]: DeepWritable; - }; - export class ChatMessages implements ChatAPI { public uid: string | undefined; diff --git a/apps/meteor/app/user-status/server/index.ts b/apps/meteor/app/user-status/server/index.ts deleted file mode 100644 index b1622098fa0a0..0000000000000 --- a/apps/meteor/app/user-status/server/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import './methods/deleteCustomUserStatus'; -import './methods/insertOrUpdateUserStatus'; -import './methods/listCustomUserStatus'; -import './methods/setUserStatus'; -import './methods/getUserStatusText'; diff --git a/apps/meteor/app/utils/client/getURL.ts b/apps/meteor/app/utils/client/getURL.ts index 0a292e6009589..2c50225e0da53 100644 --- a/apps/meteor/app/utils/client/getURL.ts +++ b/apps/meteor/app/utils/client/getURL.ts @@ -3,7 +3,7 @@ import { getURLWithoutSettings } from '../lib/getURL'; import { Info } from '../rocketchat.info'; export const getURL = function ( - path: string, // eslint-disable-next-line @typescript-eslint/naming-convention + path: string, params: { cdn?: boolean; full?: boolean; diff --git a/apps/meteor/app/utils/lib/getURL.ts b/apps/meteor/app/utils/lib/getURL.ts index ed510767311da..5cbf9f2d670ca 100644 --- a/apps/meteor/app/utils/lib/getURL.ts +++ b/apps/meteor/app/utils/lib/getURL.ts @@ -37,7 +37,7 @@ function getCloudUrl( export const _getURL = ( path: string, - // eslint-disable-next-line @typescript-eslint/naming-convention + { cdn, full, cloud, cloud_route, cloud_params, _cdn_prefix, _root_url_path_prefix, _site_url }: Record, deeplinkUrl?: string, ): string => { @@ -76,7 +76,7 @@ export const _getURL = ( export const getURLWithoutSettings = ( path: string, - // eslint-disable-next-line @typescript-eslint/naming-convention + { cdn = true, full = false, diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 3516532a3cc73..daeddd14cccff 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "8.6.0" + "version": "9.0.0-develop" } diff --git a/apps/meteor/app/utils/server/getURL.ts b/apps/meteor/app/utils/server/getURL.ts deleted file mode 100644 index 4703569736add..0000000000000 --- a/apps/meteor/app/utils/server/getURL.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { settings } from '../../settings/server'; -import { getURLWithoutSettings } from '../lib/getURL'; - -export const getURL = function ( - path: string, // eslint-disable-next-line @typescript-eslint/naming-convention - params: { - cdn?: boolean; - full?: boolean; - cloud?: boolean; - cloud_route?: string; - cloud_params?: Record; - } = {}, - cloudDeepLinkUrl?: string, -): string { - const cdnPrefix = settings.get('CDN_PREFIX') || ''; - const siteUrl = settings.get('Site_Url') || ''; - - return getURLWithoutSettings(path, params, cdnPrefix, siteUrl, cloudDeepLinkUrl); -}; diff --git a/apps/meteor/app/utils/server/restrictions.ts b/apps/meteor/app/utils/server/restrictions.ts deleted file mode 100644 index 6eb1c9a655d4a..0000000000000 --- a/apps/meteor/app/utils/server/restrictions.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { settings } from '../../settings/server'; -import { fileUploadIsValidContentTypeFromSettings } from '../lib/restrictions'; - -export const fileUploadIsValidContentType = function (type: string | undefined, customWhiteList?: string): boolean { - const blackList = settings.get('FileUpload_MediaTypeBlackList'); - const whiteList = customWhiteList || settings.get('FileUpload_MediaTypeWhiteList'); - - return fileUploadIsValidContentTypeFromSettings(type, whiteList, blackList); -}; diff --git a/apps/meteor/app/version-check/server/index.ts b/apps/meteor/app/version-check/server/index.ts deleted file mode 100644 index 45e9c5197c305..0000000000000 --- a/apps/meteor/app/version-check/server/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { cronJobs } from '@rocket.chat/cron'; -import { Meteor } from 'meteor/meteor'; - -import { checkVersionUpdate } from './functions/checkVersionUpdate'; -import { settings } from '../../settings/server'; -import './methods/banner_dismiss'; - -const jobName = 'version_check'; - -if (await cronJobs.has(jobName)) { - await cronJobs.remove(jobName); -} - -const addVersionCheckJob = async () => { - await cronJobs.add(jobName, '0 2 * * *', async () => checkVersionUpdate()); -}; - -Meteor.startup(() => { - setImmediate(() => { - if (settings.get('Update_EnableChecker')) { - void checkVersionUpdate(); - } - }); -}); - -settings.watch('Update_EnableChecker', async () => { - const checkForUpdates = settings.get('Update_EnableChecker'); - - if (checkForUpdates && (await cronJobs.has(jobName))) { - return; - } - - if (checkForUpdates) { - await addVersionCheckJob(); - return; - } - - await cronJobs.remove(jobName); -}); diff --git a/apps/meteor/app/webdav/README.md b/apps/meteor/app/webdav/README.md deleted file mode 100644 index 9200308057830..0000000000000 --- a/apps/meteor/app/webdav/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# RocketChat WebDAV - -Package for RocketChat users to interact with WebDAV servers (Tested with ownCloud and Nextcloud). diff --git a/apps/meteor/app/webdav/server/index.ts b/apps/meteor/app/webdav/server/index.ts deleted file mode 100644 index 36bdfcae93324..0000000000000 --- a/apps/meteor/app/webdav/server/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import './methods/addWebdavAccount'; -import './methods/getWebdavFileList'; -import './methods/getWebdavFilePreview'; -import './methods/getFileFromWebdav'; -import './methods/uploadFileToWebdav'; diff --git a/apps/meteor/app/webdav/server/lib/getWebdavCredentials.ts b/apps/meteor/app/webdav/server/lib/getWebdavCredentials.ts deleted file mode 100644 index 5c12c5b676f17..0000000000000 --- a/apps/meteor/app/webdav/server/lib/getWebdavCredentials.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { WebDAVClientOptions } from 'webdav'; - -export function getWebdavCredentials(account: WebDAVClientOptions): WebDAVClientOptions { - const cred = account.token - ? { token: account.token } - : { - username: account.username, - password: account.password, - }; - return cred; -} diff --git a/apps/meteor/app/webdav/server/lib/uploadFileToWebdav.ts b/apps/meteor/app/webdav/server/lib/uploadFileToWebdav.ts deleted file mode 100644 index e7dc0df2cc632..0000000000000 --- a/apps/meteor/app/webdav/server/lib/uploadFileToWebdav.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { IWebdavAccount } from '@rocket.chat/core-typings'; -import { WebdavAccounts } from '@rocket.chat/models'; - -import { getWebdavCredentials } from './getWebdavCredentials'; -import { WebdavClientAdapter } from './webdavClientAdapter'; - -export const uploadFileToWebdav = async (accountId: IWebdavAccount['_id'], fileData: string | Buffer, name: string): Promise => { - const account = await WebdavAccounts.findOneById(accountId); - if (!account) { - throw new Error('error-invalid-account'); - } - - const uploadFolder = 'Rocket.Chat Uploads/'; - const buffer = Buffer.isBuffer(fileData) ? fileData : Buffer.from(fileData); - - const cred = getWebdavCredentials(account); - const client = new WebdavClientAdapter(account.serverURL, cred); - // eslint-disable-next-line @typescript-eslint/no-empty-function - await client.createDirectory(uploadFolder).catch(() => {}); - await client.putFileContents(`${uploadFolder}/${name}`, buffer, { overwrite: false }); -}; diff --git a/apps/meteor/app/webdav/server/lib/webdavClientAdapter.ts b/apps/meteor/app/webdav/server/lib/webdavClientAdapter.ts deleted file mode 100644 index c3d7df2330509..0000000000000 --- a/apps/meteor/app/webdav/server/lib/webdavClientAdapter.ts +++ /dev/null @@ -1,91 +0,0 @@ -import stream from 'node:stream'; -import type { Readable, Writable } from 'node:stream'; - -import type { WebDAVClient, FileStat, ResponseDataDetailed, WebDAVClientOptions } from 'webdav'; -import { createClient } from 'webdav'; - -export class WebdavClientAdapter { - _client: WebDAVClient; - - constructor(serverConfig: string, cred: WebDAVClientOptions) { - if (cred.token) { - this._client = createClient(serverConfig, { token: cred.token }); - } else { - this._client = createClient(serverConfig, { - username: cred.username, - password: cred.password, - }); - } - } - - async stat(path: string): Promise> { - try { - return await this._client.stat(path); - } catch (error: any) { - throw new Error(error.response?.statusText ? error.response.statusText : 'Error checking if directory exists on webdav'); - } - } - - async createDirectory(path: string): Promise { - try { - return await this._client.createDirectory(path); - } catch (error: any) { - throw new Error(error.response?.statusText ? error.response.statusText : 'Error creating directory on webdav'); - } - } - - async deleteFile(path: string): Promise { - try { - return await this._client.deleteFile(path); - } catch (error: any) { - throw new Error(error.response?.statusText ? error.response.statusText : 'Error deleting file on webdav'); - } - } - - async getFileContents(filename: string): Promise { - try { - return (await this._client.getFileContents(filename)) as Buffer; - } catch (error: any) { - throw new Error(error.response?.statusText ? error.response.statusText : 'Error getting file contents webdav'); - } - } - - async getDirectoryContents(path: string): Promise> { - try { - return await this._client.getDirectoryContents(path); - } catch (error: any) { - throw new Error(error.response?.statusText ? error.response.statusText : 'Error getting directory contents webdav'); - } - } - - async putFileContents(path: string, data: Buffer, options: Record = {}): Promise { - try { - return await this._client.putFileContents(path, data, options); - } catch (error: any) { - throw new Error(error.response?.statusText ?? 'Error updating file contents.'); - } - } - - createReadStream(path: string, options?: Record): Readable { - return this._client.createReadStream(path, options); - } - - createWriteStream(path: string, fileSize: number): Writable { - const ws = new stream.PassThrough(); - - this._client - .customRequest(path, { - method: 'PUT', - headers: { - ...(fileSize ? { 'Content-Length': String(fileSize) } : {}), - }, - data: ws, - maxRedirects: 0, - }) - .catch((err) => { - ws.emit('error', err); - }); - - return ws; - } -} diff --git a/apps/meteor/app/webdav/server/methods/addWebdavAccount.ts b/apps/meteor/app/webdav/server/methods/addWebdavAccount.ts deleted file mode 100644 index 2c39736490913..0000000000000 --- a/apps/meteor/app/webdav/server/methods/addWebdavAccount.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { api } from '@rocket.chat/core-services'; -import type { IWebdavAccountPayload } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { WebdavAccounts } from '@rocket.chat/models'; -import { Match, check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../../settings/server'; -import { WebdavClientAdapter } from '../lib/webdavClientAdapter'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - addWebdavAccount(formData: IWebdavAccountPayload): boolean; - addWebdavAccountByToken(data: IWebdavAccountPayload): boolean; - } -} - -export const addWebdavAccountByToken = async (userId: string, data: IWebdavAccountPayload): Promise => { - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid User', { method: 'addWebdavAccount' }); - } - - if (!settings.get('Webdav_Integration_Enabled')) { - throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', { - method: 'addWebdavAccount', - }); - } - - check( - data, - Match.ObjectIncluding({ - serverURL: String, - token: Match.ObjectIncluding({ - access_token: String, - token_type: String, - refresh_token: Match.Optional(String), - }), - name: Match.Maybe(String), - }), - ); - - try { - const client = new WebdavClientAdapter(data.serverURL, { token: data.token }); - - const accountData = { - userId, - serverURL: data.serverURL, - token: data.token, - name: data.name ?? '', - }; - - await client.stat('/'); - await WebdavAccounts.updateOne( - { - userId, - serverURL: data.serverURL, - name: data.name ?? '', - }, - { - $set: accountData, - }, - { - upsert: true, - }, - ); - void api.broadcast('notify.webdav', userId, { - type: 'changed', - account: accountData, - }); - } catch (error) { - throw new Meteor.Error('could-not-access-webdav', 'Could not access webdav', { - method: 'addWebdavAccount', - }); - } - - return true; -}; - -Meteor.methods({ - async addWebdavAccount(formData) { - const userId = Meteor.userId(); - - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid User', { method: 'addWebdavAccount' }); - } - - if (!settings.get('Webdav_Integration_Enabled')) { - throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', { - method: 'addWebdavAccount', - }); - } - - check( - formData, - Match.ObjectIncluding({ - serverURL: String, - username: String, - password: String, - name: Match.Maybe(String), - }), - ); - - const duplicateAccount = await WebdavAccounts.findOneByUserIdServerUrlAndUsername( - { userId, serverURL: formData.serverURL, username: formData.username }, - {}, - ); - - if (duplicateAccount !== null) { - throw new Meteor.Error('duplicated-account', 'Account not found', { - method: 'addWebdavAccount', - }); - } - - try { - const client = new WebdavClientAdapter(formData.serverURL, { - username: formData.username, - password: formData.password, - }); - - const accountData = { - userId, - serverURL: formData.serverURL, - username: formData.username, - password: formData.password, - name: formData.name ?? '', - }; - - await client.stat('/'); - await WebdavAccounts.insertOne(accountData); - - void api.broadcast('notify.webdav', userId, { - type: 'changed', - account: accountData, - }); - } catch (error) { - throw new Meteor.Error('could-not-access-webdav', 'Could not access webdav', { - method: 'addWebdavAccount', - }); - } - return true; - }, - - async addWebdavAccountByToken(data) { - const userId = Meteor.userId(); - - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid User', { method: 'addWebdavAccount' }); - } - - return addWebdavAccountByToken(userId, data); - }, -}); diff --git a/apps/meteor/app/webdav/server/methods/getFileFromWebdav.ts b/apps/meteor/app/webdav/server/methods/getFileFromWebdav.ts deleted file mode 100644 index f8a2281586e44..0000000000000 --- a/apps/meteor/app/webdav/server/methods/getFileFromWebdav.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { IWebdavAccount, IWebdavNode } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { WebdavAccounts } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../../settings/server'; -import { getWebdavCredentials } from '../lib/getWebdavCredentials'; -import { WebdavClientAdapter } from '../lib/webdavClientAdapter'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - getFileFromWebdav(accountId: IWebdavAccount['_id'], file: IWebdavNode): Promise<{ success: boolean; data: Uint8Array }>; - } -} - -Meteor.methods({ - async getFileFromWebdav(accountId, file) { - const userId = Meteor.userId(); - - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid User', { method: 'getFileFromWebdav' }); - } - if (!settings.get('Webdav_Integration_Enabled')) { - throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', { - method: 'getFileFromWebdav', - }); - } - - const account = await WebdavAccounts.findOneByIdAndUserId(accountId, userId, {}); - if (!account) { - throw new Meteor.Error('error-invalid-account', 'Invalid WebDAV Account', { - method: 'getFileFromWebdav', - }); - } - - try { - const cred = getWebdavCredentials(account); - const client = new WebdavClientAdapter(account.serverURL, cred); - const fileContent = await client.getFileContents(file.filename); - const data = new Uint8Array(fileContent); - return { success: true, data }; - } catch (error) { - throw new Meteor.Error('unable-to-get-file', 'Unable to get file', { - method: 'getFileFromWebdav', - }); - } - }, -}); diff --git a/apps/meteor/app/webdav/server/methods/getWebdavFileList.ts b/apps/meteor/app/webdav/server/methods/getWebdavFileList.ts deleted file mode 100644 index 66e924b719a2a..0000000000000 --- a/apps/meteor/app/webdav/server/methods/getWebdavFileList.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { IWebdavAccount, IWebdavNode } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { WebdavAccounts } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../../settings/server'; -import { getWebdavCredentials } from '../lib/getWebdavCredentials'; -import { WebdavClientAdapter } from '../lib/webdavClientAdapter'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - getWebdavFileList(accountId: IWebdavAccount['_id'], path: string): { success: boolean; data: IWebdavNode[] }; - } -} - -Meteor.methods({ - async getWebdavFileList(accountId, path) { - const userId = Meteor.userId(); - - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid User', { method: 'getWebdavFileList' }); - } - - if (!settings.get('Webdav_Integration_Enabled')) { - throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', { - method: 'getWebdavFileList', - }); - } - - const account = await WebdavAccounts.findOneByIdAndUserId(accountId, userId, {}); - if (!account) { - throw new Meteor.Error('error-invalid-account', 'Invalid WebDAV Account', { - method: 'getWebdavFileList', - }); - } - - try { - const cred = getWebdavCredentials(account); - const client = new WebdavClientAdapter(account.serverURL, cred); - const data = (await client.getDirectoryContents(path)) as IWebdavNode[]; - return { success: true, data }; - } catch (error) { - throw new Meteor.Error('could-not-access-webdav', 'Could not access webdav', { - method: 'getWebdavFileList', - }); - } - }, -}); diff --git a/apps/meteor/app/webdav/server/methods/getWebdavFilePreview.ts b/apps/meteor/app/webdav/server/methods/getWebdavFilePreview.ts deleted file mode 100644 index 34077babf276c..0000000000000 --- a/apps/meteor/app/webdav/server/methods/getWebdavFilePreview.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { IWebdavAccount } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { WebdavAccounts } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; -import { createClient } from 'webdav'; - -import { settings } from '../../../settings/server'; -import { getWebdavCredentials } from '../lib/getWebdavCredentials'; - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - getWebdavFilePreview(accountId: IWebdavAccount['_id'], path: string): { success: true; data: ArrayBuffer } | undefined; - } -} - -Meteor.methods({ - async getWebdavFilePreview(accountId, path) { - const userId = Meteor.userId(); - - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid User', { - method: 'getWebdavFilePreview', - }); - } - - if (!settings.get('Webdav_Integration_Enabled') || !settings.get('Accounts_OAuth_Nextcloud_URL')) { - throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', { - method: 'getWebdavFilePreview', - }); - } - - const account = await WebdavAccounts.findOneByIdAndUserId(accountId, userId, {}); - if (!account) { - throw new Meteor.Error('error-invalid-account', 'Invalid WebDAV Account', { - method: 'getWebdavFilePreview', - }); - } - - try { - const cred = getWebdavCredentials(account); - const client = createClient(account.serverURL, cred); - const serverURL = settings.get('Accounts_OAuth_Nextcloud_URL'); - const res = await client.customRequest(`${serverURL}/index.php/core/preview.png?file=${path}&x=64&y=64`, { - method: 'GET', - responseType: 'arraybuffer', - }); - return { success: true, data: res.data as ArrayBuffer }; - } catch (error) { - // ignore error - } - }, -}); diff --git a/apps/meteor/app/webdav/server/methods/uploadFileToWebdav.ts b/apps/meteor/app/webdav/server/methods/uploadFileToWebdav.ts deleted file mode 100644 index 6d66031aa4173..0000000000000 --- a/apps/meteor/app/webdav/server/methods/uploadFileToWebdav.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { MeteorError } from '@rocket.chat/core-services'; -import type { IWebdavAccount } from '@rocket.chat/core-typings'; -import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Logger } from '@rocket.chat/logger'; -import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../../settings/server'; -import { uploadFileToWebdav } from '../lib/uploadFileToWebdav'; - -const logger = new Logger('WebDAV_Upload'); - -declare module '@rocket.chat/ddp-client' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface ServerMethods { - uploadFileToWebdav( - accountId: IWebdavAccount['_id'], - fileData: string | Buffer | ArrayBuffer, - name: string, - ): { success: boolean; message?: TranslationKey }; - } -} - -Meteor.methods({ - async uploadFileToWebdav(accountId, fileData, name) { - if (!Meteor.userId()) { - throw new MeteorError('error-invalid-user', 'Invalid User', { - method: 'uploadFileToWebdav', - }); - } - - if (!settings.get('Webdav_Integration_Enabled')) { - throw new MeteorError('error-not-allowed', 'WebDAV Integration Not Allowed', { - method: 'uploadFileToWebdav', - }); - } - - try { - await uploadFileToWebdav(accountId, fileData instanceof ArrayBuffer ? Buffer.from(fileData) : fileData, name); - return { success: true }; - } catch (err: any) { - if (typeof err === 'object' && err instanceof Error && err.name === 'error-invalid-account') { - throw new MeteorError(err.name, 'Invalid WebDAV Account', { - method: 'uploadFileToWebdav', - }); - } - - logger.error({ err }); - - if (err.response) { - const { status } = err.response; - if (status === 404) { - return { success: false, message: 'webdav-server-not-found' }; - } - if (status === 401) { - return { success: false, message: 'error-invalid-account' }; - } - if (status === 412) { - return { success: false, message: 'Duplicate_file_name_found' }; - } - } - return { success: false, message: 'FileUpload_Error' }; - } - }, -}); diff --git a/apps/meteor/app/wordpress/server/index.ts b/apps/meteor/app/wordpress/server/index.ts deleted file mode 100644 index cf327e4971bb2..0000000000000 --- a/apps/meteor/app/wordpress/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './lib'; diff --git a/apps/meteor/app/wordpress/server/lib.ts b/apps/meteor/app/wordpress/server/lib.ts deleted file mode 100644 index 7777ea1382215..0000000000000 --- a/apps/meteor/app/wordpress/server/lib.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { OauthConfig } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; -import { ServiceConfiguration } from 'meteor/service-configuration'; -import _ from 'underscore'; - -import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; -import { settings } from '../../settings/server'; - -const config: OauthConfig = { - serverURL: '', - identityPath: '/oauth/me', - - addAutopublishFields: { - forLoggedInUser: ['services.wordpress'], - forOtherUsers: ['services.wordpress.user_login'], - }, - accessTokenParam: 'access_token', -}; - -const WordPress = new CustomOAuth('wordpress', config); - -const fillSettings = _.debounce(async (): Promise => { - config.serverURL = settings.get('API_Wordpress_URL'); - if (!config.serverURL) { - if (config.serverURL === undefined) { - return fillSettings(); - } - return; - } - - delete config.identityPath; - delete config.identityTokenSentVia; - delete config.authorizePath; - delete config.tokenPath; - delete config.scope; - - const serverType = settings.get('Accounts_OAuth_Wordpress_server_type'); - switch (serverType) { - case 'custom': - if (settings.get('Accounts_OAuth_Wordpress_identity_path')) { - config.identityPath = settings.get('Accounts_OAuth_Wordpress_identity_path'); - } - - if (settings.get('Accounts_OAuth_Wordpress_identity_token_sent_via')) { - config.identityTokenSentVia = settings.get('Accounts_OAuth_Wordpress_identity_token_sent_via'); - } - - if (settings.get('Accounts_OAuth_Wordpress_token_path')) { - config.tokenPath = settings.get('Accounts_OAuth_Wordpress_token_path'); - } - - if (settings.get('Accounts_OAuth_Wordpress_authorize_path')) { - config.authorizePath = settings.get('Accounts_OAuth_Wordpress_authorize_path'); - } - - if (settings.get('Accounts_OAuth_Wordpress_scope')) { - config.scope = settings.get('Accounts_OAuth_Wordpress_scope'); - } - break; - case 'wordpress-com': - config.identityPath = 'https://public-api.wordpress.com/rest/v1/me'; - config.identityTokenSentVia = 'header'; - config.authorizePath = 'https://public-api.wordpress.com/oauth2/authorize'; - config.tokenPath = 'https://public-api.wordpress.com/oauth2/token'; - config.scope = 'auth'; - break; - default: - config.identityPath = '/oauth/me'; - break; - } - - const result = WordPress.configure(config); - const enabled = settings.get('Accounts_OAuth_Wordpress'); - if (enabled) { - await ServiceConfiguration.configurations.upsertAsync( - { - service: 'wordpress', - }, - { - $set: config, - }, - ); - } else { - await ServiceConfiguration.configurations.removeAsync({ - service: 'wordpress', - }); - } - - return result; -}, 1000); - -Meteor.startup(() => { - return settings.watchByRegex(/(API\_Wordpress\_URL)?(Accounts\_OAuth\_Wordpress\_)?/, () => fillSettings()); -}); diff --git a/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx b/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx index c6b9b95ffe120..5c2467482af1e 100644 --- a/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx @@ -29,7 +29,7 @@ const GameCenterContainer = ({ handleClose, handleBack, game }: IGameCenterConta {handleClose && } - +