diff --git a/common/locales/en/groups.json b/common/locales/en/groups.json index b1183a72aa..4a9e35faef 100644 --- a/common/locales/en/groups.json +++ b/common/locales/en/groups.json @@ -163,6 +163,7 @@ "partyOnName": "Party On", "partyUpAchievement": "Joined a Party with another person! Have fun battling monsters and supporting each other.", "partyOnAchievement": "Joined a Party with at least four people! Enjoy your increased accountability as you unite with your friends to vanquish your foes!", + "largeGroupNote": "Note: This Guild is now too large to support notifications! Be sure to check back every day to see new messages.", "groupIdRequired": "\"groupId\" must be a valid UUID", "groupNotFound": "Group not found or you don't have access.", "groupTypesRequired": "You must supply a valid \"type\" query string.", diff --git a/common/script/constants.js b/common/script/constants.js index d1cda98f0d..9be5756caa 100644 --- a/common/script/constants.js +++ b/common/script/constants.js @@ -3,4 +3,5 @@ export const MAX_LEVEL = 100; export const MAX_STAT_POINTS = MAX_LEVEL; export const ATTRIBUTES = ['str', 'int', 'per', 'con']; -export const TAVERN_ID = '00000000-0000-4000-A000-000000000000'; \ No newline at end of file +export const TAVERN_ID = '00000000-0000-4000-A000-000000000000'; +export const LARGE_GROUP_COUNT_MESSAGE_CUTOFF = 5000; diff --git a/common/script/index.js b/common/script/index.js index 91b290c2ed..5fd66a10a9 100644 --- a/common/script/index.js +++ b/common/script/index.js @@ -17,13 +17,18 @@ import { shouldDo, daysSince } from './cron'; api.shouldDo = shouldDo; api.daysSince = daysSince; -// TODO under api.constants? and capitalize exported names too import { MAX_HEALTH, MAX_LEVEL, MAX_STAT_POINTS, TAVERN_ID, + LARGE_GROUP_COUNT_MESSAGE_CUTOFF, } from './constants'; + +api.constants = { + LARGE_GROUP_COUNT_MESSAGE_CUTOFF, +}; +// TODO Move these under api.constants api.maxLevel = MAX_LEVEL; api.maxHealth = MAX_HEALTH; api.maxStatPoints = MAX_STAT_POINTS; diff --git a/test/api/v3/unit/models/group.test.js b/test/api/v3/unit/models/group.test.js index e3754ef2df..aa8ba37026 100644 --- a/test/api/v3/unit/models/group.test.js +++ b/test/api/v3/unit/models/group.test.js @@ -3,6 +3,8 @@ import { model as Group } from '../../../../../website/server/models/group'; import { model as User } from '../../../../../website/server/models/user'; import { quests as questScrolls } from '../../../../../common/script/content'; import * as email from '../../../../../website/server/libs/api-v3/email'; +import validator from 'validator'; +import { TAVERN_ID } from '../../../../../common/script/'; describe('Group Model', () => { let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember; @@ -395,6 +397,147 @@ describe('Group Model', () => { }); context('Instance Methods', () => { + describe('#sendChat', () => { + beforeEach(() => { + sandbox.spy(User, 'update'); + }); + + it('puts message at top of chat array', () => { + let oldMessage = { + text: 'a message', + }; + party.chat.push(oldMessage, oldMessage, oldMessage); + + party.sendChat('a new message', {_id: 'user-id', profile: { name: 'user name' }}); + + expect(party.chat).to.have.a.lengthOf(4); + expect(party.chat[0].text).to.eql('a new message'); + expect(party.chat[0].uuid).to.eql('user-id'); + }); + + it('formats message', () => { + party.sendChat('a new message', { + _id: 'user-id', + profile: { name: 'user name' }, + contributor: { toObject () { return 'contributor object'; } }, + backer: { toObject () { return 'backer object'; } }, + }); + + let chat = party.chat[0]; + + expect(chat.text).to.eql('a new message'); + expect(validator.isUUID(chat.id)).to.eql(true); + expect(chat.timestamp).to.be.a('number'); + expect(chat.likes).to.eql({}); + expect(chat.flags).to.eql({}); + expect(chat.flagCount).to.eql(0); + expect(chat.uuid).to.eql('user-id'); + expect(chat.contributor).to.eql('contributor object'); + expect(chat.backer).to.eql('backer object'); + expect(chat.user).to.eql('user name'); + }); + + it('formats message as system if no user is passed in', () => { + party.sendChat('a system message'); + + let chat = party.chat[0]; + + expect(chat.text).to.eql('a system message'); + expect(validator.isUUID(chat.id)).to.eql(true); + expect(chat.timestamp).to.be.a('number'); + expect(chat.likes).to.eql({}); + expect(chat.flags).to.eql({}); + expect(chat.flagCount).to.eql(0); + expect(chat.uuid).to.eql('system'); + expect(chat.contributor).to.not.exist;; + expect(chat.backer).to.not.exist;; + expect(chat.user).to.not.exist;; + }); + + it('cuts down chat to 200 messages', () => { + for (var i = 0; i < 220; i++) { + party.chat.push({ text: 'a message' }); + }; + + expect(party.chat).to.have.a.lengthOf(220); + + party.sendChat('message'); + + expect(party.chat).to.have.a.lengthOf(200); + }); + + it('updates users about new messages in party', () => { + party.sendChat('message'); + + expect(User.update).to.be.calledOnce; + expect(User.update).to.be.calledWithMatch({ + 'party._id': party._id, + _id: { $ne: '' }, + }, { + $set: { + [`newMessages.${party._id}`]: { + name: party.name, + value: true, + }, + }, + }); + }); + + it('updates users about new messages in group', () => { + let group = new Group({ + type: 'guild', + }); + + group.sendChat('message'); + + expect(User.update).to.be.calledOnce; + expect(User.update).to.be.calledWithMatch({ + 'guilds': group._id, + _id: { $ne: '' }, + }, { + $set: { + [`newMessages.${group._id}`]: { + name: group.name, + value: true, + }, + }, + }); + }); + + it('does not send update to user that sent the message', () => { + party.sendChat('message', {_id: 'user-id', profile: { name: 'user' }}); + + expect(User.update).to.be.calledOnce; + expect(User.update).to.be.calledWithMatch({ + 'party._id': party._id, + _id: { $ne: 'user-id' }, + }, { + $set: { + [`newMessages.${party._id}`]: { + name: party.name, + value: true, + }, + }, + }); + }); + + it('skips sending new message notification for guilds with > 5000 members', () => { + party.memberCount = 5001; + + party.sendChat('message'); + + expect(User.update).to.not.be.called; + }); + + it('skips sending messages to the tavern', () => { + party._id = TAVERN_ID; + + party.sendChat('message'); + + expect(User.update).to.not.be.called; + }); + }); + describe('#startQuest', () => { context('Failure Conditions', () => { it('throws an error if group is not a party', async () => { diff --git a/website/server/models/group.js b/website/server/models/group.js index 820041cc68..7fbeb8abe4 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -25,6 +25,9 @@ const Schema = mongoose.Schema; export const INVITES_LIMIT = 100; export const TAVERN_ID = shared.TAVERN_ID; +const NO_CHAT_NOTIFICATIONS = [TAVERN_ID]; +const LARGE_GROUP_COUNT_MESSAGE_CUTOFF = shared.constants.LARGE_GROUP_COUNT_MESSAGE_CUTOFF; + const CRON_SAFE_MODE = nconf.get('CRON_SAFE_MODE') === 'true'; const CRON_SEMI_SAFE_MODE = nconf.get('CRON_SEMI_SAFE_MODE') === 'true'; @@ -300,34 +303,30 @@ export function chatDefaults (msg, user) { return message; } -const NO_CHAT_NOTIFICATIONS = [TAVERN_ID]; - schema.methods.sendChat = function sendChat (message, user) { this.chat.unshift(chatDefaults(message, user)); this.chat.splice(200); - // Kick off chat notifications in the background. - let lastSeenUpdate = {$set: {}}; - lastSeenUpdate.$set[`newMessages.${this._id}`] = {name: this.name, value: true}; - // do not send notifications for guilds with more than 5000 users and for the tavern - if (NO_CHAT_NOTIFICATIONS.indexOf(this._id) !== -1 || this.memberCount > 5000) { - // TODO For Tavern, only notify them if their name was mentioned - // var profileNames = [] // get usernames from regex of @xyz. how to handle space-delimited profile names? - // User.update({'profile.name':{$in:profileNames}},lastSeenUpdate,{multi:true}).exec(); - } else { - let query = {}; - - if (this.type === 'party') { - query['party._id'] = this._id; - } else { - query.guilds = this._id; - } - - query._id = { $ne: user ? user._id : ''}; - - User.update(query, lastSeenUpdate, {multi: true}).exec(); + if (NO_CHAT_NOTIFICATIONS.indexOf(this._id) !== -1 || this.memberCount > LARGE_GROUP_COUNT_MESSAGE_CUTOFF) { + return; } + + // Kick off chat notifications in the background. + let lastSeenUpdate = {$set: { + [`newMessages.${this._id}`]: {name: this.name, value: true}, + }}; + let query = {}; + + if (this.type === 'party') { + query['party._id'] = this._id; + } else { + query.guilds = this._id; + } + + query._id = { $ne: user ? user._id : ''}; + + User.update(query, lastSeenUpdate, {multi: true}).exec(); }; schema.methods.startQuest = async function startQuest (user) { diff --git a/website/views/options/social/group.jade b/website/views/options/social/group.jade index 6232990915..4eb78e45a9 100644 --- a/website/views/options/social/group.jade +++ b/website/views/options/social/group.jade @@ -82,7 +82,7 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter span.glyphicon.glyphicon-ban-circle(tooltip=env.t('banTip')) a.media-body span(ng-click='clickMember(member._id, true)') - | {{member.profile.name}} + | {{member.profile.name}} span(ng-if='group.type === "party"') | (#[strong {{member.stats.hp.toFixed(1)}}] #{env.t('hp')}) tr(ng-if='::group.memberCount > group.members.length') @@ -145,6 +145,7 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter .popover-content markdown(text='group._editing ? groupCopy.leaderMessage : group.leaderMessage') div(ng-controller='ChatCtrl') + .alert.alert-info.alert-sm(ng-if='group.memberCount > Shared.constants.LARGE_GROUP_COUNT_MESSAGE_CUTOFF')=env.t('largeGroupNote') h3=env.t('chat') include ./chat-box