From a2f169ab76bdd49663a2ee58b3067b7995ded616 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Jul 2022 12:10:45 -0400 Subject: [PATCH 1/9] build(deps): bump core-js from 3.23.3 to 3.23.4 in /website/client (#14114) Bumps [core-js](https://github.com/zloirock/core-js) from 3.23.3 to 3.23.4. - [Release notes](https://github.com/zloirock/core-js/releases) - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/zloirock/core-js/compare/v3.23.3...v3.23.4) --- updated-dependencies: - dependency-name: core-js dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- website/client/package-lock.json | 6 +++--- website/client/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/website/client/package-lock.json b/website/client/package-lock.json index 665172c843..6324158a05 100644 --- a/website/client/package-lock.json +++ b/website/client/package-lock.json @@ -15839,9 +15839,9 @@ } }, "core-js": { - "version": "3.23.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.23.3.tgz", - "integrity": "sha512-oAKwkj9xcWNBAvGbT//WiCdOMpb9XQG92/Fe3ABFM/R16BsHgePG00mFOgKf7IsCtfj8tA1kHtf/VwErhriz5Q==" + "version": "3.23.4", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.23.4.tgz", + "integrity": "sha512-vjsKqRc1RyAJC3Ye2kYqgfdThb3zYnx9CrqoCcjMOENMtQPC7ZViBvlDxwYU/2z2NI/IPuiXw5mT4hWhddqjzQ==" }, "core-js-compat": { "version": "3.11.0", diff --git a/website/client/package.json b/website/client/package.json index 5e0526848a..27ea0a2a20 100644 --- a/website/client/package.json +++ b/website/client/package.json @@ -32,7 +32,7 @@ "bootstrap": "^4.6.0", "bootstrap-vue": "^2.22.0", "chai": "^4.3.6", - "core-js": "^3.23.3", + "core-js": "^3.23.4", "dompurify": "^2.3.8", "eslint": "^6.8.0", "eslint-config-habitrpg": "^6.2.0", From 94b9bb103694e0ae8de1548ffac4d895ca76b12b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Jul 2022 12:11:38 -0400 Subject: [PATCH 2/9] build(deps): bump image-size from 1.0.1 to 1.0.2 (#14123) Bumps [image-size](https://github.com/image-size/image-size) from 1.0.1 to 1.0.2. - [Release notes](https://github.com/image-size/image-size/releases) - [Commits](https://github.com/image-size/image-size/compare/v1.0.1...v1.0.2) --- updated-dependencies: - dependency-name: image-size dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index b6e905add9..d60ba91155 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8678,9 +8678,9 @@ "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=" }, "image-size": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.1.tgz", - "integrity": "sha512-VAwkvNSNGClRw9mDHhc5Efax8PLlsOGcUTh0T/LIriC8vPA3U5PdqXWqkz406MoYHMKW8Uf9gWr05T/rYB44kQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz", + "integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==", "requires": { "queue": "6.0.2" } diff --git a/package.json b/package.json index 0476235ff1..b48f17fde1 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "gulp.spritesmith": "^6.13.0", "habitica-markdown": "^3.0.0", "helmet": "^4.6.0", - "image-size": "^1.0.1", + "image-size": "^1.0.2", "in-app-purchase": "^1.11.3", "js2xmlparser": "^4.0.2", "jsonwebtoken": "^8.5.1", From 3aa7b8b447edd2e7f2cc48f40c54c8b6d00a0285 Mon Sep 17 00:00:00 2001 From: Natalie L <78037386+CuriousMagpie@users.noreply.github.com> Date: Wed, 13 Jul 2022 15:17:28 -0400 Subject: [PATCH 3/9] Gifting modal design (#14124) * update selectUserModal.vue * more updates to selectUserModal.vue, typo fix in subscriber.json * remove exact sizing for selectUserModal.vue * update to size for selectUserModal.vue * added sendGiftModal.vue file * updates to selectUser & sendGift modals * making the modals go & position cursor * working working working * added a return to method * avatar display & placeholder profile.name and username * subscription-options added * added menu row & started on gem options * Added selectPage function, have not tested. * updated habitica-images * state changes * bringing in gem counter * arranging elements * state changes, gem input boxes * styling sendGiftModal.vue * more sendGiftModal.vue styling and new close.svg icon * more styling! * and more styling of send own gems part of page * images update * more styling of own gems & some attempts to adjust :class on the menu * styling styling styling * replace +/- svg, styling * styling, mostly * new SVGs * stylin' * reverting svg changes * no more stylin' * finally got the +/- icons to show up...but they're the wrong color * solved svg icon color problem! :) * habitica-images * working on sendGift part of button * trying to make it do math, failing * more attempts at math * +/- buttons work on gem pages & cost calculation on buyGems * trying to get hover colors working on +/- svgs * formatted dollar amount as currency * css/html for subscription-options & payments-buttons simplified * swag at payments-buttons parameter (not tested) * send gems from own balance works! * working on starting page * increment gem amount limited to maxGems and not < 0 * uncommented onHide() * got bg color on sub options to work! yay! * payment buttons! * making g1g1 look good * position modal on page properly & code clean-up * Changes as requested! * small color update * fixed ternary function * chore(html): indentation and comments * fix(fn): correct catch for under-0 * chore(json): whitespace * update gem styling; add linebreak to notifications.vue bc linter * updating subscriptionOptions * snackbar css fix * reverting commit e16c12f * removing merge conflict markers * just a little comment * fixed some navigation, clear input field on selectPage, cleaned up code; another try at subscriptionOption.vue * merge upstream/develop * update selectPage() to disable Gems menu items when on 'ownGems' or 'buyGems' states * working on subscriptionOptions.vue logic * fix(script): changed props & added updateSubscriptionData() * fix(script): forgot to call updateSubscriptionData() * fix(scripts): corrected :userReceivingGift on sendGiftModal.vue * fix(scripts): correct props userReceivingGift to an Object * fix(scripts): corrected v-if & revised props * fix(style/html/whitespace): updated css for close.svg and added missing * style(radio-buttons): updated focus states and added hover states * style(radio-buttons): refined focus and hover states * fix(function): changed buyGemsLink to buyGems; still working on menu * style(radio buttons): ensured consistent display of radio buttons through-out site; still struggling with hover states * style(radio buttons): updated focus/active/hover to match design & removed unnecessary code * fix: set default subscription option to 1 month * fix(function): add default amounts to gem states when modal selected from user profile * fix(build): use develop package json * fix: SCSS commenting & abstracted setGemsDefault() * fix(packages): revert to develop * fix: remove unnecessary console.log statement Co-authored-by: SabreCat --- website/client/src/assets/scss/form.scss | 102 ++- website/client/src/assets/svg/big-gift.svg | 27 + website/client/src/assets/svg/close.svg | 14 +- .../group-plans/createGroupModalPages.vue | 5 + website/client/src/components/header/menu.vue | 6 +- .../src/components/payments/buttons/list.vue | 8 + .../components/payments/selectUserModal.vue | 132 +++- .../src/components/payments/sendGiftModal.vue | 659 ++++++++++++++++++ .../src/components/settings/subscription.vue | 4 - .../settings/subscriptionOptions.vue | 39 +- .../client/src/components/tasks/taskModal.vue | 5 + .../src/components/userMenu/profile.vue | 3 +- website/client/src/store/index.js | 3 + website/common/locales/en/groups.json | 1 + website/common/locales/en/settings.json | 1 + website/common/locales/en/subscriber.json | 7 +- 16 files changed, 956 insertions(+), 60 deletions(-) create mode 100644 website/client/src/assets/svg/big-gift.svg create mode 100644 website/client/src/components/payments/sendGiftModal.vue diff --git a/website/client/src/assets/scss/form.scss b/website/client/src/assets/scss/form.scss index c5cf0b61f7..8e21bc06a4 100644 --- a/website/client/src/assets/scss/form.scss +++ b/website/client/src/assets/scss/form.scss @@ -82,7 +82,7 @@ input, textarea, input.form-control, textarea.form-control { } } -/** Colored Input-Groups, ignoring checklist */ +// Colored Input-Groups, ignoring checklist .input-group:not(.checklist-group) { border-radius: 2px; border: solid 1px $gray-400; @@ -100,7 +100,7 @@ input, textarea, input.form-control, textarea.form-control { } } -/** Generic Input Group Styles */ +// Generic Input Group Styles .input-group { height: 2rem; @@ -179,10 +179,11 @@ input, textarea, input.form-control, textarea.form-control { padding-left: 0px; } -// Checkboxes and radios +// used in checkboxes and radios $bg-focused-active-control: #4f2993; $bg-disabled-control: #34303a; +// custom control .custom-control { margin-bottom: .5rem; @@ -205,6 +206,7 @@ $bg-disabled-control: #34303a; } } +// checkboxes .custom-checkbox { .custom-control-label::before { border-radius: 2px; @@ -280,11 +282,26 @@ $bg-disabled-control: #34303a; padding-left: 36px; } +// radio buttons +$bg-color: $purple-400; + +// svg for the purple dot @mixin custom-radio-checked-icon ($bg-color) { background-image: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='#{$bg-color}'/%3E%3C/svg%3E"), "#", "%23"); } .custom-radio .custom-control-input { + opacity: 0; + margin: 15px 25px 34px 25px; + + // outside circle + &:checked~.custom-control-label::before { + background-color: $gray-700; + background-size: 12px 12px; + border-color: $purple-400; + } + + // checked indicator &:checked~.custom-control-label::after { @include custom-radio-checked-icon($purple-400); width: 18px; @@ -292,51 +309,84 @@ $bg-disabled-control: #34303a; background-size: 12px 12px; } - &:checked~.custom-control-label::before { - background-color: $gray-700; - background-size: 12px 12px; - border-color: $purple-400; - } - &:active~.custom-control-label::before { background-color: inherit; } - &:focus:not(:checked):not(:disabled)~.custom-control-label::before, &:active:not(:checked):not(:disabled)~.custom-control-label::before { - box-shadow: 0 0 0 6px rgba($bg-focused-active-control, 0.1); +// focus / not checked / not disabled + &:focus:not(:checked):not(:disabled)~.custom-control-label::before, + &:active:not(:checked):not(:disabled)~.custom-control-label::before { + border: 2px solid $gray-300; + box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5); } - &:focus:checked:not(:disabled)~.custom-control-label::before, &:active:checked:not(:disabled)~.custom-control-label::before { - box-shadow: 0 0 0 6px rgba($bg-focused-active-control, 0.1); - border-color: $purple-400; - background-color: rgba($bg-focused-active-control, 0.1); +// focus / checked / not disabled + &:focus:checked:not(:disabled)~.custom-control-label::before, + &:active:checked:not(:disabled)~.custom-control-label::before { + border: 2px solid $purple-400; + box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5); } - &:disabled:checked~.custom-control-label::before { - border-color: $gray-400; - background-color: transparent; +// hover / not checked / not disabled + &:hover:not(:checked):not(:disabled)~.custom-control-label::before, + &:active:not(:checked):not(:disabled)~.custom-control-label::before { + width: 18px; + height: 18px; + background: 50%/50% 50% no-repeat; + @include custom-radio-checked-icon($purple-400); + background-size: 12px 12px; + border: solid 2px $purple-400; } - &:disabled:checked~.custom-control-label::after { +// hover / checked / not disabled + &:hover:checked:not(:disabled)~.custom-control-label::before, + &:active::checked:not(:disabled)~.custom-control-label::before { + width: 18px; + height: 18px; + background: 50%/50% 50% no-repeat; @include custom-radio-checked-icon($gray-400); + background-size: 12px 12px; + border: solid 2px $purple-300; + } + +// disabled / checked / before + &:disabled:checked~.custom-control-label::before { + background: 50%/50% 50% no-repeat; + @include custom-radio-checked-icon($gray-300); + border: 2px solid $gray-200; + background-color: transparent; + opacity: 0.75; + } + +// disabled / checked / after + &:disabled:checked~.custom-control-label::after { + background: 50%/50% 50% no-repeat; + @include custom-radio-checked-icon($gray-300); width: 18px; height: 18px; background-size: 12px 12px; } +// disabled / not checked / before &:disabled:not(:checked)~.custom-control-label::before { - border-color: $gray-300; - background-color: transparent; + background-color: $gray-600; + border: 2px solid $gray-200; } - &:focus:disabled~.custom-control-label::before, &:active:disabled~.custom-control-label::before { - box-shadow: 0 0 0 6px rgba($bg-disabled-control, 0.1); - border-color: $gray-300; +// focus and disabled / not checked / before + &:focus:disabled~.custom-control-label::before, + &:active:disabled~.custom-control-label::before { background-color: rgba($bg-disabled-control, 0.1); + box-shadow: 0 0 0 6px rgba($bg-disabled-control, 0.1); + border: 2px solid $gray-200; } - &:focus:disabled:checked~.custom-control-label::before, &:active:disabled:checked~.custom-control-label::before { - border-color: $gray-400; +// focus and disabled / checked / before + &:focus:disabled:checked~.custom-control-label::before, + &:active:disabled:checked~.custom-control-label::before { + background: 50%/50% 50% no-repeat; + @include custom-radio-checked-icon($gray-300); + border: 2px solid $gray-200; } } diff --git a/website/client/src/assets/svg/big-gift.svg b/website/client/src/assets/svg/big-gift.svg new file mode 100644 index 0000000000..1d756e4dfa --- /dev/null +++ b/website/client/src/assets/svg/big-gift.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/client/src/assets/svg/close.svg b/website/client/src/assets/svg/close.svg index 5070218c06..398440da99 100644 --- a/website/client/src/assets/svg/close.svg +++ b/website/client/src/assets/svg/close.svg @@ -1,5 +1,11 @@ - - - + + + Icon/Close + + + + + + - + \ No newline at end of file diff --git a/website/client/src/components/group-plans/createGroupModalPages.vue b/website/client/src/components/group-plans/createGroupModalPages.vue index 2b314834ad..b354f6ac63 100644 --- a/website/client/src/components/group-plans/createGroupModalPages.vue +++ b/website/client/src/components/group-plans/createGroupModalPages.vue @@ -122,6 +122,11 @@ font-weight: bold; } + .custom-control-input { + z-index: -1; + opacity: 0; + } + .box:hover { cursor: pointer; opacity: 0.7; diff --git a/website/client/src/components/header/menu.vue b/website/client/src/components/header/menu.vue index d77c6f8e0d..fbfdaed141 100644 --- a/website/client/src/components/header/menu.vue +++ b/website/client/src/components/header/menu.vue @@ -3,7 +3,7 @@ - +
+

{{ $t('choosePaymentMethod') }}

* style(radio-buttons): updated focus states and added hover states * style(radio-buttons): refined focus and hover states * fix(function): changed buyGemsLink to buyGems; still working on menu * style(radio buttons): ensured consistent display of radio buttons through-out site; still struggling with hover states * style(radio buttons): updated focus/active/hover to match design & removed unnecessary code * fix: set default subscription option to 1 month * fix(function): add default amounts to gem states when modal selected from user profile * fix(build): use develop package json * fix: SCSS commenting & abstracted setGemsDefault() * fix(packages): revert to develop * fix: remove unnecessary console.log statement * fix(payments): storePaymentStatusAndReload() modified Co-authored-by: SabreCat --- website/client/src/components/payments/amazonModal.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/client/src/components/payments/amazonModal.vue b/website/client/src/components/payments/amazonModal.vue index 578c627b08..8641a957bb 100644 --- a/website/client/src/components/payments/amazonModal.vue +++ b/website/client/src/components/payments/amazonModal.vue @@ -198,7 +198,7 @@ export default { appState.newGroup = false; appState.group = pick(this.amazonPayments.group, ['_id', 'memberCount', 'name']); } - } else if (paymentType.indexOf('gift-') === 0) { + } else if (paymentType && paymentType.indexOf('gift-') === 0) { appState.gift = this.amazonPayments.gift; appState.giftReceiver = this.amazonPayments.giftReceiver; } else if (paymentType === 'gems') { From 8e717de039de3007156a3e7d6094e784008269b7 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Thu, 21 Jul 2022 15:32:28 -0500 Subject: [PATCH 9/9] Server setting to disallow chat from new accounts (#13952) * feat(chat): server setting to disallow chat from new accounts * fix(tests): many adjustments to handle chat minimum age * fix(tests): address issues outside of chat posting * chore(analytics): add incident logging * fix(config): allow instant chat for dev purposes * fix(test): finely age one more user * fix(test): member not leader Co-authored-by: SabreCat --- config.json.example | 1 + .../integration/chat/DELETE-chat_id.test.js | 4 ++ .../integration/chat/POST-chat.flag.test.js | 4 +- .../integration/chat/POST-chat.like.test.js | 5 ++ .../api/v3/integration/chat/POST-chat.test.js | 46 +++++++++++++++++-- .../integration/chat/POST-chat_seen.test.js | 7 +++ ...POST-groups_id_chat_id_clear_flags.test.js | 5 ++ .../groups/POST-groups_groupId_leave.test.js | 5 ++ .../POST-groups_id_removeMember.test.js | 1 + .../prevent-multiple-notification.js | 1 + test/helpers/start-server.js | 1 + website/common/locales/en/groups.json | 3 +- website/server/controllers/api-v3/chat.js | 16 +++++++ 13 files changed, 92 insertions(+), 7 deletions(-) diff --git a/config.json.example b/config.json.example index 04c1c535b4..3875097d6c 100644 --- a/config.json.example +++ b/config.json.example @@ -1,4 +1,5 @@ { + "ACCOUNT_MIN_CHAT_AGE": "0", "ADMIN_EMAIL": "you@example.com", "AMAZON_PAYMENTS_CLIENT_ID": "CLIENT_ID", "AMAZON_PAYMENTS_MODE": "sandbox", diff --git a/test/api/v3/integration/chat/DELETE-chat_id.test.js b/test/api/v3/integration/chat/DELETE-chat_id.test.js index 0d53dfa999..7a9756e673 100644 --- a/test/api/v3/integration/chat/DELETE-chat_id.test.js +++ b/test/api/v3/integration/chat/DELETE-chat_id.test.js @@ -15,6 +15,10 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => { type: 'guild', privacy: 'public', }, + leaderDetails: { + 'auth.timestamps.created': new Date('2022-01-01'), + balance: 10, + }, }); groupWithChat = group; diff --git a/test/api/v3/integration/chat/POST-chat.flag.test.js b/test/api/v3/integration/chat/POST-chat.flag.test.js index acff8b507b..909fbfa7a0 100644 --- a/test/api/v3/integration/chat/POST-chat.flag.test.js +++ b/test/api/v3/integration/chat/POST-chat.flag.test.js @@ -117,7 +117,9 @@ describe('POST /chat/:chatId/flag', () => { }); it('Flags a chat when the author\'s account was deleted', async () => { - const deletedUser = await generateUser(); + const deletedUser = await generateUser({ + 'auth.timestamps.created': new Date('2022-01-01'), + }); const { message } = await deletedUser.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE }); await deletedUser.del('/user', { password: 'password', diff --git a/test/api/v3/integration/chat/POST-chat.like.test.js b/test/api/v3/integration/chat/POST-chat.like.test.js index 192f3e3aa7..beab851bb0 100644 --- a/test/api/v3/integration/chat/POST-chat.like.test.js +++ b/test/api/v3/integration/chat/POST-chat.like.test.js @@ -18,11 +18,16 @@ describe('POST /chat/:chatId/like', () => { privacy: 'public', }, members: 1, + leaderDetails: { + 'auth.timestamps.created': new Date('2022-01-01'), + balance: 10, + }, }); user = groupLeader; groupWithChat = group; anotherUser = members[0]; // eslint-disable-line prefer-destructuring + await anotherUser.update({ 'auth.timestamps.created': new Date('2022-01-01') }); }); it('Returns an error when chat message is not found', async () => { diff --git a/test/api/v3/integration/chat/POST-chat.test.js b/test/api/v3/integration/chat/POST-chat.test.js index 24283de89e..73ee343c79 100644 --- a/test/api/v3/integration/chat/POST-chat.test.js +++ b/test/api/v3/integration/chat/POST-chat.test.js @@ -38,10 +38,15 @@ describe('POST /chat', () => { members: 2, }); user = groupLeader; - await user.update({ 'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL }); // prevent tests accidentally throwing messageGroupChatSpam + await user.update({ + 'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL, + 'auth.timestamps.created': new Date('2022-01-01'), + }); // prevent tests accidentally throwing messageGroupChatSpam groupWithChat = group; member = members[0]; // eslint-disable-line prefer-destructuring additionalMember = members[1]; // eslint-disable-line prefer-destructuring + await member.update({ 'auth.timestamps.created': new Date('2022-01-01') }); + await additionalMember.update({ 'auth.timestamps.created': new Date('2022-01-01') }); }); it('Returns an error when no message is provided', async () => { @@ -104,7 +109,10 @@ describe('POST /chat', () => { }); const privateGuildMemberWithChatsRevoked = members[0]; - await privateGuildMemberWithChatsRevoked.update({ 'flags.chatRevoked': true }); + await privateGuildMemberWithChatsRevoked.update({ + 'flags.chatRevoked': true, + 'auth.timestamps.created': new Date('2022-01-01'), + }); const message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage }); @@ -122,7 +130,10 @@ describe('POST /chat', () => { }); const privatePartyMemberWithChatsRevoked = members[0]; - await privatePartyMemberWithChatsRevoked.update({ 'flags.chatRevoked': true }); + await privatePartyMemberWithChatsRevoked.update({ + 'flags.chatRevoked': true, + 'auth.timestamps.created': new Date('2022-01-01'), + }); const message = await privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage }); @@ -183,7 +194,10 @@ describe('POST /chat', () => { }); const userWithChatShadowMuted = members[0]; - await userWithChatShadowMuted.update({ 'flags.chatShadowMuted': true }); + await userWithChatShadowMuted.update({ + 'flags.chatShadowMuted': true, + 'auth.timestamps.created': new Date('2022-01-01'), + }); const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage }); @@ -202,7 +216,10 @@ describe('POST /chat', () => { }); const userWithChatShadowMuted = members[0]; - await userWithChatShadowMuted.update({ 'flags.chatShadowMuted': true }); + await userWithChatShadowMuted.update({ + 'flags.chatShadowMuted': true, + 'auth.timestamps.created': new Date('2022-01-01'), + }); const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage }); @@ -312,6 +329,7 @@ describe('POST /chat', () => { }, members: 1, }); + await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') }); const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage }); @@ -330,6 +348,7 @@ describe('POST /chat', () => { // Update the bannedWordsAllowed property for the group group.update({ bannedWordsAllowed: true }); + await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') }); const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage }); @@ -345,6 +364,7 @@ describe('POST /chat', () => { }, members: 1, }); + await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') }); const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage }); @@ -411,6 +431,7 @@ describe('POST /chat', () => { }, members: 1, }); + await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') }); const message = await members[0].post(`/groups/${group._id}/chat`, { message: testSlurMessage }); @@ -430,6 +451,16 @@ describe('POST /chat', () => { }); }); + it('errors when user account is too young', async () => { + const brandNewUser = await generateUser(); + await expect(brandNewUser.post('/groups/habitrpg/chat', { message: 'hi im new' })) + .to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('chatTemporarilyUnavailable'), + }); + }); + it('creates a chat', async () => { const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`); @@ -492,6 +523,7 @@ describe('POST /chat', () => { 'items.currentMount': mount, 'items.currentPet': pet, 'preferences.style': style, + 'auth.timestamps.created': new Date('2022-01-01'), }); await userWithStyle.sync(); @@ -517,6 +549,7 @@ describe('POST /chat', () => { }; const backer = await generateUser({ backer: backerInfo, + 'auth.timestamps.created': new Date('2022-01-01'), }); const message = await backer.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }); @@ -587,6 +620,9 @@ describe('POST /chat', () => { privacy: 'private', }, members: 1, + leaderDetails: { + 'auth.timestamps.created': new Date('2022-01-01'), + }, }); const message = await groupLeader.post(`/groups/${group._id}/chat`, { message: testMessage }); diff --git a/test/api/v3/integration/chat/POST-chat_seen.test.js b/test/api/v3/integration/chat/POST-chat_seen.test.js index f64df56146..9cd2103bc6 100644 --- a/test/api/v3/integration/chat/POST-chat_seen.test.js +++ b/test/api/v3/integration/chat/POST-chat_seen.test.js @@ -15,6 +15,10 @@ describe('POST /groups/:id/chat/seen', () => { privacy: 'public', }, members: 1, + leaderDetails: { + 'auth.timestamps.created': new Date('2022-01-01'), + balance: 10, + }, }); guild = group; @@ -51,6 +55,9 @@ describe('POST /groups/:id/chat/seen', () => { privacy: 'private', }, members: 1, + leaderDetails: { + 'auth.timestamps.created': new Date('2022-01-01'), + }, }); party = group; diff --git a/test/api/v3/integration/chat/POST-groups_id_chat_id_clear_flags.test.js b/test/api/v3/integration/chat/POST-groups_id_chat_id_clear_flags.test.js index e9f77e5561..4bb99cc33f 100644 --- a/test/api/v3/integration/chat/POST-groups_id_chat_id_clear_flags.test.js +++ b/test/api/v3/integration/chat/POST-groups_id_chat_id_clear_flags.test.js @@ -18,6 +18,10 @@ describe('POST /groups/:id/chat/:id/clearflags', () => { type: 'guild', privacy: 'public', }, + leaderDetails: { + 'auth.timestamps.created': new Date('2022-01-01'), + balance: 10, + }, }); groupWithChat = group; @@ -65,6 +69,7 @@ describe('POST /groups/:id/chat/:id/clearflags', () => { members: 1, }); + await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') }); let privateMessage = await members[0].post(`/groups/${group._id}/chat`, { message: 'Some message' }); privateMessage = privateMessage.message; diff --git a/test/api/v3/integration/groups/POST-groups_groupId_leave.test.js b/test/api/v3/integration/groups/POST-groups_groupId_leave.test.js index 56de50b822..b7b16a7f04 100644 --- a/test/api/v3/integration/groups/POST-groups_groupId_leave.test.js +++ b/test/api/v3/integration/groups/POST-groups_groupId_leave.test.js @@ -37,6 +37,7 @@ describe('POST /groups/:groupId/leave', () => { leader = groupLeader; member = members[0]; // eslint-disable-line prefer-destructuring memberCount = group.memberCount; + await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') }); }); it('prevents non members from leaving', async () => { @@ -152,6 +153,10 @@ describe('POST /groups/:groupId/leave', () => { type: 'guild', }, invites: 1, + leaderDetails: { + 'auth.timestamps.created': new Date('2022-01-01'), + balance: 10, + }, }); privateGuild = group; diff --git a/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js b/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js index 7b7fc0e56e..c8a903d891 100644 --- a/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js +++ b/test/api/v3/integration/groups/POST-groups_id_removeMember.test.js @@ -153,6 +153,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => { }, invites: 1, members: 2, + leaderDetails: { 'auth.timestamps.created': new Date('2022-01-01') }, }); party = group; diff --git a/test/api/v3/integration/notifications/prevent-multiple-notification.js b/test/api/v3/integration/notifications/prevent-multiple-notification.js index cc4fd20a3d..31da8c1e16 100644 --- a/test/api/v3/integration/notifications/prevent-multiple-notification.js +++ b/test/api/v3/integration/notifications/prevent-multiple-notification.js @@ -25,6 +25,7 @@ describe('Prevent multiple notifications', () => { for (let i = 0; i < 4; i += 1) { for (let memberIndex = 0; memberIndex < partyMembers.length; memberIndex += 1) { + await partyMembers[memberIndex].update({ 'auth.timestamps.created': new Date('2022-01-01') }); // eslint-disable-line no-await-in-loop multipleChatMessages.push( partyMembers[memberIndex].post(`/groups/${party._id}/chat`, { message: `Message ${i}_${memberIndex}` }), ); diff --git a/test/helpers/start-server.js b/test/helpers/start-server.js index 8830224f70..570f53461f 100644 --- a/test/helpers/start-server.js +++ b/test/helpers/start-server.js @@ -11,6 +11,7 @@ if (process.env.LOAD_SERVER === '0') { // when the server is in a different proc setupNconf('./config.json.example'); nconf.set('NODE_DB_URI', nconf.get('TEST_DB_URI')); nconf.set('NODE_ENV', 'test'); + nconf.set('ACCOUNT_MIN_CHAT_AGE', '2'); nconf.set('IS_TEST', true); // We require src/server and not src/index because // 1. nconf is already setup diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json index e5186a08f8..e6c0f943cc 100644 --- a/website/common/locales/en/groups.json +++ b/website/common/locales/en/groups.json @@ -364,5 +364,6 @@ "managerNotes": "Manager's Notes", "assignedDateOnly": "Assigned on <%= date %>", "assignedDateAndUser": "Assigned by @<%- username %> on <%= date %>", - "claimRewards": "Claim Rewards" + "claimRewards": "Claim Rewards", + "chatTemporarilyUnavailable": "Chat is temporarily unavailable. Please try again later." } diff --git a/website/server/controllers/api-v3/chat.js b/website/server/controllers/api-v3/chat.js index c19a16e5be..72df0f719d 100644 --- a/website/server/controllers/api-v3/chat.js +++ b/website/server/controllers/api-v3/chat.js @@ -1,3 +1,4 @@ +import moment from 'moment'; import nconf from 'nconf'; import { authWithHeaders } from '../../middlewares/auth'; import { model as Group } from '../../models/group'; @@ -22,7 +23,11 @@ import { getMatchesByWordArray } from '../../libs/stringUtils'; import bannedSlurs from '../../libs/bannedSlurs'; import apiError from '../../libs/apiError'; import highlightMentions from '../../libs/highlightMentions'; +import { getAnalyticsServiceByEnvironment } from '../../libs/analyticsService'; +const analytics = getAnalyticsServiceByEnvironment(); + +const ACCOUNT_MIN_CHAT_AGE = Number(nconf.get('ACCOUNT_MIN_CHAT_AGE')); const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map(email => ({ email, canSend: true })); /** @@ -188,6 +193,17 @@ api.postChat = { throw new NotAuthorized(res.t('messageGroupChatSpam')); } + // Check if account is newer than the minimum age for chat participation + if (moment().diff(user.auth.timestamps.created, 'minutes') < ACCOUNT_MIN_CHAT_AGE) { + analytics.track('chat age error', { + uuid: user._id, + hitType: 'event', + category: 'behavior', + headers: req.headers, + }); + throw new BadRequest(res.t('chatTemporarilyUnavailable')); + } + const sanitizedMessageText = sanitizeMessageText(req.body.message); const [message, mentions, mentionedMembers] = await highlightMentions(sanitizedMessageText); let client = req.headers['x-client'] || '3rd Party';