Merge pull request #11170 from HabitRPG/Yutsuten-party-chat-translations

Party Chat Translations (Continuation of #10019)
This commit is contained in:
Matteo Pagliazzi 2019-05-26 11:45:12 +02:00 committed by GitHub
commit 270078a030
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 671 additions and 91 deletions

View file

@ -16,7 +16,7 @@ describe('auth middleware', () => {
describe('auth with headers', () => {
it('allows to specify a list of user field that we do not want to load', (done) => {
const authWithHeaders = authWithHeadersFactory({
userFieldsToExclude: ['items', 'flags', 'auth.timestamps'],
userFieldsToExclude: ['items'],
});
req.headers['x-api-user'] = user._id;
@ -27,11 +27,34 @@ describe('auth middleware', () => {
const userToJSON = res.locals.user.toJSON();
expect(userToJSON.items).to.not.exist;
expect(userToJSON.flags).to.not.exist;
expect(userToJSON.auth.timestamps).to.not.exist;
expect(userToJSON.auth).to.exist;
done();
});
});
it('makes sure some fields are always included', (done) => {
const authWithHeaders = authWithHeadersFactory({
userFieldsToExclude: [
'items', 'auth.timestamps',
'preferences', 'notifications', '_id', 'flags', 'auth', // these are always loaded
],
});
req.headers['x-api-user'] = user._id;
req.headers['x-api-key'] = user.apiToken;
authWithHeaders(req, res, (err) => {
if (err) return done(err);
const userToJSON = res.locals.user.toJSON();
expect(userToJSON.items).to.not.exist;
expect(userToJSON.auth.timestamps).to.exist;
expect(userToJSON.auth).to.exist;
expect(userToJSON.notifications).to.exist;
expect(userToJSON.preferences).to.exist;
expect(userToJSON._id).to.exist;
expect(userToJSON.flags).to.exist;
done();
});

View file

@ -1,7 +1,7 @@
import moment from 'moment';
import { v4 as generateUUID } from 'uuid';
import validator from 'validator';
import { sleep } from '../../../helpers/api-unit.helper';
import { sleep, translationCheck } from '../../../helpers/api-unit.helper';
import {
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
@ -271,7 +271,16 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member attacks Wailing Whale for 5.0 damage.` `Wailing Whale attacks party for 7.5 damage.`');
expect(Group.prototype.sendChat).to.be.calledWith({
message: '`Participating Member attacks Wailing Whale for 5.0 damage. Wailing Whale attacks party for 7.5 damage.`',
info: {
bossDamage: '7.5',
quest: 'whale',
type: 'boss_damage',
user: 'Participating Member',
userDamage: '5.0',
},
});
});
it('applies damage only to participating members of party', async () => {
@ -344,7 +353,10 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledTwice;
expect(Group.prototype.sendChat).to.be.calledWith('`You defeated Wailing Whale! Questing party members receive the rewards of victory.`');
expect(Group.prototype.sendChat).to.be.calledWith({
message: '`You defeated Wailing Whale! Questing party members receive the rewards of victory.`',
info: { quest: 'whale', type: 'boss_defeated' },
});
});
it('calls finishQuest when boss has <= 0 hp', async () => {
@ -387,7 +399,10 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledWith(quest.boss.rage.effect('en'));
expect(Group.prototype.sendChat).to.be.calledWith({
message: quest.boss.rage.effect('en'),
info: { quest: 'trex_undead', type: 'boss_rage' },
});
expect(party.quest.progress.hp).to.eql(383.5);
expect(party.quest.progress.rage).to.eql(0);
});
@ -437,7 +452,10 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledWith(quest.boss.rage.effect('en'));
expect(Group.prototype.sendChat).to.be.calledWith({
message: quest.boss.rage.effect('en'),
info: { quest: 'lostMasterclasser4', type: 'boss_rage' },
});
expect(party.quest.progress.rage).to.eql(0);
let drainedUser = await User.findById(participatingMember._id);
@ -488,7 +506,15 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member found 5 Bars of Soap.`');
expect(Group.prototype.sendChat).to.be.calledWith({
message: '`Participating Member found 5 Bars of Soap.`',
info: {
items: { soapBars: 5 },
quest: 'atom1',
type: 'user_found_items',
user: 'Participating Member',
},
});
});
it('sends a chat message if no progress is made', async () => {
@ -499,7 +525,15 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member found 0 Bars of Soap.`');
expect(Group.prototype.sendChat).to.be.calledWith({
message: '`Participating Member found 0 Bars of Soap.`',
info: {
items: { soapBars: 0 },
quest: 'atom1',
type: 'user_found_items',
user: 'Participating Member',
},
});
});
it('sends a chat message if no progress is made on quest with multiple items', async () => {
@ -516,9 +550,15 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWithMatch(/`Participating Member found/);
expect(Group.prototype.sendChat).to.be.calledWithMatch(/0 Blue Fins/);
expect(Group.prototype.sendChat).to.be.calledWithMatch(/0 Fire Coral/);
expect(Group.prototype.sendChat).to.be.calledWith({
message: '`Participating Member found 0 Fire Coral, 0 Blue Fins.`',
info: {
items: { blueFins: 0, fireCoral: 0 },
quest: 'dilatoryDistress1',
type: 'user_found_items',
user: 'Participating Member',
},
});
});
it('handles collection quests with multiple items', async () => {
@ -535,8 +575,14 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWithMatch(/`Participating Member found/);
expect(Group.prototype.sendChat).to.be.calledWithMatch(/\d* (Tracks|Broken Twigs)/);
expect(Group.prototype.sendChat).to.be.calledWithMatch({
message: sinon.match(/`Participating Member found/).and(sinon.match(/\d* (Tracks|Broken Twigs)/)),
info: {
quest: 'evilsanta2',
type: 'user_found_items',
user: 'Participating Member',
},
});
});
it('sends message about victory', async () => {
@ -547,7 +593,10 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledTwice;
expect(Group.prototype.sendChat).to.be.calledWith('`All items found! Party has received their rewards.`');
expect(Group.prototype.sendChat).to.be.calledWith({
message: '`All items found! Party has received their rewards.`',
info: { type: 'all_items_found' },
});
});
it('calls finishQuest when all items are found', async () => {
@ -718,6 +767,258 @@ describe('Group Model', () => {
expect(res.t).to.not.be.called;
});
});
describe('translateSystemMessages', () => {
it('translate quest_start', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'quest_start',
quest: 'basilist',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate boss_damage', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'boss_damage',
user: questLeader.profile.name,
quest: 'basilist',
userDamage: 15.3,
bossDamage: 3.7,
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate boss_dont_attack', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'boss_dont_attack',
user: questLeader.profile.name,
quest: 'basilist',
userDamage: 15.3,
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate boss_rage', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'boss_rage',
quest: 'lostMasterclasser3',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate boss_defeated', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'boss_defeated',
quest: 'lostMasterclasser3',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate user_found_items', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'user_found_items',
user: questLeader.profile.name,
quest: 'lostMasterclasser1',
items: {
ancientTome: 3,
forbiddenTome: 2,
hiddenTome: 1,
},
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate all_items_found', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'all_items_found',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate spell_cast_party', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'spell_cast_party',
user: questLeader.profile.name,
class: 'wizard',
spell: 'earth',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate spell_cast_user', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'spell_cast_user',
user: questLeader.profile.name,
class: 'special',
spell: 'snowball',
target: participatingMember.profile.name,
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate quest_cancel', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'quest_cancel',
user: questLeader.profile.name,
quest: 'basilist',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate quest_abort', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'quest_abort',
user: questLeader.profile.name,
quest: 'basilist',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate tavern_quest_completed', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'tavern_quest_completed',
quest: 'stressbeast',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate tavern_boss_rage_tired', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'tavern_boss_rage_tired',
quest: 'stressbeast',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate tavern_boss_rage', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'tavern_boss_rage',
quest: 'dysheartener',
scene: 'market',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate tavern_boss_desperation', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'tavern_boss_desperation',
quest: 'stressbeast',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate claim_task', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'claim_task',
user: questLeader.profile.name,
task: 'Feed the pet',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
});
describe('toJSONCleanChat', () => {
it('shows messages with 1 flag to non-admins', async () => {
party.chat = [{
flagCount: 1,
info: {
type: 'quest_start',
quest: 'basilist',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
expect(toJSON.chat.length).to.equal(1);
});
it('shows messages with >= 2 flag to admins', async () => {
party.chat = [{
flagCount: 3,
info: {
type: 'quest_start',
quest: 'basilist',
},
}];
const admin = new User({'contributor.admin': true});
let toJSON = await Group.toJSONCleanChat(party, admin);
expect(toJSON.chat.length).to.equal(1);
});
it('doesn\'t show flagged messages to non-admins', async () => {
party.chat = [{
flagCount: 3,
info: {
type: 'quest_start',
quest: 'basilist',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
expect(toJSON.chat.length).to.equal(0);
});
});
});
context('Instance Methods', () => {
@ -1007,20 +1308,22 @@ describe('Group Model', () => {
});
it('formats message', () => {
const chatMessage = party.sendChat('a new message', {
_id: 'user-id',
profile: { name: 'user name' },
contributor: {
toObject () {
return 'contributor object';
const chatMessage = party.sendChat({
message: 'a new message', user: {
_id: 'user-id',
profile: { name: 'user name' },
contributor: {
toObject () {
return 'contributor object';
},
},
},
backer: {
toObject () {
return 'backer object';
backer: {
toObject () {
return 'backer object';
},
},
},
});
}}
);
const chat = chatMessage;
@ -1037,7 +1340,7 @@ describe('Group Model', () => {
});
it('formats message as system if no user is passed in', () => {
const chat = party.sendChat('a system message');
const chat = party.sendChat({message: 'a system message'});
expect(chat.text).to.eql('a system message');
expect(validator.isUUID(chat.id)).to.eql(true);
@ -1052,7 +1355,7 @@ describe('Group Model', () => {
});
it('updates users about new messages in party', () => {
party.sendChat('message');
party.sendChat({message: 'message'});
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({
@ -1066,7 +1369,7 @@ describe('Group Model', () => {
type: 'guild',
});
group.sendChat('message');
group.sendChat({message: 'message'});
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({
@ -1076,7 +1379,7 @@ describe('Group Model', () => {
});
it('does not send update to user that sent the message', () => {
party.sendChat('message', {_id: 'user-id', profile: { name: 'user' }});
party.sendChat({message: 'message', user: {_id: 'user-id', profile: { name: 'user' }}});
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({
@ -1088,7 +1391,7 @@ describe('Group Model', () => {
it('skips sending new message notification for guilds with > 5000 members', () => {
party.memberCount = 5001;
party.sendChat('message');
party.sendChat({message: 'message'});
expect(User.update).to.not.be.called;
});
@ -1096,7 +1399,7 @@ describe('Group Model', () => {
it('skips sending messages to the tavern', () => {
party._id = TAVERN_ID;
party.sendChat('message');
party.sendChat({message: 'message'});
expect(User.update).to.not.be.called;
});
@ -1928,7 +2231,7 @@ describe('Group Model', () => {
await guild.save();
const groupMessage = guild.sendChat('Test message.');
const groupMessage = guild.sendChat({message: 'Test message.'});
await groupMessage.save();
await sleep();

View file

@ -127,7 +127,13 @@ describe('POST /groups/:groupId/quests/abort', () => {
members: {},
});
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWithMatch(/aborted the party quest Wail of the Whale.`/);
expect(Group.prototype.sendChat).to.be.calledWithMatch({
message: sinon.match(/aborted the party quest Wail of the Whale.`/),
info: {
quest: 'whale',
type: 'quest_abort',
},
});
stub.restore();
});

View file

@ -141,7 +141,14 @@ describe('POST /groups/:groupId/quests/cancel', () => {
members: {},
});
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWithMatch(/cancelled the party quest Wail of the Whale.`/);
expect(Group.prototype.sendChat).to.be.calledWithMatch({
message: sinon.match(/cancelled the party quest Wail of the Whale.`/),
info: {
quest: 'whale',
type: 'quest_cancel',
user: sinon.match.any,
},
});
stub.restore();
});

View file

@ -8,6 +8,7 @@ import mongo from './mongo'; // eslint-disable-line
import moment from 'moment';
import i18n from '../../website/common/script/i18n';
import * as Tasks from '../../website/server/models/task';
export { translationCheck } from './translate';
afterEach((done) => {
sandbox.restore();

View file

@ -16,3 +16,9 @@ export function translate (key, variables, language) {
return translatedString;
}
export function translationCheck (translatedString) {
expect(translatedString).to.not.be.empty;
expect(translatedString).to.not.eql(STRING_ERROR_MSG);
expect(translatedString).to.not.match(STRING_DOES_NOT_EXIST_MSG);
}

View file

@ -175,6 +175,8 @@
"youCast": "You cast <%= spell %>.",
"youCastTarget": "You cast <%= spell %> on <%= target %>.",
"youCastParty": "You cast <%= spell %> for the party.",
"chatCastSpellParty": "<%= username %> casts <%= spell %> for the party.",
"chatCastSpellUser": "<%= username %> casts <%= spell %> on <%= target %>.",
"critBonus": "Critical Hit! Bonus: ",
"gainedGold": "You gained some Gold",
"gainedMana": "You gained some Mana",

View file

@ -127,5 +127,14 @@
"bossHealth": "<%= currentHealth %> / <%= maxHealth %> Health",
"rageAttack": "Rage Attack:",
"bossRage": "<%= currentRage %> / <%= maxRage %> Rage",
"rageStrikes": "Rage Strikes"
"rageStrikes": "Rage Strikes",
"chatQuestStarted": "Your quest, <%= questName %>, has started.",
"chatBossDamage": "<%= username %> attacks <%= bossName %> for <%= userDamage %> damage. <%= bossName %> attacks party for <%= bossDamage %> damage.",
"chatBossDontAttack": "<%= username %> attacks <%= bossName %> for <%= userDamage %> damage. <%= bossName %> does not attack, because it respects the fact that there are some bugs post-maintenance, and it doesn't want to hurt anyone unfairly. It will continue its rampage soon!",
"chatBossDefeated": "You defeated <%= bossName %>! Questing party members receive the rewards of victory.",
"chatFindItems": "<%= username %> found <%= items %>.",
"chatItemQuestFinish": "All items found! Party has received their rewards.",
"chatQuestAborted": "<%= username %> aborted the party quest <%= questName %>.",
"chatQuestCancelled": "<%= username %> cancelled the party quest <%= questName %>.",
"tavernBossTired": "<%= bossName %> tries to unleash <%= rageName %> but is too tired."
}

View file

@ -442,7 +442,8 @@ api.getGroupChallenges = {
method: 'GET',
url: '/challenges/groups/:groupId',
middlewares: [authWithHeaders({
userFieldsToInclude: ['_id', 'party', 'guilds'],
// Some fields (including _id) are always loaded (see middlewares/auth)
userFieldsToInclude: ['party', 'guilds'], // Some fields are always loaded (see middlewares/auth)
})],
async handler (req, res) {
let user = res.locals.user;

View file

@ -186,7 +186,7 @@ api.postChat = {
if (client) {
client = client.replace('habitica-', '');
}
const newChatMessage = group.sendChat(req.body.message, user, null, client);
const newChatMessage = group.sendChat({message: req.body.message, user, metaData: null, client});
let toSave = [newChatMessage.save()];
if (group.type === 'party') {

View file

@ -375,7 +375,8 @@ api.getGroup = {
method: 'GET',
url: '/groups/:groupId',
middlewares: [authWithHeaders({
userFieldsToInclude: ['_id', 'party', 'guilds', 'contributor'],
// Some fields (including _id, preferences) are always loaded (see middlewares/auth)
userFieldsToInclude: ['party', 'guilds', 'contributor'],
})],
async handler (req, res) {
let user = res.locals.user;

View file

@ -371,7 +371,14 @@ api.cancelQuest = {
if (group.quest.active) throw new NotAuthorized(res.t('cantCancelActiveQuest'));
let questName = questScrolls[group.quest.key].text('en');
const newChatMessage = group.sendChat(`\`${user.profile.name} cancelled the party quest ${questName}.\``);
const newChatMessage = group.sendChat({
message: `\`${user.profile.name} cancelled the party quest ${questName}.\``,
info: {
type: 'quest_cancel',
user: user.profile.name,
quest: group.quest.key,
},
});
group.quest = Group.cleanGroupQuest();
group.markModified('quest');
@ -427,7 +434,14 @@ api.abortQuest = {
if (user._id !== group.leader && user._id !== group.quest.leader) throw new NotAuthorized(res.t('onlyLeaderAbortQuest'));
let questName = questScrolls[group.quest.key].text('en');
const newChatMessage = group.sendChat(`\`${user.profile.name} aborted the party quest ${questName}.\``);
const newChatMessage = group.sendChat({
message: `\`${common.i18n.t('chatQuestAborted', {username: user.profile.name, questName}, 'en')}\``,
info: {
type: 'quest_abort',
user: user.profile.name,
quest: group.quest.key,
},
});
await newChatMessage.save();
let memberUpdates = User.update({

View file

@ -285,7 +285,8 @@ api.getUserTasks = {
method: 'GET',
url: '/tasks/user',
middlewares: [authWithHeaders({
userFieldsToInclude: ['_id', 'tasksOrder', 'preferences'],
// Some fields (including _id, preferences) are always loaded (see middlewares/auth)
userFieldsToInclude: ['tasksOrder'],
})],
async handler (req, res) {
let types = Tasks.tasksTypes.map(type => `${type}s`);

View file

@ -204,7 +204,14 @@ api.assignTask = {
// User is claiming the task
if (user._id === assignedUserId) {
let message = res.t('userIsClamingTask', {username: user.profile.name, task: task.text});
const newMessage = group.sendChat(message);
const newMessage = group.sendChat({
message,
info: {
type: 'claim_task',
user: user.profile.name,
task: task.text,
},
});
promises.push(newMessage.save());
} else {
const taskText = task.text;

View file

@ -1,6 +1,10 @@
import { chatModel as Chat } from '../../models/message';
import shared from '../../../common';
import _ from 'lodash';
import { MAX_CHAT_COUNT, MAX_SUBBED_GROUP_CHAT_COUNT } from '../../models/group';
const questScrolls = shared.content.quests;
// @TODO: Don't use this method when the group can be saved.
export async function getGroupChat (group) {
const maxChatCount = group.isSubscribed() ? MAX_SUBBED_GROUP_CHAT_COUNT : MAX_CHAT_COUNT;
@ -22,3 +26,82 @@ export async function getGroupChat (group) {
return previous;
}, []);
}
export function translateMessage (lang, info) {
let msg;
let foundText = '';
let spells = shared.content.spells;
let quests = shared.content.quests;
switch (info.type) {
case 'quest_start':
msg = `\`${shared.i18n.t('chatQuestStarted', {questName: questScrolls[info.quest].text(lang)}, lang)}\``;
break;
case 'boss_damage':
msg = `\`${shared.i18n.t('chatBossDamage', {username: info.user, bossName: questScrolls[info.quest].boss.name(lang), userDamage: info.userDamage, bossDamage: info.bossDamage}, lang)}\``;
break;
case 'boss_dont_attack':
msg = `\`${shared.i18n.t('chatBossDontAttack', {username: info.user, bossName: questScrolls[info.quest].boss.name(lang), userDamage: info.userDamage}, lang)}\``;
break;
case 'boss_rage':
msg = `\`${questScrolls[info.quest].boss.rage.effect(lang)}\``;
break;
case 'boss_defeated':
msg = `\`${shared.i18n.t('chatBossDefeated', {bossName: questScrolls[info.quest].boss.name(lang)}, lang)}\``;
break;
case 'user_found_items':
foundText = _.reduce(info.items, (m, v, k) => {
m.push(`${v} ${questScrolls[info.quest].collect[k].text(lang)}`);
return m;
}, []).join(', ');
msg = `\`${shared.i18n.t('chatFindItems', {username: info.user, items: foundText}, lang)}\``;
break;
case 'all_items_found':
msg = `\`${shared.i18n.t('chatItemQuestFinish', lang)}\``;
break;
case 'spell_cast_party':
msg = `\`${shared.i18n.t('chatCastSpellParty', {username: info.user, spell: spells[info.class][info.spell].text(lang)}, lang)}\``;
break;
case 'spell_cast_user':
msg = `\`${shared.i18n.t('chatCastSpellUser', {username: info.user, spell: spells[info.class][info.spell].text(lang), target: info.target}, lang)}\``;
break;
case 'quest_cancel':
msg = `\`${shared.i18n.t('chatQuestCancelled', {username: info.user, questName: questScrolls[info.quest].text(lang)}, lang)}\``;
break;
case 'quest_abort':
msg = `\`${shared.i18n.t('chatQuestAborted', {username: info.user, questName: questScrolls[info.quest].text(lang)}, lang)}\``;
break;
case 'tavern_quest_completed':
msg = `\`${quests[info.quest].completionChat(lang)}\``;
break;
case 'tavern_boss_rage_tired':
msg = `\`${shared.i18n.t('tavernBossTired', {rageName: quests[info.quest].boss.rage.title(lang), bossName: quests[info.quest].boss.name(lang)}, lang)}\``;
break;
case 'tavern_boss_rage':
msg = `\`${quests[info.quest].boss.rage[info.scene](lang)}\``;
break;
case 'tavern_boss_desperation':
msg = `\`${quests[info.quest].boss.desperation.text(lang)}\``;
break;
case 'claim_task':
msg = `${shared.i18n.t('userIsClamingTask', {username: info.user, task: info.task}, lang)}`;
break;
}
return msg;
}

View file

@ -197,9 +197,30 @@ async function castSpell (req, res, {isV3 = false}) {
});
if (party && !spell.silent) {
let message = `\`${user.profile.name} casts ${spell.text()}${targetType === 'user' ? ` on ${partyMembers.profile.name}` : ' for the party'}.\``;
const newChatMessage = party.sendChat(message);
await newChatMessage.save();
if (targetType === 'user') {
const newChatMessage = party.sendChat({
message: `\`${common.i18n.t('chatCastSpellUser', {username: user.profile.name, spell: spell.text(), target: partyMembers.profile.name}, 'en')}\``,
info: {
type: 'spell_cast_user',
user: user.profile.name,
class: klass,
spell: spellId,
target: partyMembers.profile.name,
},
});
await newChatMessage.save();
} else {
const newChatMessage = party.sendChat({
message: `\`${common.i18n.t('chatCastSpellParty', {username: user.profile.name, spell: spell.text()}, 'en')}\``,
info: {
type: 'spell_cast_party',
user: user.profile.name,
class: klass,
spell: spellId,
},
});
await newChatMessage.save();
}
}
}
}

View file

@ -9,22 +9,27 @@ import url from 'url';
import gcpStackdriverTracer from '../libs/gcpTraceAgent';
const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL');
const USER_FIELDS_ALWAYS_LOADED = ['_id', 'notifications', 'preferences', 'auth', 'flags'];
function getUserFields (options, req) {
// A list of user fields that aren't needed for the route and are not loaded from the db.
// Must be an array
if (options.userFieldsToExclude) {
return options.userFieldsToExclude.map(field => {
return `-${field}`; // -${field} means exclude ${field} in mongodb
}).join(' ');
return options.userFieldsToExclude
.filter(field => {
return !USER_FIELDS_ALWAYS_LOADED.find(fieldToInclude => field.startsWith(fieldToInclude));
})
.map(field => {
return `-${field}`; // -${field} means exclude ${field} in mongodb
})
.join(' ');
}
if (options.userFieldsToInclude) {
return options.userFieldsToInclude.join(' ');
return options.userFieldsToInclude.concat(USER_FIELDS_ALWAYS_LOADED).join(' ');
}
// Allows GET requests to /user to specify a list of user fields to return instead of the entire doc
// Notifications are always included
const urlPath = url.parse(req.url).pathname;
const userFields = req.query.userFields;
if (!userFields || urlPath !== '/user') return '';
@ -32,7 +37,7 @@ function getUserFields (options, req) {
const userFieldOptions = userFields.split(',');
if (userFieldOptions.length === 0) return '';
return `notifications ${userFieldOptions.join(' ')}`;
return userFieldOptions.concat(USER_FIELDS_ALWAYS_LOADED).join(' ');
}
// Make sure stackdriver traces are storing the user id

View file

@ -37,7 +37,7 @@ import {
} from './subscriptionPlan';
import amazonPayments from '../libs/payments/amazon';
import stripePayments from '../libs/payments/stripe';
import { getGroupChat } from '../libs/chat/group-chat';
import { getGroupChat, translateMessage } from '../libs/chat/group-chat';
import { model as UserNotification } from './userNotification';
const questScrolls = shared.content.quests;
@ -344,26 +344,38 @@ schema.statics.toJSONCleanChat = async function groupToJSONCleanChat (group, use
await getGroupChat(group);
}
let toJSON = group.toJSON();
const groupToJson = group.toJSON();
const userLang = user.preferences.language;
if (!user.contributor.admin) {
_.remove(toJSON.chat, chatMsg => {
chatMsg.flags = {};
if (chatMsg._meta) chatMsg._meta = undefined;
return user._id !== chatMsg.uuid && chatMsg.flagCount >= 2;
});
}
groupToJson.chat = groupToJson.chat
.map(chatMsg => {
// Translate system messages
if (!_.isEmpty(chatMsg.info)) {
chatMsg.text = translateMessage(userLang, chatMsg.info);
}
// Convert to timestamps because Android expects it
toJSON.chat.forEach(chat => {
// old chats are saved with a numeric timestamp
// new chats use `Date` which then has to be converted to the numeric timestamp
if (chat.timestamp && chat.timestamp.getTime) {
chat.timestamp = chat.timestamp.getTime();
}
});
// Convert to timestamps because Android expects it
// old chats are saved with a numeric timestamp
// new chats use `Date` which then has to be converted to the numeric timestamp
if (chatMsg.timestamp && chatMsg.timestamp.getTime) {
chatMsg.timestamp = chatMsg.timestamp.getTime();
}
return toJSON;
if (!user.contributor.admin) {
// Flags are hidden to non admins
chatMsg.flags = {};
if (chatMsg._meta) chatMsg._meta = undefined;
// Messages with >= 2 flags are hidden to non admins and non authors
if (user._id !== chatMsg.uuid && chatMsg.flagCount >= 2) return undefined;
}
return chatMsg;
})
// Used to filter for undefined chat messages that should not be shown to non-admins
.filter(chatMsg => chatMsg !== undefined);
return groupToJson;
};
function getInviteError (uuids, emails, usernames) {
@ -496,8 +508,9 @@ schema.methods.getMemberCount = async function getMemberCount () {
return await User.count(query).exec();
};
schema.methods.sendChat = function sendChat (message, user, metaData, client) {
let newMessage = messageDefaults(message, user, client);
schema.methods.sendChat = function sendChat (options = {}) {
const {message, user, metaData, client, info = {}} = options;
let newMessage = messageDefaults(message, user, client, info);
let newChatMessage = new Chat();
newChatMessage = Object.assign(newChatMessage, newMessage);
newChatMessage.groupId = this._id;
@ -653,8 +666,15 @@ schema.methods.startQuest = async function startQuest (user) {
}, _cleanQuestParty(),
{ multi: true }).exec();
const newMessage = this.sendChat(`\`Your quest, ${quest.text('en')}, has started.\``, null, {
participatingMembers: this.getParticipatingQuestMembers().join(', '),
const newMessage = this.sendChat({
message: `\`${shared.i18n.t('chatQuestStarted', {questName: quest.text('en')}, 'en')}\``,
metaData: {
participatingMembers: this.getParticipatingQuestMembers().join(', '),
},
info: {
type: 'quest_start',
quest: quest.key,
},
});
await newMessage.save();
@ -913,18 +933,42 @@ schema.methods._processBossQuest = async function processBossQuest (options) {
const promises = [];
group.quest.progress.hp -= progress.up;
// TODO Create a party preferred language option so emits like this can be localized. Suggestion: Always display the English version too. Or, if English is not displayed to the players, at least include it in a new field in the chat object that's visible in the database - essential for admins when troubleshooting quests!
let playerAttack = `${user.profile.name} attacks ${quest.boss.name('en')} for ${progress.up.toFixed(1)} damage.`;
let bossAttack = CRON_SAFE_MODE || CRON_SEMI_SAFE_MODE ? `${quest.boss.name('en')} does not attack, because it respects the fact that there are some bugs\` \`post-maintenance and it doesn't want to hurt anyone unfairly. It will continue its rampage soon!` : `${quest.boss.name('en')} attacks party for ${Math.abs(down).toFixed(1)} damage.`;
// TODO Consider putting the safe mode boss attack message in an ENV var
const groupMessage = group.sendChat(`\`${playerAttack}\` \`${bossAttack}\``);
promises.push(groupMessage.save());
if (CRON_SAFE_MODE || CRON_SEMI_SAFE_MODE) {
const groupMessage = group.sendChat({
message: `\`${shared.i18n.t('chatBossDontAttack', {bossName: quest.boss.name('en')}, 'en')}\``,
info: {
type: 'boss_dont_attack',
user: user.profile.name,
quest: group.quest.key,
userDamage: progress.up.toFixed(1),
},
});
promises.push(groupMessage.save());
} else {
const groupMessage = group.sendChat({
message: `\`${shared.i18n.t('chatBossDamage', {username: user.profile.name, bossName: quest.boss.name('en'), userDamage: progress.up.toFixed(1), bossDamage: Math.abs(down).toFixed(1)}, user.preferences.language)}\``,
info: {
type: 'boss_damage',
user: user.profile.name,
quest: group.quest.key,
userDamage: progress.up.toFixed(1),
bossDamage: Math.abs(down).toFixed(1),
},
});
promises.push(groupMessage.save());
}
// If boss has Rage, increment Rage as well
if (quest.boss.rage) {
group.quest.progress.rage += Math.abs(down);
if (group.quest.progress.rage >= quest.boss.rage.value) {
const rageMessage = group.sendChat(quest.boss.rage.effect('en'));
const rageMessage = group.sendChat({
message: quest.boss.rage.effect('en'),
info: {
type: 'boss_rage',
quest: quest.key,
},
});
promises.push(rageMessage.save());
group.quest.progress.rage = 0;
@ -952,7 +996,13 @@ schema.methods._processBossQuest = async function processBossQuest (options) {
// Boss slain, finish quest
if (group.quest.progress.hp <= 0) {
const questFinishChat = group.sendChat(`\`You defeated ${quest.boss.name('en')}! Questing party members receive the rewards of victory.\``);
const questFinishChat = group.sendChat({
message: `\`${shared.i18n.t('chatBossDefeated', {bossName: quest.boss.name('en')}, 'en')}\``,
info: {
type: 'boss_defeated',
quest: quest.key,
},
});
promises.push(questFinishChat.save());
// Participants: Grant rewards & achievements, finish quest
@ -1005,7 +1055,15 @@ schema.methods._processCollectionQuest = async function processCollectionQuest (
}, []);
foundText = foundText.join(', ');
const foundChat = group.sendChat(`\`${user.profile.name} found ${foundText}.\``);
const foundChat = group.sendChat({
message: `\`${shared.i18n.t('chatFindItems', {username: user.profile.name, items: foundText}, 'en')}\``,
info: {
type: 'user_found_items',
user: user.profile.name,
quest: quest.key,
items: itemsFound,
},
});
group.markModified('quest.progress.collect');
// Still needs completing
@ -1018,7 +1076,12 @@ schema.methods._processCollectionQuest = async function processCollectionQuest (
}
await group.finishQuest(quest);
const allItemsFoundChat = group.sendChat('`All items found! Party has received their rewards.`');
const allItemsFoundChat = group.sendChat({
message: `\`${shared.i18n.t('chatItemQuestFinish', 'en')}\``,
info: {
type: 'all_items_found',
},
});
const promises = [group.save(), foundChat.save(), allItemsFoundChat.save()];
@ -1082,7 +1145,13 @@ schema.statics.tavernBoss = async function tavernBoss (user, progress) {
const chatPromises = [];
if (tavern.quest.progress.hp <= 0) {
const completeChat = tavern.sendChat(quest.completionChat('en'));
const completeChat = tavern.sendChat({
message: quest.completionChat('en'),
info: {
type: 'tavern_quest_completed',
quest: quest.key,
},
});
chatPromises.push(completeChat.save());
await tavern.finishQuest(quest);
_.assign(tavernQuest, {extra: null});
@ -1111,11 +1180,24 @@ schema.statics.tavernBoss = async function tavernBoss (user, progress) {
}
if (!scene) {
const tiredChat = tavern.sendChat(`\`${quest.boss.name('en')} tries to unleash ${quest.boss.rage.title('en')} but is too tired.\``);
const tiredChat = tavern.sendChat({
message: `\`${shared.i18n.t('tavernBossTired', {rageName: quest.boss.rage.title('en'), bossName: quest.boss.name('en')}, 'en')}\``,
info: {
type: 'tavern_boss_rage_tired',
quest: quest.key,
},
});
chatPromises.push(tiredChat.save());
tavern.quest.progress.rage = 0; // quest.boss.rage.value;
} else {
const rageChat = tavern.sendChat(quest.boss.rage[scene]('en'));
const rageChat = tavern.sendChat({
message: quest.boss.rage[scene]('en'),
info: {
type: 'tavern_boss_rage',
quest: quest.key,
scene,
},
});
chatPromises.push(rageChat.save());
tavern.quest.extra.worldDmg[scene] = true;
tavern.markModified('quest.extra.worldDmg');
@ -1127,7 +1209,13 @@ schema.statics.tavernBoss = async function tavernBoss (user, progress) {
}
if (quest.boss.desperation && tavern.quest.progress.hp < quest.boss.desperation.threshold && !tavern.quest.extra.desperate) {
const progressChat = tavern.sendChat(quest.boss.desperation.text('en'));
const progressChat = tavern.sendChat({
message: quest.boss.desperation.text('en'),
info: {
type: 'tavern_boss_desperation',
quest: quest.key,
},
});
chatPromises.push(progressChat.save());
tavern.quest.extra.desperate = true;
tavern.quest.extra.def = quest.boss.desperation.def;

View file

@ -7,6 +7,7 @@ const defaultSchema = () => ({
id: String,
timestamp: Date,
text: String,
info: {$type: mongoose.Schema.Types.Mixed},
// sender properties
user: String, // profile name (unfortunately)
@ -101,12 +102,13 @@ export function setUserStyles (newMessage, user) {
newMessage.markModified('userStyles');
}
export function messageDefaults (msg, user, client) {
export function messageDefaults (msg, user, client, info = {}) {
const id = uuid();
const message = {
id,
_id: id,
text: msg.substring(0, 3000),
info,
timestamp: Number(new Date()),
likes: {},
flags: {},