fix linting for server (except for length of apidoc)

This commit is contained in:
Matteo Pagliazzi
2019-10-10 20:11:50 +02:00
parent bcf7bcf03c
commit 8bae0223bb
84 changed files with 913 additions and 469 deletions

View File

@@ -12,7 +12,6 @@ import {
} from '../../libs/errors'; } from '../../libs/errors';
import * as passwordUtils from '../../libs/password'; import * as passwordUtils from '../../libs/password';
import { sendTxn as sendTxnEmail } from '../../libs/email'; import { sendTxn as sendTxnEmail } from '../../libs/email';
import { validatePasswordResetCodeAndFindUser, convertToBcrypt } from '../../libs/password';
import { encrypt } from '../../libs/encryption'; import { encrypt } from '../../libs/encryption';
import { import {
loginRes, loginRes,
@@ -125,7 +124,7 @@ api.loginLocal = {
headers: req.headers, headers: req.headers,
}); });
return loginRes(user, ...arguments); return loginRes(user, req, res);
}, },
}; };
@@ -137,7 +136,7 @@ api.loginSocial = {
})], })],
url: '/user/auth/social', url: '/user/auth/social',
async handler (req, res) { async handler (req, res) {
return await loginSocial(req, res); await loginSocial(req, res);
}, },
}; };
@@ -377,7 +376,7 @@ api.resetPasswordSetNewOne = {
method: 'POST', method: 'POST',
url: '/user/auth/reset-password-set-new-one', url: '/user/auth/reset-password-set-new-one',
async handler (req, res) { async handler (req, res) {
const user = await validatePasswordResetCodeAndFindUser(req.body.code); const user = await passwordUtils.validatePasswordResetCodeAndFindUser(req.body.code);
const isValidCode = Boolean(user); const isValidCode = Boolean(user);
if (!isValidCode) throw new NotAuthorized(res.t('invalidPasswordResetCode')); if (!isValidCode) throw new NotAuthorized(res.t('invalidPasswordResetCode'));
@@ -395,7 +394,7 @@ api.resetPasswordSetNewOne = {
} }
// set new password and make sure it's using bcrypt for hashing // set new password and make sure it's using bcrypt for hashing
await convertToBcrypt(user, String(newPassword)); await passwordUtils.convertToBcrypt(user, String(newPassword));
user.auth.local.passwordResetCode = undefined; // Reset saved password reset code user.auth.local.passwordResetCode = undefined; // Reset saved password reset code
await user.save(); await user.save();
@@ -418,7 +417,8 @@ api.deleteSocial = {
async handler (req, res) { async handler (req, res) {
const { user } = res.locals; const { user } = res.locals;
const { network } = req.params; const { network } = req.params;
const isSupportedNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS.find(supportedNetwork => supportedNetwork.key === network); const isSupportedNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS
.find(supportedNetwork => supportedNetwork.key === network);
if (!isSupportedNetwork) throw new BadRequest(res.t('unsupportedNetwork')); if (!isSupportedNetwork) throw new BadRequest(res.t('unsupportedNetwork'));
if (!hasBackupAuth(user, network)) throw new NotAuthorized(res.t('cantDetachSocial')); if (!hasBackupAuth(user, network)) throw new NotAuthorized(res.t('cantDetachSocial'));
const unset = { const unset = {

View File

@@ -413,8 +413,12 @@ api.getUserChallenges = {
User.findById(chal.leader).select(`${nameFields} backer contributor`).exec(), User.findById(chal.leader).select(`${nameFields} backer contributor`).exec(),
Group.findById(chal.group).select(basicGroupFields).exec(), Group.findById(chal.group).select(basicGroupFields).exec(),
]).then(populatedData => { ]).then(populatedData => {
resChals[index].leader = populatedData[0] ? populatedData[0].toJSON({ minimize: true }) : null; resChals[index].leader = populatedData[0]
resChals[index].group = populatedData[1] ? populatedData[1].toJSON({ minimize: true }) : null; ? populatedData[0].toJSON({ minimize: true })
: null;
resChals[index].group = populatedData[1]
? populatedData[1].toJSON({ minimize: true })
: null;
}))); })));
res.respond(200, resChals); res.respond(200, resChals);
@@ -460,12 +464,17 @@ api.getGroupChallenges = {
const challenges = await Challenge.find({ group: groupId }) const challenges = await Challenge.find({ group: groupId })
.sort('-createdAt') .sort('-createdAt')
// .populate('leader', nameFields) // Only populate the leader as the group is implicit // see below why we're not using populate // Only populate the leader as the group is implicit // see below why we're not using populate
// .populate('leader', nameFields)
.exec(); .exec();
let resChals = challenges.map(challenge => challenge.toJSON()); let resChals = challenges.map(challenge => challenge.toJSON());
resChals = _.orderBy(resChals, [challenge => challenge.categories.map(category => category.slug).includes('habitica_official')], ['desc']); resChals = _.orderBy(
resChals,
[challenge => challenge.categories.map(category => category.slug).includes('habitica_official')],
['desc'],
);
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833 // Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
await Promise.all(resChals.map((chal, index) => User await Promise.all(resChals.map((chal, index) => User
@@ -473,7 +482,9 @@ api.getGroupChallenges = {
.select(nameFields) .select(nameFields)
.exec() .exec()
.then(populatedLeader => { .then(populatedLeader => {
resChals[index].leader = populatedLeader ? populatedLeader.toJSON({ minimize: true }) : null; resChals[index].leader = populatedLeader
? populatedLeader.toJSON({ minimize: true })
: null;
}))); })));
res.respond(200, resChals); res.respond(200, resChals);
@@ -559,7 +570,8 @@ api.exportChallengeCsv = {
}); });
if (!group || !challenge.canView(user, group)) throw new NotFound(res.t('challengeNotFound')); if (!group || !challenge.canView(user, group)) throw new NotFound(res.t('challengeNotFound'));
// In v2 this used the aggregation framework to run some computation on MongoDB but then iterated through all // In v2 this used the aggregation framework to run some
// computation on MongoDB but then iterated through all
// results on the server so the perf difference isn't that big (hopefully) // results on the server so the perf difference isn't that big (hopefully)
const [members, tasks] = await Promise.all([ const [members, tasks] = await Promise.all([
@@ -578,7 +590,8 @@ api.exportChallengeCsv = {
.exec(), .exec(),
]); ]);
let resArray = members.map(member => [member._id, member.profile.name, member.auth.local.username]); let resArray = members
.map(member => [member._id, member.profile.name, member.auth.local.username]);
let lastUserId; let lastUserId;
let index = -1; let index = -1;
@@ -594,8 +607,8 @@ api.exportChallengeCsv = {
return; return;
} }
while (task.userId !== lastUserId) { while (task.userId !== lastUserId) {
index++; index += 1;
lastUserId = resArray[index][0]; // resArray[index][0] is an user id lastUserId = [resArray[index]]; // resArray[index][0] is an user id
} }
const streak = task.streak || 0; const streak = task.streak || 0;
@@ -603,8 +616,12 @@ api.exportChallengeCsv = {
resArray[index].push(`${task.type}:${task.text}`, task.value, task.notes, streak); resArray[index].push(`${task.type}:${task.text}`, task.value, task.notes, streak);
}); });
// The first row is going to be UUID name Task Value Notes repeated n times for the n challenge tasks // The first row is going to be UUID name Task Value Notes
const challengeTasks = _.reduce(challenge.tasksOrder.toObject(), (result, array) => result.concat(array), []).sort(); // repeated n times for the n challenge tasks
const challengeTasks = _.reduce(
challenge.tasksOrder.toObject(),
(result, array) => result.concat(array), [],
).sort();
resArray.unshift(['UUID', 'Display Name', 'Username']); resArray.unshift(['UUID', 'Display Name', 'Username']);
_.times(challengeTasks.length, () => resArray[0].push('Task', 'Value', 'Notes', 'Streak')); _.times(challengeTasks.length, () => resArray[0].push('Task', 'Value', 'Notes', 'Streak'));

View File

@@ -110,7 +110,6 @@ api.postChat = {
async handler (req, res) { async handler (req, res) {
const { user } = res.locals; const { user } = res.locals;
const { groupId } = req.params; const { groupId } = req.params;
let chatUpdated;
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty(); req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
req.sanitize('message').trim(); req.sanitize('message').trim();
@@ -165,7 +164,8 @@ api.postChat = {
throw new NotAuthorized(res.t('chatPrivilegesRevoked')); throw new NotAuthorized(res.t('chatPrivilegesRevoked'));
} }
// prevent banned words being posted, except in private guilds/parties and in certain public guilds with specific topics // prevent banned words being posted, except in private guilds/parties
// and in certain public guilds with specific topics
if (group.privacy === 'public' && !guildsAllowingBannedWords[group._id]) { if (group.privacy === 'public' && !guildsAllowingBannedWords[group._id]) {
const matchedBadWords = getBannedWordsFromText(req.body.message); const matchedBadWords = getBannedWordsFromText(req.body.message);
if (matchedBadWords.length > 0) { if (matchedBadWords.length > 0) {
@@ -175,7 +175,9 @@ api.postChat = {
const chatRes = await Group.toJSONCleanChat(group, user); const chatRes = await Group.toJSONCleanChat(group, user);
const lastClientMsg = req.query.previousMsg; const lastClientMsg = req.query.previousMsg;
chatUpdated = !!(lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg); const chatUpdated = !!(
lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg
);
if (group.checkChatSpam(user)) { if (group.checkChatSpam(user)) {
throw new NotAuthorized(res.t('messageGroupChatSpam')); throw new NotAuthorized(res.t('messageGroupChatSpam'));
@@ -449,7 +451,8 @@ api.seenChat = {
const validationErrors = req.validationErrors(); const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors; if (validationErrors) throw validationErrors;
// Do not validate group existence, it doesn't really matter and make it works if the group gets deleted // Do not validate group existence,
// it doesn't really matter and make it works if the group gets deleted
// let group = await Group.getGroup({user, groupId}); // let group = await Group.getGroup({user, groupId});
// if (!group) throw new NotFound(res.t('groupNotFound')); // if (!group) throw new NotFound(res.t('groupNotFound'));
@@ -476,7 +479,7 @@ api.seenChat = {
// Update the user version field manually, // Update the user version field manually,
// it cannot be updated in the pre update hook // it cannot be updated in the pre update hook
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info // See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
user._v++; user._v += 1;
await User.update({ _id: user._id }, update).exec(); await User.update({ _id: user._id }, update).exec();
res.respond(200, {}); res.respond(200, {});
@@ -529,7 +532,9 @@ api.deleteChat = {
const chatRes = await Group.toJSONCleanChat(group, user); const chatRes = await Group.toJSONCleanChat(group, user);
const lastClientMsg = req.query.previousMsg; const lastClientMsg = req.query.previousMsg;
const chatUpdated = !!(lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg); const chatUpdated = !!(
lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg
);
await Chat.remove({ _id: message._id }).exec(); await Chat.remove({ _id: message._id }).exec();

View File

@@ -18,13 +18,17 @@ const api = {};
function walkContent (obj, lang) { function walkContent (obj, lang) {
_.each(obj, (item, key, source) => { _.each(obj, (item, key, source) => {
if (_.isPlainObject(item) || _.isArray(item)) return walkContent(item, lang); if (_.isPlainObject(item) || _.isArray(item)) {
if (_.isFunction(item) && item.i18nLangFunc) source[key] = item(lang); walkContent(item, lang);
} else if (_.isFunction(item) && item.i18nLangFunc) {
source[key] = item(lang);
}
}); });
} }
// After the getContent route is called the first time for a certain language // After the getContent route is called the first time for a certain language
// the response is saved on disk and subsequentially served directly from there to reduce computation. // the response is saved on disk and subsequentially served
// directly from there to reduce computation.
// Example: if `cachedContentResponses.en` is true it means that the response is cached // Example: if `cachedContentResponses.en` is true it means that the response is cached
const cachedContentResponses = {}; const cachedContentResponses = {};
@@ -43,18 +47,21 @@ async function saveContentToDisk (language, content) {
try { try {
cacheBeingWritten[language] = true; cacheBeingWritten[language] = true;
await fs.stat(CONTENT_CACHE_PATH); // check if the directory exists, if it doesn't an error is thrown // check if the directory exists, if it doesn't an error is thrown
await fs.stat(CONTENT_CACHE_PATH);
await fs.writeFile(`${CONTENT_CACHE_PATH}${language}.json`, content, 'utf8'); await fs.writeFile(`${CONTENT_CACHE_PATH}${language}.json`, content, 'utf8');
cacheBeingWritten[language] = false; cacheBeingWritten[language] = false;
cachedContentResponses[language] = true; cachedContentResponses[language] = true;
} catch (err) { } catch (err) {
if (err.code === 'ENOENT' && err.syscall === 'stat') { // the directory doesn't exists, create it and retry // the directory doesn't exists, create it and retry
if (err.code === 'ENOENT' && err.syscall === 'stat') {
await fs.mkdir(CONTENT_CACHE_PATH); await fs.mkdir(CONTENT_CACHE_PATH);
return saveContentToDisk(language, content); saveContentToDisk(language, content);
} else {
cacheBeingWritten[language] = false;
logger.error(err);
} }
cacheBeingWritten[language] = false;
logger.error(err);
} }
} }

View File

@@ -25,7 +25,6 @@ import common from '../../../common';
import payments from '../../libs/payments/payments'; import payments from '../../libs/payments/payments';
import stripePayments from '../../libs/payments/stripe'; import stripePayments from '../../libs/payments/stripe';
import amzLib from '../../libs/payments/amazon'; import amzLib from '../../libs/payments/amazon';
import shared from '../../../common';
import apiError from '../../libs/apiError'; import apiError from '../../libs/apiError';
const MAX_EMAIL_INVITES_BY_USER = 200; const MAX_EMAIL_INVITES_BY_USER = 200;
@@ -122,7 +121,7 @@ api.createGroup = {
group.balance = 1; group.balance = 1;
user.balance--; user.balance -= 1;
user.guilds.push(group._id); user.guilds.push(group._id);
if (!user.achievements.joinedGuild) { if (!user.achievements.joinedGuild) {
user.achievements.joinedGuild = true; user.achievements.joinedGuild = true;
@@ -139,7 +138,8 @@ api.createGroup = {
const savedGroup = results[1]; const savedGroup = results[1];
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833 // Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
// await Q.ninvoke(savedGroup, 'populate', ['leader', nameFields]); // doc.populate doesn't return a promise // await Q.ninvoke(savedGroup, 'populate', ['leader', nameFields]);
// doc.populate doesn't return a promise
const response = savedGroup.toJSON(); const response = savedGroup.toJSON();
// the leader is the authenticated user // the leader is the authenticated user
response.leader = { response.leader = {
@@ -210,7 +210,7 @@ api.createGroupPlan = {
if (req.body.paymentType === 'Stripe') { if (req.body.paymentType === 'Stripe') {
const token = req.body.id; const token = req.body.id;
const gift = req.query.gift ? JSON.parse(req.query.gift) : undefined; const gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
const sub = req.query.sub ? shared.content.subscriptionBlocks[req.query.sub] : false; const sub = req.query.sub ? common.content.subscriptionBlocks[req.query.sub] : false;
const groupId = savedGroup._id; const groupId = savedGroup._id;
const { email } = req.body; const { email } = req.body;
const { headers } = req; const { headers } = req;
@@ -228,7 +228,9 @@ api.createGroupPlan = {
}); });
} else if (req.body.paymentType === 'Amazon') { } else if (req.body.paymentType === 'Amazon') {
const { billingAgreementId } = req.body; const { billingAgreementId } = req.body;
const sub = req.body.subscription ? shared.content.subscriptionBlocks[req.body.subscription] : false; const sub = req.body.subscription
? common.content.subscriptionBlocks[req.body.subscription]
: false;
const { coupon } = req.body; const { coupon } = req.body;
const groupId = savedGroup._id; const groupId = savedGroup._id;
const { headers } = req; const { headers } = req;
@@ -244,7 +246,8 @@ api.createGroupPlan = {
} }
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833 // Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
// await Q.ninvoke(savedGroup, 'populate', ['leader', nameFields]); // doc.populate doesn't return a promise // await Q.ninvoke(savedGroup, 'populate', ['leader', nameFields]);
// doc.populate doesn't return a promise
const response = savedGroup.toJSON(); const response = savedGroup.toJSON();
// the leader is the authenticated user // the leader is the authenticated user
response.leader = { response.leader = {
@@ -514,7 +517,9 @@ api.joinGroup = {
if (validationErrors) throw validationErrors; if (validationErrors) throw validationErrors;
// Works even if the user is not yet a member of the group // Works even if the user is not yet a member of the group
const group = await Group.getGroup({ user, groupId: req.params.groupId, optionalMembership: true }); // Do not fetch chat and work even if the user is not yet a member of the group // Do not fetch chat and work even if the user is not yet a member of the group
const group = await Group
.getGroup({ user, groupId: req.params.groupId, optionalMembership: true });
if (!group) throw new NotFound(res.t('groupNotFound')); if (!group) throw new NotFound(res.t('groupNotFound'));
let isUserInvited = false; let isUserInvited = false;
@@ -565,7 +570,8 @@ api.joinGroup = {
} }
if (isUserInvited && group.type === 'guild') { if (isUserInvited && group.type === 'guild') {
if (user.guilds.indexOf(group._id) !== -1) { // if user is already a member (party is checked previously) // if user is already a member (party is checked previously)
if (user.guilds.indexOf(group._id) !== -1) {
throw new NotAuthorized(res.t('youAreAlreadyInGroup')); throw new NotAuthorized(res.t('youAreAlreadyInGroup'));
} }
user.guilds.push(group._id); // Add group to user's guilds user.guilds.push(group._id); // Add group to user's guilds
@@ -577,7 +583,9 @@ api.joinGroup = {
if (!isUserInvited) throw new NotAuthorized(res.t('messageGroupRequiresInvite')); if (!isUserInvited) throw new NotAuthorized(res.t('messageGroupRequiresInvite'));
// @TODO: Review the need for this and if still needed, don't base this on memberCount // @TODO: Review the need for this and if still needed, don't base this on memberCount
if (!group.hasNotCancelled() && group.memberCount === 0) group.leader = user._id; // If new user is only member -> set as leader if (!group.hasNotCancelled() && group.memberCount === 0) {
group.leader = user._id; // If new user is only member -> set as leader
}
group.memberCount += 1; group.memberCount += 1;
@@ -600,7 +608,7 @@ api.joinGroup = {
if (!inviter.items.quests.basilist) { if (!inviter.items.quests.basilist) {
inviter.items.quests.basilist = 0; inviter.items.quests.basilist = 0;
} }
inviter.items.quests.basilist++; inviter.items.quests.basilist += 1;
inviter.markModified('items.quests'); inviter.markModified('items.quests');
} }
promises.push(inviter.save()); promises.push(inviter.save());
@@ -686,7 +694,9 @@ api.rejectGroupInvite = {
const hasPartyInvitation = removeFromArray(user.invitations.parties, { id: groupId }); const hasPartyInvitation = removeFromArray(user.invitations.parties, { id: groupId });
if (hasPartyInvitation) { if (hasPartyInvitation) {
user.invitations.party = user.invitations.parties.length > 0 ? user.invitations.parties[user.invitations.parties.length - 1] : {}; user.invitations.party = user.invitations.parties.length > 0
? user.invitations.parties[user.invitations.parties.length - 1]
: {};
user.markModified('invitations.party'); user.markModified('invitations.party');
isUserInvited = true; isUserInvited = true;
} else { } else {
@@ -772,7 +782,10 @@ api.leaveGroup = {
throw new NotAuthorized(res.t('questLeaderCannotLeaveGroup')); throw new NotAuthorized(res.t('questLeaderCannotLeaveGroup'));
} }
if (group.quest && group.quest.active && group.quest.members && group.quest.members[user._id]) { if (
group.quest && group.quest.active
&& group.quest.members && group.quest.members[user._id]
) {
throw new NotAuthorized(res.t('cannotLeaveWhileActiveQuest')); throw new NotAuthorized(res.t('cannotLeaveWhileActiveQuest'));
} }
} }
@@ -909,7 +922,9 @@ api.removeGroupMember = {
} }
if (isInvited === 'party') { if (isInvited === 'party') {
removeFromArray(member.invitations.parties, { id: group._id }); removeFromArray(member.invitations.parties, { id: group._id });
member.invitations.party = member.invitations.parties.length > 0 ? member.invitations.parties[member.invitations.parties.length - 1] : {}; member.invitations.party = member.invitations.parties.length > 0
? member.invitations.parties[member.invitations.parties.length - 1]
: {};
member.markModified('invitations.party'); member.markModified('invitations.party');
} }
} else { } else {
@@ -1064,7 +1079,8 @@ api.inviteToGroup = {
} }
if (usernames) { if (usernames) {
const usernameInvites = usernames.map(username => inviteByUserName(username, group, user, req, res)); const usernameInvites = usernames
.map(username => inviteByUserName(username, group, user, req, res));
const usernameResults = await Promise.all(usernameInvites); const usernameResults = await Promise.all(usernameInvites);
results.push(...usernameResults); results.push(...usernameResults);
} }

View File

@@ -168,10 +168,9 @@ api.getHero = {
url: '/hall/heroes/:heroId', url: '/hall/heroes/:heroId',
middlewares: [authWithHeaders(), ensureAdmin], middlewares: [authWithHeaders(), ensureAdmin],
async handler (req, res) { async handler (req, res) {
let validationErrors;
req.checkParams('heroId', res.t('heroIdRequired')).notEmpty(); req.checkParams('heroId', res.t('heroIdRequired')).notEmpty();
validationErrors = req.validationErrors(); const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors; if (validationErrors) throw validationErrors;
const { heroId } = req.params; const { heroId } = req.params;
@@ -256,22 +255,26 @@ api.updateHero = {
if (updateData.balance) hero.balance = updateData.balance; if (updateData.balance) hero.balance = updateData.balance;
// give them gems if they got an higher level // give them gems if they got an higher level
let newTier = updateData.contributor && updateData.contributor.level; // tier = level in this context // tier = level in this context
const oldTier = hero.contributor && hero.contributor.level || 0; let newTier = updateData.contributor && updateData.contributor.level;
const oldTier = (hero.contributor && hero.contributor.level) || 0;
if (newTier > oldTier) { if (newTier > oldTier) {
hero.flags.contributor = true; hero.flags.contributor = true;
let tierDiff = newTier - oldTier; // can be 2+ tier increases at once let tierDiff = newTier - oldTier; // can be 2+ tier increases at once
while (tierDiff) { while (tierDiff) {
hero.balance += gemsPerTier[newTier] / 4; // balance is in $ hero.balance += gemsPerTier[newTier] / 4; // balance is in $
tierDiff--; tierDiff -= 1;
newTier--; // give them gems for the next tier down if they weren't aready that tier newTier -= 1; // give them gems for the next tier down if they weren't aready that tier
} }
hero.addNotification('NEW_CONTRIBUTOR_LEVEL'); hero.addNotification('NEW_CONTRIBUTOR_LEVEL');
} }
if (updateData.contributor) _.assign(hero.contributor, updateData.contributor); if (updateData.contributor) _.assign(hero.contributor, updateData.contributor);
if (updateData.purchased && updateData.purchased.ads) hero.purchased.ads = updateData.purchased.ads; if (updateData.purchased && updateData.purchased.ads) {
hero.purchased.ads = updateData.purchased.ads;
}
// give them the Dragon Hydra pet if they're above level 6 // give them the Dragon Hydra pet if they're above level 6
if (hero.contributor.level >= 6) { if (hero.contributor.level >= 6) {
@@ -279,7 +282,8 @@ api.updateHero = {
hero.markModified('items.pets'); hero.markModified('items.pets');
} }
if (updateData.itemPath && updateData.itemVal && validateItemPath(updateData.itemPath)) { if (updateData.itemPath && updateData.itemVal && validateItemPath(updateData.itemPath)) {
_.set(hero, updateData.itemPath, castItemVal(updateData.itemPath, updateData.itemVal)); // Sanitization at 5c30944 (deemed unnecessary) // Sanitization at 5c30944 (deemed unnecessary)
_.set(hero, updateData.itemPath, castItemVal(updateData.itemPath, updateData.itemVal));
} }
if (updateData.auth && updateData.auth.blocked === true) { if (updateData.auth && updateData.auth.blocked === true) {
@@ -290,8 +294,12 @@ api.updateHero = {
hero.auth.blocked = false; hero.auth.blocked = false;
} }
if (updateData.flags && _.isBoolean(updateData.flags.chatRevoked)) hero.flags.chatRevoked = updateData.flags.chatRevoked; if (updateData.flags && _.isBoolean(updateData.flags.chatRevoked)) {
if (updateData.flags && _.isBoolean(updateData.flags.chatShadowMuted)) hero.flags.chatShadowMuted = updateData.flags.chatShadowMuted; hero.flags.chatRevoked = updateData.flags.chatRevoked;
}
if (updateData.flags && _.isBoolean(updateData.flags.chatShadowMuted)) {
hero.flags.chatShadowMuted = updateData.flags.chatShadowMuted;
}
const savedHero = await hero.save(); const savedHero = await hero.save();
const heroJSON = savedHero.toJSON(); const heroJSON = savedHero.toJSON();

View File

@@ -259,7 +259,8 @@ api.getMemberAchievements = {
// Return a request handler for getMembersForGroup / getInvitesForGroup / getMembersForChallenge // Return a request handler for getMembersForGroup / getInvitesForGroup / getMembersForChallenge
// @TODO: This violates the Liskov substitution principle. We should create factory functions. See Webhooks for a good example // @TODO: This violates the Liskov substitution principle.
// We should create factory functions. See Webhooks for a good example
function _getMembersForItem (type) { function _getMembersForItem (type) {
// check for allowed `type` // check for allowed `type`
if (['group-members', 'group-invites', 'challenge-members'].indexOf(type) === -1) { if (['group-members', 'group-invites', 'challenge-members'].indexOf(type) === -1) {
@@ -288,7 +289,8 @@ function _getMembersForItem (type) {
challenge = await Challenge.findById(challengeId).select('_id type leader group').exec(); challenge = await Challenge.findById(challengeId).select('_id type leader group').exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound')); if (!challenge) throw new NotFound(res.t('challengeNotFound'));
// optionalMembership is set to true because even if you're not member of the group you may be able to access the challenge // optionalMembership is set to true because even
// if you're not member of the group you may be able to access the challenge
// for example if you've been booted from it, are the leader or a site admin // for example if you've been booted from it, are the leader or a site admin
group = await Group.getGroup({ group = await Group.getGroup({
user, user,
@@ -305,7 +307,8 @@ function _getMembersForItem (type) {
const query = {}; const query = {};
let fields = nameFields; let fields = nameFields;
let addComputedStats = false; // add computes stats to the member info when items and stats are available // add computes stats to the member info when items and stats are available
let addComputedStats = false;
if (type === 'challenge-members') { if (type === 'challenge-members') {
query.challenges = challenge._id; query.challenges = challenge._id;
@@ -349,7 +352,8 @@ function _getMembersForItem (type) {
} }
} else { } else {
query['invitations.party.id'] = group._id; // group._id and not groupId because groupId could be === 'party' query['invitations.party.id'] = group._id; // group._id and not groupId because groupId could be === 'party'
// @TODO invitations are now stored like this: `'invitations.parties': []` Probably need a database index for it. // @TODO invitations are now stored like this: `'invitations.parties': []`
// Probably need a database index for it.
if (req.query.includeAllPublicFields === 'true') { if (req.query.includeAllPublicFields === 'true') {
fields = memberFields; fields = memberFields;
addComputedStats = true; addComputedStats = true;
@@ -554,7 +558,8 @@ api.getChallengeMemberProgress = {
const challenge = await Challenge.findById(challengeId).exec(); const challenge = await Challenge.findById(challengeId).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound')); if (!challenge) throw new NotFound(res.t('challengeNotFound'));
// optionalMembership is set to true because even if you're not member of the group you may be able to access the challenge // optionalMembership is set to true because even if you're
// not member of the group you may be able to access the challenge
// for example if you've been booted from it, are the leader or a site admin // for example if you've been booted from it, are the leader or a site admin
const group = await Group.getGroup({ const group = await Group.getGroup({
user, groupId: challenge.group, fields: '_id type privacy', optionalMembership: true, user, groupId: challenge.group, fields: '_id type privacy', optionalMembership: true,

View File

@@ -2,7 +2,8 @@ import { authWithHeaders } from '../../middlewares/auth';
const api = {}; const api = {};
// @TODO export this const, cannot export it from here because only routes are exported from controllers // @TODO export this const, cannot export it
// from here because only routes are exported from controllers
const LAST_ANNOUNCEMENT_TITLE = 'SUPERNATURAL SKINS AND HAUNTED HAIR COLORS'; const LAST_ANNOUNCEMENT_TITLE = 'SUPERNATURAL SKINS AND HAUNTED HAIR COLORS';
const worldDmg = { // @TODO const worldDmg = { // @TODO
bailey: false, bailey: false,

View File

@@ -43,7 +43,7 @@ api.readNotification = {
// Update the user version field manually, // Update the user version field manually,
// it cannot be updated in the pre update hook // it cannot be updated in the pre update hook
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info // See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
user._v++; user._v += 1;
await user.update({ await user.update({
$pull: { notifications: { id: req.params.notificationId } }, $pull: { notifications: { id: req.params.notificationId } },
@@ -90,7 +90,7 @@ api.readNotifications = {
// Update the user version field manually, // Update the user version field manually,
// it cannot be updated in the pre update hook // it cannot be updated in the pre update hook
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info // See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
user._v++; user._v += 1;
res.respond(200, UserNotification.convertNotificationsToSafeJson(user.notifications)); res.respond(200, UserNotification.convertNotificationsToSafeJson(user.notifications));
}, },
@@ -140,7 +140,7 @@ api.seeNotification = {
// Update the user version field manually, // Update the user version field manually,
// it cannot be updated in the pre update hook // it cannot be updated in the pre update hook
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info // See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
user._v++; user._v += 1;
res.respond(200, notification); res.respond(200, notification);
}, },

View File

@@ -305,7 +305,9 @@ api.forceStart = {
if (group.type !== 'party') throw new NotAuthorized(res.t('guildQuestsNotSupported')); if (group.type !== 'party') throw new NotAuthorized(res.t('guildQuestsNotSupported'));
if (!group.quest.key) throw new NotFound(res.t('questNotPending')); if (!group.quest.key) throw new NotFound(res.t('questNotPending'));
if (group.quest.active) throw new NotAuthorized(res.t('questAlreadyUnderway')); if (group.quest.active) throw new NotAuthorized(res.t('questAlreadyUnderway'));
if (!(user._id === group.quest.leader || user._id === group.leader)) throw new NotAuthorized(res.t('questOrGroupLeaderOnlyStartQuest')); if (!(user._id === group.quest.leader || user._id === group.leader)) {
throw new NotAuthorized(res.t('questOrGroupLeaderOnlyStartQuest'));
}
group.markModified('quest'); group.markModified('quest');
@@ -352,7 +354,8 @@ api.cancelQuest = {
async handler (req, res) { async handler (req, res) {
// Cancel a quest BEFORE it has begun (i.e., in the invitation stage) // Cancel a quest BEFORE it has begun (i.e., in the invitation stage)
// Quest scroll has not yet left quest owner's inventory so no need to return it. // Quest scroll has not yet left quest owner's inventory so no need to return it.
// Do not wipe quest progress for members because they'll want it to be applied to the next quest that's started. // Do not wipe quest progress for members because they'll
// want it to be applied to the next quest that's started.
const { user } = res.locals; const { user } = res.locals;
const { groupId } = req.params; const { groupId } = req.params;
@@ -366,7 +369,9 @@ api.cancelQuest = {
if (!group) throw new NotFound(res.t('groupNotFound')); if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.type !== 'party') throw new NotAuthorized(res.t('guildQuestsNotSupported')); if (group.type !== 'party') throw new NotAuthorized(res.t('guildQuestsNotSupported'));
if (!group.quest.key) throw new NotFound(res.t('questInvitationDoesNotExist')); if (!group.quest.key) throw new NotFound(res.t('questInvitationDoesNotExist'));
if (user._id !== group.leader && group.quest.leader !== user._id) throw new NotAuthorized(res.t('onlyLeaderCancelQuest')); if (user._id !== group.leader && group.quest.leader !== user._id) {
throw new NotAuthorized(res.t('onlyLeaderCancelQuest'));
}
if (group.quest.active) throw new NotAuthorized(res.t('cantCancelActiveQuest')); if (group.quest.active) throw new NotAuthorized(res.t('cantCancelActiveQuest'));
const questName = questScrolls[group.quest.key].text('en'); const questName = questScrolls[group.quest.key].text('en');

View File

@@ -227,7 +227,7 @@ api.deleteTag = {
// Update the user version field manually, // Update the user version field manually,
// it cannot be updated in the pre update hook // it cannot be updated in the pre update hook
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info // See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
user._v++; user._v += 1;
// Remove from all the tasks TODO test // Remove from all the tasks TODO test
await Tasks.Task.update({ await Tasks.Task.update({

View File

@@ -383,12 +383,23 @@ api.getTask = {
if (!task) { if (!task) {
throw new NotFound(res.t('taskNotFound')); throw new NotFound(res.t('taskNotFound'));
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
// If the task belongs to a challenge make sure the user has rights
} else if (task.challenge.id && !task.userId) {
const challenge = await Challenge.find({ _id: task.challenge.id }).select('leader').exec(); const challenge = await Challenge.find({ _id: task.challenge.id }).select('leader').exec();
if (!challenge || (user.challenges.indexOf(task.challenge.id) === -1 && challenge.leader !== user._id && !user.contributor.admin)) { // eslint-disable-line no-extra-parens if (
!challenge
|| (
user.challenges.indexOf(task.challenge.id) === -1
&& challenge.leader !== user._id
&& !user.contributor.admin
)
) { // eslint-disable-line no-extra-parens
throw new NotFound(res.t('taskNotFound')); throw new NotFound(res.t('taskNotFound'));
} }
} else if (task.userId !== user._id) { // If the task is owned by a user make it's the current one
// If the task is owned by a user make it's the current one
} else if (task.userId !== user._id) {
throw new NotFound(res.t('taskNotFound')); throw new NotFound(res.t('taskNotFound'));
} }
@@ -451,16 +462,21 @@ api.updateTask = {
group = await Group.getGroup({ user, groupId: task.group.id, fields }); group = await Group.getGroup({ user, groupId: task.group.id, fields });
if (!group) throw new NotFound(res.t('groupNotFound')); if (!group) throw new NotFound(res.t('groupNotFound'));
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
// If the task belongs to a challenge make sure the user has rights
} else if (task.challenge.id && !task.userId) {
challenge = await Challenge.findOne({ _id: task.challenge.id }).exec(); challenge = await Challenge.findOne({ _id: task.challenge.id }).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound')); if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks')); if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
} else if (task.userId !== user._id) { // If the task is owned by a user make it's the current one
// If the task is owned by a user make it's the current one
} else if (task.userId !== user._id) {
throw new NotFound(res.t('taskNotFound')); throw new NotFound(res.t('taskNotFound'));
} }
const oldCheckList = task.checklist; const oldCheckList = task.checklist;
// we have to convert task to an object because otherwise things don't get merged correctly. Bad for performances? // we have to convert task to an object because otherwise things
// don't get merged correctly. Bad for performances?
const [updatedTaskObj] = common.ops.updateTask(task.toObject(), req); const [updatedTaskObj] = common.ops.updateTask(task.toObject(), req);
// Sanitize differently user tasks linked to a challenge // Sanitize differently user tasks linked to a challenge
let sanitizedObj; let sanitizedObj;
@@ -476,7 +492,8 @@ api.updateTask = {
_.assign(task, sanitizedObj); _.assign(task, sanitizedObj);
// console.log(task.modifiedPaths(), task.toObject().repeat === tep) // console.log(task.modifiedPaths(), task.toObject().repeat === tep)
// repeat is always among modifiedPaths because mongoose changes the other of the keys when using .toObject() // repeat is always among modifiedPaths because mongoose changes
// the other of the keys when using .toObject()
// see https://github.com/Automattic/mongoose/issues/2749 // see https://github.com/Automattic/mongoose/issues/2749
task.group.approval.required = false; task.group.approval.required = false;
@@ -585,7 +602,8 @@ api.scoreTask = {
const managers = await User.find({ _id: managerIds }, 'notifications preferences').exec(); // Use this method so we can get access to notifications const managers = await User.find({ _id: managerIds }, 'notifications preferences').exec(); // Use this method so we can get access to notifications
// @TODO: we can use the User.pushNotification function because we need to ensure notifications are translated // @TODO: we can use the User.pushNotification function because
// we need to ensure notifications are translated
const managerPromises = []; const managerPromises = [];
managers.forEach(manager => { managers.forEach(manager => {
manager.addNotification('GROUP_TASK_APPROVAL', { manager.addNotification('GROUP_TASK_APPROVAL', {
@@ -594,7 +612,8 @@ api.scoreTask = {
taskName: task.text, taskName: task.text,
}, manager.preferences.language), }, manager.preferences.language),
groupId: group._id, groupId: group._id,
taskId: task._id, // user task id, used to match the notification when the task is approved // user task id, used to match the notification when the task is approved
taskId: task._id,
userId: user._id, userId: user._id,
groupTaskId: task.group.taskId, // the original task id groupTaskId: task.group.taskId, // the original task id
direction, direction,
@@ -612,7 +631,8 @@ api.scoreTask = {
const wasCompleted = task.completed; const wasCompleted = task.completed;
const [delta] = common.ops.scoreTask({ task, user, direction }, req); const [delta] = common.ops.scoreTask({ task, user, direction }, req);
// Drop system (don't run on the client, as it would only be discarded since ops are sent to the API, not the results) // Drop system (don't run on the client,
// as it would only be discarded since ops are sent to the API, not the results)
if (direction === 'up') common.fns.randomDrop(user, { task, delta }, req, res.analytics); if (direction === 'up') common.fns.randomDrop(user, { task, delta }, req, res.analytics);
// If a todo was completed or uncompleted move it in or out of the user.tasksOrder.todos list // If a todo was completed or uncompleted move it in or out of the user.tasksOrder.todos list
@@ -626,7 +646,11 @@ api.scoreTask = {
$pull: { 'tasksOrder.todos': task._id }, $pull: { 'tasksOrder.todos': task._id },
}).exec(); }).exec();
// user.tasksOrder.todos.pull(task._id); // user.tasksOrder.todos.pull(task._id);
} else if (wasCompleted && !task.completed && user.tasksOrder.todos.indexOf(task._id) === -1) { } else if (
wasCompleted
&& !task.completed
&& user.tasksOrder.todos.indexOf(task._id) === -1
) {
taskOrderPromise = user.update({ taskOrderPromise = user.update({
$push: { 'tasksOrder.todos': task._id }, $push: { 'tasksOrder.todos': task._id },
}).exec(); }).exec();
@@ -649,7 +673,9 @@ api.scoreTask = {
}).exec(); }).exec();
if (groupTask) { if (groupTask) {
const groupDelta = groupTask.group.assignedUsers ? delta / groupTask.group.assignedUsers.length : delta; const groupDelta = groupTask.group.assignedUsers
? delta / groupTask.group.assignedUsers.length
: delta;
await groupTask.scoreChallengeTask(groupDelta, direction); await groupTask.scoreChallengeTask(groupDelta, direction);
} }
} catch (e) { } catch (e) {
@@ -675,7 +701,8 @@ api.scoreTask = {
}); });
if (task.challenge && task.challenge.id && task.challenge.taskId && !task.challenge.broken && task.type !== 'reward') { if (task.challenge && task.challenge.id && task.challenge.taskId && !task.challenge.broken && task.type !== 'reward') {
// Wrapping everything in a try/catch block because if an error occurs using `await` it MUST NOT bubble up because the request has already been handled // Wrapping everything in a try/catch block because if an error occurs
// using `await` it MUST NOT bubble up because the request has already been handled
try { try {
const chalTask = await Tasks.Task.findOne({ const chalTask = await Tasks.Task.findOne({
_id: task.challenge.taskId, _id: task.challenge.taskId,
@@ -763,7 +790,7 @@ api.moveTask = {
// Update the user version field manually, // Update the user version field manually,
// it cannot be updated in the pre update hook // it cannot be updated in the pre update hook
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info // See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
user._v++; user._v += 1;
res.respond(200, order); res.respond(200, order);
}, },
@@ -811,11 +838,15 @@ api.addChecklistItem = {
const fields = requiredGroupFields.concat(' managers'); const fields = requiredGroupFields.concat(' managers');
group = await Group.getGroup({ user, groupId: task.group.id, fields }); group = await Group.getGroup({ user, groupId: task.group.id, fields });
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
// If the task belongs to a challenge make sure the user has rights
} else if (task.challenge.id && !task.userId) {
challenge = await Challenge.findOne({ _id: task.challenge.id }).exec(); challenge = await Challenge.findOne({ _id: task.challenge.id }).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound')); if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks')); if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
} else if (task.userId !== user._id) { // If the task is owned by a user make it's the current one
// If the task is owned by a user make it's the current one
} else if (task.userId !== user._id) {
throw new NotFound(res.t('taskNotFound')); throw new NotFound(res.t('taskNotFound'));
} }
@@ -927,11 +958,15 @@ api.updateChecklistItem = {
group = await Group.getGroup({ user, groupId: task.group.id, fields }); group = await Group.getGroup({ user, groupId: task.group.id, fields });
if (!group) throw new NotFound(res.t('groupNotFound')); if (!group) throw new NotFound(res.t('groupNotFound'));
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
// If the task belongs to a challenge make sure the user has rights
} else if (task.challenge.id && !task.userId) {
challenge = await Challenge.findOne({ _id: task.challenge.id }).exec(); challenge = await Challenge.findOne({ _id: task.challenge.id }).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound')); if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks')); if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
} else if (task.userId !== user._id) { // If the task is owned by a user make it's the current one
// If the task is owned by a user make it's the current one
} else if (task.userId !== user._id) {
throw new NotFound(res.t('taskNotFound')); throw new NotFound(res.t('taskNotFound'));
} }
if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo')); if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo'));
@@ -992,11 +1027,15 @@ api.removeChecklistItem = {
group = await Group.getGroup({ user, groupId: task.group.id, fields }); group = await Group.getGroup({ user, groupId: task.group.id, fields });
if (!group) throw new NotFound(res.t('groupNotFound')); if (!group) throw new NotFound(res.t('groupNotFound'));
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
// If the task belongs to a challenge make sure the user has rights
} else if (task.challenge.id && !task.userId) {
challenge = await Challenge.findOne({ _id: task.challenge.id }).exec(); challenge = await Challenge.findOne({ _id: task.challenge.id }).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound')); if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks')); if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
} else if (task.userId !== user._id) { // If the task is owned by a user make it's the current one
// If the task is owned by a user make it's the current one
} else if (task.userId !== user._id) {
throw new NotFound(res.t('taskNotFound')); throw new NotFound(res.t('taskNotFound'));
} }
if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo')); if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo'));
@@ -1310,15 +1349,23 @@ api.deleteTask = {
if (!group) throw new NotFound(res.t('groupNotFound')); if (!group) throw new NotFound(res.t('groupNotFound'));
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
await group.removeTask(task); await group.removeTask(task);
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
// If the task belongs to a challenge make sure the user has rights
} else if (task.challenge.id && !task.userId) {
challenge = await Challenge.findOne({ _id: task.challenge.id }).exec(); challenge = await Challenge.findOne({ _id: task.challenge.id }).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound')); if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks')); if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
} else if (task.userId !== user._id) { // If the task is owned by a user make it's the current one
// If the task is owned by a user make it's the current one
} else if (task.userId !== user._id) {
throw new NotFound(res.t('taskNotFound')); throw new NotFound(res.t('taskNotFound'));
} else if (task.userId && task.challenge.id && !task.challenge.broken) { } else if (task.userId && task.challenge.id && !task.challenge.broken) {
throw new NotAuthorized(res.t('cantDeleteChallengeTasks')); throw new NotAuthorized(res.t('cantDeleteChallengeTasks'));
} else if (task.group.id && task.group.assignedUsers.indexOf(user._id) !== -1 && !task.group.broken) { } else if (
task.group.id
&& task.group.assignedUsers.indexOf(user._id) !== -1
&& !task.group.broken
) {
throw new NotAuthorized(res.t('cantDeleteAssignedGroupTasks')); throw new NotAuthorized(res.t('cantDeleteAssignedGroupTasks'));
} }
@@ -1332,7 +1379,7 @@ api.deleteTask = {
// Update the user version field manually, // Update the user version field manually,
// it cannot be updated in the pre update hook // it cannot be updated in the pre update hook
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info // See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
if (!challenge) user._v++; if (!challenge) user._v += 1;
await Promise.all([taskOrderUpdate, task.remove()]); await Promise.all([taskOrderUpdate, task.remove()]);
} else { } else {

View File

@@ -18,7 +18,8 @@ import apiError from '../../../libs/apiError';
const requiredGroupFields = '_id leader tasksOrder name'; const requiredGroupFields = '_id leader tasksOrder name';
// @TODO: abstract to task lib // @TODO: abstract to task lib
const types = Tasks.tasksTypes.map(type => `${type}s`); const types = Tasks.tasksTypes.map(type => `${type}s`);
types.push('completedTodos', '_allCompletedTodos'); // _allCompletedTodos is currently in BETA and is likely to be removed in future // _allCompletedTodos is currently in BETA and is likely to be removed in future
types.push('completedTodos', '_allCompletedTodos');
function canNotEditTasks (group, user, assignedUserId) { function canNotEditTasks (group, user, assignedUserId) {
const isNotGroupLeader = group.leader !== user._id; const isNotGroupLeader = group.leader !== user._id;
@@ -96,7 +97,11 @@ api.getGroupTasks = {
const { user } = res.locals; const { user } = res.locals;
const group = await Group.getGroup({ user, groupId: req.params.groupId, fields: requiredGroupFields }); const group = await Group.getGroup({
user,
groupId: req.params.groupId,
fields: requiredGroupFields,
});
if (!group) throw new NotFound(res.t('groupNotFound')); if (!group) throw new NotFound(res.t('groupNotFound'));
const tasks = await getTasks(req, res, { user, group }); const tasks = await getTasks(req, res, { user, group });
@@ -142,7 +147,11 @@ api.groupMoveTask = {
if (task.type === 'todo' && task.completed) throw new BadRequest(res.t('cantMoveCompletedTodo')); if (task.type === 'todo' && task.completed) throw new BadRequest(res.t('cantMoveCompletedTodo'));
const group = await Group.getGroup({ user, groupId: task.group.id, fields: requiredGroupFields }); const group = await Group.getGroup({
user,
groupId: task.group.id,
fields: requiredGroupFields,
});
if (!group) throw new NotFound(res.t('groupNotFound')); if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));

View File

@@ -123,7 +123,10 @@ api.getBuyList = {
// return text and notes strings // return text and notes strings
_.each(list, item => { _.each(list, item => {
_.each(item, (itemPropVal, itemPropKey) => { _.each(item, (itemPropVal, itemPropKey) => {
if (_.isFunction(itemPropVal) && itemPropVal.i18nLangFunc) item[itemPropKey] = itemPropVal(req.language); if (
_.isFunction(itemPropVal)
&& itemPropVal.i18nLangFunc
) item[itemPropKey] = itemPropVal(req.language);
}); });
}); });
@@ -166,7 +169,10 @@ api.getInAppRewardsList = {
// return text and notes strings // return text and notes strings
_.each(list, item => { _.each(list, item => {
_.each(item, (itemPropVal, itemPropKey) => { _.each(item, (itemPropVal, itemPropKey) => {
if (_.isFunction(itemPropVal) && itemPropVal.i18nLangFunc) item[itemPropKey] = itemPropVal(req.language); if (
_.isFunction(itemPropVal)
&& itemPropVal.i18nLangFunc
) item[itemPropKey] = itemPropVal(req.language);
}); });
}); });
@@ -455,7 +461,6 @@ api.buy = {
async handler (req, res) { async handler (req, res) {
const { user } = res.locals; const { user } = res.locals;
let buyRes;
// @TODO: Remove this when mobile passes type in body // @TODO: Remove this when mobile passes type in body
const type = req.params.key; const type = req.params.key;
if (buySpecialKeys.indexOf(type) !== -1) { if (buySpecialKeys.indexOf(type) !== -1) {
@@ -470,7 +475,7 @@ api.buy = {
let quantity = 1; let quantity = 1;
if (req.body.quantity) quantity = req.body.quantity; if (req.body.quantity) quantity = req.body.quantity;
req.quantity = quantity; req.quantity = quantity;
buyRes = common.ops.buy(user, req, res.analytics); const buyRes = common.ops.buy(user, req, res.analytics);
await user.save(); await user.save();
res.respond(200, ...buyRes); res.respond(200, ...buyRes);
@@ -1003,7 +1008,12 @@ api.userPurchaseHourglass = {
const { user } = res.locals; const { user } = res.locals;
const quantity = req.body.quantity || 1; const quantity = req.body.quantity || 1;
if (quantity < 1 || !Number.isInteger(quantity)) throw new BadRequest(res.t('invalidQuantity'), req.language); if (quantity < 1 || !Number.isInteger(quantity)) throw new BadRequest(res.t('invalidQuantity'), req.language);
const purchaseHourglassRes = common.ops.buy(user, req, res.analytics, { quantity, hourglass: true }); const purchaseHourglassRes = common.ops.buy(
user,
req,
res.analytics,
{ quantity, hourglass: true },
);
await user.save(); await user.save();
res.respond(200, ...purchaseHourglassRes); res.respond(200, ...purchaseHourglassRes);
}, },

View File

@@ -285,7 +285,7 @@ api.exportUserAvatarPng = {
}); });
}); });
res.redirect(s3res.Location); return res.redirect(s3res.Location);
}, },
}; };
@@ -317,11 +317,11 @@ api.exportUserPrivateMessages = {
const messageUser = message.user; const messageUser = message.user;
const timestamp = moment.utc(message.timestamp).zone(timezoneOffset).format(`${dateFormat} HH:mm:ss`); const timestamp = moment.utc(message.timestamp).zone(timezoneOffset).format(`${dateFormat} HH:mm:ss`);
const text = md.render(message.text); const text = md.render(message.text);
index = `(${index + 1}/${inbox.length})`; const pageIndex = `(${index + 1}/${inbox.length})`;
messages += ` messages += `
<p> <p>
${recipientLabel} <strong>${messageUser}</strong> ${timestamp} ${recipientLabel} <strong>${messageUser}</strong> ${timestamp}
${index} ${pageIndex}
<br /> <br />
${text} ${text}
</p> </p>

View File

@@ -45,7 +45,8 @@ api.unsubscribe = {
res.send(`<h1>${res.t('unsubscribedSuccessfully')}</h1> ${res.t('unsubscribedTextUsers')}`); res.send(`<h1>${res.t('unsubscribedSuccessfully')}</h1> ${res.t('unsubscribedTextUsers')}`);
} else { } else {
const unsubscribedEmail = await EmailUnsubscription.findOne({ email: data.email.toLowerCase() }).exec(); const unsubscribedEmail = await EmailUnsubscription
.findOne({ email: data.email.toLowerCase() }).exec();
if (!unsubscribedEmail) await EmailUnsubscription.create({ email: data.email.toLowerCase() }); if (!unsubscribedEmail) await EmailUnsubscription.create({ email: data.email.toLowerCase() });
const okResponse = `<h1>${res.t('unsubscribedSuccessfully')}</h1> ${res.t('unsubscribedTextOthers')}`; const okResponse = `<h1>${res.t('unsubscribedSuccessfully')}</h1> ${res.t('unsubscribedTextOthers')}`;

View File

@@ -102,7 +102,9 @@ api.subscribe = {
middlewares: [authWithHeaders()], middlewares: [authWithHeaders()],
async handler (req, res) { async handler (req, res) {
const { billingAgreementId } = req.body; const { billingAgreementId } = req.body;
const sub = req.body.subscription ? shared.content.subscriptionBlocks[req.body.subscription] : false; const sub = req.body.subscription
? shared.content.subscriptionBlocks[req.body.subscription]
: false;
const { coupon } = req.body; const { coupon } = req.body;
const { user } = res.locals; const { user } = res.locals;
const { groupId } = req.body; const { groupId } = req.body;

View File

@@ -46,7 +46,13 @@ api.iapSubscriptionAndroid = {
middlewares: [authWithHeaders()], middlewares: [authWithHeaders()],
async handler (req, res) { async handler (req, res) {
if (!req.body.sku) throw new BadRequest(res.t('missingSubscriptionCode')); if (!req.body.sku) throw new BadRequest(res.t('missingSubscriptionCode'));
await googlePayments.subscribe(req.body.sku, res.locals.user, req.body.transaction.receipt, req.body.transaction.signature, req.headers); await googlePayments.subscribe(
req.body.sku,
res.locals.user,
req.body.transaction.receipt,
req.body.transaction.signature,
req.headers,
);
res.respond(200); res.respond(200);
}, },

View File

@@ -155,8 +155,9 @@ api.subscribeCancel = {
}, },
}; };
// General IPN handler. We catch cancelled Habitica subscriptions for users who manually cancel their // General IPN handler. We catch cancelled Habitica subscriptions
// recurring paypal payments in their paypal dashboard. TODO ? Remove this when we can move to webhooks or some other solution // for users who manually cancel their recurring paypal payments in their paypal dashboard.
// TODO ? Remove this when we can move to webhooks or some other solution
/** /**
* @apiIgnore Payments are considered part of the private API * @apiIgnore Payments are considered part of the private API

View File

@@ -4,7 +4,7 @@
// Register babel hook so we can write the real entry file (server.js) in ES6 // Register babel hook so we can write the real entry file (server.js) in ES6
// In production, the es6 code is pre-transpiled so it doesn't need it // In production, the es6 code is pre-transpiled so it doesn't need it
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
require('@babel/register'); require('@babel/register'); // eslint-disable-line import/no-extraneous-dependencies
} }
const cluster = require('cluster'); const cluster = require('cluster');

View File

@@ -15,7 +15,10 @@ const AMPLITUDE_TOKEN = nconf.get('AMPLITUDE_KEY');
const GA_TOKEN = nconf.get('GA_ID'); const GA_TOKEN = nconf.get('GA_ID');
const GA_POSSIBLE_LABELS = ['gaLabel', 'itemKey']; const GA_POSSIBLE_LABELS = ['gaLabel', 'itemKey'];
const GA_POSSIBLE_VALUES = ['gaValue', 'gemCost', 'goldCost']; const GA_POSSIBLE_VALUES = ['gaValue', 'gemCost', 'goldCost'];
const AMPLITUDE_PROPERTIES_TO_SCRUB = ['uuid', 'user', 'purchaseValue', 'gaLabel', 'gaValue', 'headers', 'registeredThrough']; const AMPLITUDE_PROPERTIES_TO_SCRUB = [
'uuid', 'user', 'purchaseValue',
'gaLabel', 'gaValue', 'headers', 'registeredThrough',
];
const PLATFORM_MAP = Object.freeze({ const PLATFORM_MAP = Object.freeze({
'habitica-web': 'Web', 'habitica-web': 'Web',
@@ -31,7 +34,7 @@ const ga = googleAnalytics(GA_TOKEN);
const Content = common.content; const Content = common.content;
function _lookUpItemName (itemKey) { function _lookUpItemName (itemKey) {
if (!itemKey) return; if (!itemKey) return null;
const gear = Content.gear.flat[itemKey]; const gear = Content.gear.flat[itemKey];
const egg = Content.eggs[itemKey]; const egg = Content.eggs[itemKey];
@@ -186,6 +189,8 @@ function _generateLabelForGoogleAnalytics (data) {
label = data[key]; label = data[key];
return false; // exit each early return false; // exit each early
} }
return true;
}); });
return label; return label;
@@ -199,6 +204,8 @@ function _generateValueForGoogleAnalytics (data) {
value = data[key]; value = data[key];
return false; // exit each early return false; // exit each early
} }
return true;
}); });
return value; return value;
@@ -225,7 +232,7 @@ function _sendDataToGoogle (eventType, data) {
const promise = new Promise((resolve, reject) => { const promise = new Promise((resolve, reject) => {
ga.event(eventData, err => { ga.event(eventData, err => {
if (err) return reject(err); if (err) return reject(err);
resolve(); return resolve();
}); });
}); });
@@ -264,7 +271,7 @@ function _sendPurchaseDataToGoogle (data) {
const eventPromise = new Promise((resolve, reject) => { const eventPromise = new Promise((resolve, reject) => {
ga.event(eventData, err => { ga.event(eventData, err => {
if (err) return reject(err); if (err) return reject(err);
resolve(); return resolve();
}); });
}); });
@@ -273,7 +280,7 @@ function _sendPurchaseDataToGoogle (data) {
.item(price, qty, sku, itemKey, variation) .item(price, qty, sku, itemKey, variation)
.send(err => { .send(err => {
if (err) return reject(err); if (err) return reject(err);
resolve(); return resolve();
}); });
}); });
@@ -308,7 +315,8 @@ async function track (eventType, data) {
return Promise.all(promises); return Promise.all(promises);
} }
// There's no error handling directly here because it's handled inside _sendPurchaseDataTo{Amplitude|Google} // There's no error handling directly here because
// it's handled inside _sendPurchaseDataTo{Amplitude|Google}
async function trackPurchase (data) { async function trackPurchase (data) {
return Promise.all([ return Promise.all([
_sendPurchaseDataToAmplitude(data), _sendPurchaseDataToAmplitude(data),

View File

@@ -12,19 +12,22 @@ import common from '../../../common';
import logger from '../logger'; import logger from '../logger';
import { decrypt } from '../encryption'; import { decrypt } from '../encryption';
import { model as Group } from '../../models/group'; import { model as Group } from '../../models/group';
import { loginSocial } from './social.js'; import { loginSocial } from './social';
import { loginRes } from './utils'; import { loginRes } from './utils';
import { verifyUsername } from '../user/validation'; import { verifyUsername } from '../user/validation';
const USERNAME_LENGTH_MIN = 1; const USERNAME_LENGTH_MIN = 1;
const USERNAME_LENGTH_MAX = 20; const USERNAME_LENGTH_MAX = 20;
// When the user signed up after having been invited to a group, invite them automatically to the group // When the user signed up after having been invited to a group,
// invite them automatically to the group
async function _handleGroupInvitation (user, invite) { async function _handleGroupInvitation (user, invite) {
// wrapping the code in a try because we don't want it to prevent the user from signing up // wrapping the code in a try because we don't want it to prevent the user from signing up
// that's why errors are not translated // that's why errors are not translated
try { try {
let { sentAt, id: groupId, inviter } = JSON.parse(decrypt(invite)); const decryptedInvite = JSON.parse(decrypt(invite));
let { inviter } = decryptedInvite;
const { sentAt, id: groupId } = decryptedInvite;
// check that the invite has not expired (after 7 days) // check that the invite has not expired (after 7 days)
if (sentAt && moment().subtract(7, 'days').isAfter(sentAt)) { if (sentAt && moment().subtract(7, 'days').isAfter(sentAt)) {
@@ -66,7 +69,8 @@ function hasBackupAuth (user, networkToRemove) {
return true; return true;
} }
const hasAlternateNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS.find(network => network.key !== networkToRemove && user.auth[network.key].id); const hasAlternateNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS
.find(network => network.key !== networkToRemove && user.auth[network.key].id);
return hasAlternateNetwork; return hasAlternateNetwork;
} }
@@ -100,10 +104,12 @@ async function registerLocal (req, res, { isV3 = false }) {
const issues = verifyUsername(req.body.username, res); const issues = verifyUsername(req.body.username, res);
if (issues.length > 0) throw new BadRequest(issues.join(' ')); if (issues.length > 0) throw new BadRequest(issues.join(' '));
let { email, username, password } = req.body; let { email, username } = req.body;
const { password } = req.body;
// Get the lowercase version of username to check that we do not have duplicates // Get the lowercase version of username to check that we do not have duplicates
// So we can search for it in the database and then reject the choosen username if 1 or more results are found // So we can search for it in the database and then reject the choosen
// username if 1 or more results are found
email = email.toLowerCase(); email = email.toLowerCase();
username = username.trim(); username = username.trim();
const lowerCaseUsername = username.toLowerCase(); const lowerCaseUsername = username.toLowerCase();
@@ -147,9 +153,10 @@ async function registerLocal (req, res, { isV3 = false }) {
if (existingUser) { if (existingUser) {
const hasSocialAuth = common.constants.SUPPORTED_SOCIAL_NETWORKS.find(network => { const hasSocialAuth = common.constants.SUPPORTED_SOCIAL_NETWORKS.find(network => {
if (existingUser.auth.hasOwnProperty(network.key)) { if (existingUser.auth.hasOwnProperty(network.key)) { // eslint-disable-line no-prototype-builtins, max-len
return existingUser.auth[network.key].id; return existingUser.auth[network.key].id;
} }
return false;
}); });
if (!hasSocialAuth) throw new NotAuthorized(res.t('onlySocialAttachLocal')); if (!hasSocialAuth) throw new NotAuthorized(res.t('onlySocialAttachLocal'));
existingUser.auth.local = newUser.auth.local; existingUser.auth.local = newUser.auth.local;

View File

@@ -21,12 +21,13 @@ function _passportProfile (network, accessToken) {
}); });
} }
export async function loginSocial (req, res) { export async function loginSocial (req, res) { // eslint-disable-line import/prefer-default-export
const existingUser = res.locals.user; const existingUser = res.locals.user;
const accessToken = req.body.authResponse.access_token; const accessToken = req.body.authResponse.access_token;
const { network } = req.body; const { network } = req.body;
const isSupportedNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS.find(supportedNetwork => supportedNetwork.key === network); const isSupportedNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS
.find(supportedNetwork => supportedNetwork.key === network);
if (!isSupportedNetwork) throw new BadRequest(res.t('unsupportedNetwork')); if (!isSupportedNetwork) throw new BadRequest(res.t('unsupportedNetwork'));
const profile = await _passportProfile(network, accessToken); const profile = await _passportProfile(network, accessToken);
@@ -37,7 +38,7 @@ export async function loginSocial (req, res) {
// User already signed up // User already signed up
if (user) { if (user) {
return loginRes(user, ...arguments); return loginRes(user, req, res);
} }
const generatedUsername = generateUsername(); const generatedUsername = generateUsername();
@@ -78,10 +79,14 @@ export async function loginSocial (req, res) {
user.newUser = true; user.newUser = true;
} }
loginRes(user, ...arguments); loginRes(user, req, res);
// Clean previous email preferences // Clean previous email preferences
if (savedUser.auth[network].emails && savedUser.auth[network].emails[0] && savedUser.auth[network].emails[0].value) { if (
savedUser.auth[network].emails
&& savedUser.auth[network].emails[0]
&& savedUser.auth[network].emails[0].value
) {
EmailUnsubscription EmailUnsubscription
.remove({ email: savedUser.auth[network].emails[0].value.toLowerCase() }) .remove({ email: savedUser.auth[network].emails[0].value.toLowerCase() })
.exec() .exec()

View File

@@ -12,7 +12,12 @@ export function generateUsername () {
} }
export function loginRes (user, req, res) { export function loginRes (user, req, res) {
if (user.auth.blocked) throw new NotAuthorized(res.t('accountSuspended', { communityManagerEmail: COMMUNITY_MANAGER_EMAIL, userId: user._id })); if (user.auth.blocked) {
throw new NotAuthorized(res.t(
'accountSuspended',
{ communityManagerEmail: COMMUNITY_MANAGER_EMAIL, userId: user._id },
));
}
const responseData = { const responseData = {
id: user._id, id: user._id,

View File

@@ -1,7 +1,7 @@
import AWS from 'aws-sdk'; import AWS from 'aws-sdk';
import nconf from 'nconf'; import nconf from 'nconf';
export const S3 = new AWS.S3({ export const S3 = new AWS.S3({ // eslint-disable-line import/prefer-default-export
accessKeyId: nconf.get('S3_ACCESS_KEY_ID'), accessKeyId: nconf.get('S3_ACCESS_KEY_ID'),
secretAccessKey: nconf.get('S3_SECRET_ACCESS_KEY'), secretAccessKey: nconf.get('S3_SECRET_ACCESS_KEY'),
}); });

View File

@@ -3,7 +3,8 @@
// CONTENT WARNING: // CONTENT WARNING:
// This file contains slurs on race, gender, sexual orientation, etc. // This file contains slurs on race, gender, sexual orientation, etc.
// Do not read this file if you do not want to be exposed to those words. // Do not read this file if you do not want to be exposed to those words.
// The words are stored in an array called `bannedSlurs` which is then exported with `module.exports = bannedSlurs;` // The words are stored in an array called `bannedSlurs`
// which is then exported with `module.exports = bannedSlurs;`
// This file does not contain any other code. // This file does not contain any other code.
// //
// //
@@ -76,11 +77,14 @@
// Some words that are slurs in English are not included here because they are valid words in other languages and so must not cause an automatic mute. // Some words that are slurs in English are not included here
// because they are valid words in other languages and so must not cause an automatic mute.
// See the comments in bannedWords.js for details. // See the comments in bannedWords.js for details.
// 'spic' should not be banned because it's often used in the phrase "spic and span" // 'spic' should not be banned because it's often used in the phrase "spic and span"
// 'tards' is currently not in this list because it's causing a problem for French speakers - it's commonly used within French words after an accented 'e' which the word blocker's regular expression treats as a word boundary // 'tards' is currently not in this list because it's causing a problem for French speakers
// - it's commonly used within French words after an accented 'e' which
// the word blocker's regular expression treats as a word boundary
// DO NOT EDIT! See the comments at the top of this file. // DO NOT EDIT! See the comments at the top of this file.

View File

@@ -55,15 +55,20 @@
// 'fu' and 'fuq' because they have legitimate meanings in English and/or other languages. // 'fu' and 'fuq' because they have legitimate meanings in English and/or other languages.
// 'tard' because it's French for 'late' and there's no common synonyms. // 'tard' because it's French for 'late' and there's no common synonyms.
// 'god' because it is allowed for use in ways that are not oaths. // 'god' because it is allowed for use in ways that are not oaths.
// Tobacco products because they are more often mentioned when celebrating quitting than in a way that might trigger addictive behaviour. // Tobacco products because they are more often mentioned when celebrating
// quitting than in a way that might trigger addictive behaviour.
// Legitimate given names: 'Jesus', 'Sherry' // Legitimate given names: 'Jesus', 'Sherry'
// Legitimate surnames: 'Christ', 'Mead' // Legitimate surnames: 'Christ', 'Mead'
// Legitimate place names: 'Dyke' // Legitimate place names: 'Dyke'
// //
// Explanations for some blocked words: // Explanations for some blocked words:
// 'fag' means 'subject' in some Scandinavian languages but we have decided to block it for its use as an English-language slur; hopefully the Scandinavian languages have suitable synonyms. // 'fag' means 'subject' in some Scandinavian languages but
// we have decided to block it for its use as an English-language slur;
// hopefully the Scandinavian languages have suitable synonyms.
// 'slut' means 'end' in Danish but is blocked for the same reason as 'fag'. // 'slut' means 'end' in Danish but is blocked for the same reason as 'fag'.
// These words are blocked from use in the Tavern but do not appear in bannedSlurs.js because we do not want people to be automatically muted when the words are used appropriately in guilds. // These words are blocked from use in the Tavern but do not appear in bannedSlurs.js
// because we do not want people to be automatically muted when the words are used
// appropriately in guilds.
// DO NOT EDIT! See the comments at the top of this file. // DO NOT EDIT! See the comments at the top of this file.

View File

@@ -4,7 +4,7 @@ import nconf from 'nconf';
const MANIFEST_FILE_PATH = path.join(__dirname, '/../../client-old/manifest.json'); const MANIFEST_FILE_PATH = path.join(__dirname, '/../../client-old/manifest.json');
const BUILD_FOLDER_PATH = path.join(__dirname, '/../../build'); const BUILD_FOLDER_PATH = path.join(__dirname, '/../../build');
const manifestFiles = require(MANIFEST_FILE_PATH); const manifestFiles = require(MANIFEST_FILE_PATH); // eslint-disable-line import/no-dynamic-require
const IS_PROD = nconf.get('IS_PROD'); const IS_PROD = nconf.get('IS_PROD');
const buildFiles = []; const buildFiles = [];
@@ -23,7 +23,7 @@ function _walk (folder) {
if (relFolder) { if (relFolder) {
original = `${relFolder}/${original}`; original = `${relFolder}/${original}`;
fileName = `${relFolder}/${fileName}`; fileName = `${relFolder}/${fileName}`; // eslint-disable-line no-param-reassign
} }
buildFiles[original] = fileName; buildFiles[original] = fileName;

View File

@@ -1,4 +1,5 @@
// Currently this holds helpers for challenge api, but we should break this up into submodules as it expands // Currently this holds helpers for challenge api,
// but we should break this up into submodules as it expands
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import uuid from 'uuid'; import uuid from 'uuid';
import { model as Challenge } from '../../models/challenge'; import { model as Challenge } from '../../models/challenge';
@@ -11,7 +12,10 @@ import {
NotAuthorized, NotAuthorized,
} from '../errors'; } from '../errors';
const TASK_KEYS_TO_REMOVE = ['_id', 'completed', 'dateCompleted', 'history', 'id', 'streak', 'createdAt', 'challenge']; const TASK_KEYS_TO_REMOVE = [
'_id', 'completed', 'dateCompleted', 'history',
'id', 'streak', 'createdAt', 'challenge',
];
export function addUserJoinChallengeNotification (user) { export function addUserJoinChallengeNotification (user) {
if (user.achievements.joinedChallenge) return; if (user.achievements.joinedChallenge) return;

View File

@@ -1,5 +1,5 @@
import { model as User } from '../models/user'; import { model as User } from '../models/user'; // eslint-disable-line import/no-cycle
import { getUserInfo } from './email'; import { getUserInfo } from './email'; // eslint-disable-line import/no-cycle
import { sendNotification as sendPushNotification } from './pushNotifications'; import { sendNotification as sendPushNotification } from './pushNotifications';
export async function getAuthorEmailFromMessage (message) { export async function getAuthorEmailFromMessage (message) {
@@ -34,7 +34,12 @@ export async function sendChatPushNotifications (user, group, message, translate
identifier: 'groupActivity', identifier: 'groupActivity',
category: 'groupActivity', category: 'groupActivity',
payload: { payload: {
groupID: group._id, type: group.type, groupName: group.name, message: message.text, timestamp: message.timestamp, senderName: message.user, groupID: group._id,
type: group.type,
groupName: group.name,
message: message.text,
timestamp: message.timestamp,
senderName: message.user,
}, },
}, },
); );

View File

@@ -1,7 +1,10 @@
import _ from 'lodash'; import _ from 'lodash';
import { chatModel as Chat } from '../../models/message'; import { chatModel as Chat } from '../../models/message';
import shared from '../../../common'; import shared from '../../../common';
import { MAX_CHAT_COUNT, MAX_SUBBED_GROUP_CHAT_COUNT } from '../../models/group'; import { // eslint-disable-line import/no-cycle
MAX_CHAT_COUNT,
MAX_SUBBED_GROUP_CHAT_COUNT,
} from '../../models/group';
const questScrolls = shared.content.quests; const questScrolls = shared.content.quests;
@@ -31,14 +34,17 @@ export function translateMessage (lang, info) {
const { spells } = shared.content; const { spells } = shared.content;
const { quests } = shared.content; const { quests } = shared.content;
switch (info.type) { switch (info.type) { // eslint-disable-line default-case
case 'quest_start': case 'quest_start':
msg = shared.i18n.t('chatQuestStarted', { questName: questScrolls[info.quest].text(lang) }, lang); msg = shared.i18n.t('chatQuestStarted', { questName: questScrolls[info.quest].text(lang) }, lang);
break; break;
case 'boss_damage': case 'boss_damage':
msg = shared.i18n.t('chatBossDamage', { msg = shared.i18n.t('chatBossDamage', {
username: info.user, bossName: questScrolls[info.quest].boss.name(lang), userDamage: info.userDamage, bossDamage: info.bossDamage, username: info.user,
bossName: questScrolls[info.quest].boss.name(lang),
userDamage: info.userDamage,
bossDamage: info.bossDamage,
}, lang); }, lang);
break; break;

View File

@@ -7,7 +7,9 @@ export default class ChatReporter {
this.res = res; this.res = res;
} }
async validate () {} async validate () { // eslint-disable-line class-methods-use-this
throw new Error('Not implemented');
}
async getMessageVariables (group, message) { async getMessageVariables (group, message) {
const reporterEmail = getUserInfo(this.user, ['email']).email; const reporterEmail = getUserInfo(this.user, ['email']).email;
@@ -34,7 +36,7 @@ export default class ChatReporter {
]; ];
} }
createGenericAuthorVariables (prefix, { createGenericAuthorVariables (prefix, { // eslint-disable-line class-methods-use-this
user, username, uuid, email, user, username, uuid, email,
}) { }) {
return [ return [
@@ -56,7 +58,7 @@ export default class ChatReporter {
}); });
} }
async flag () { async flag () { // eslint-disable-line class-methods-use-this
throw new Error('Flag must be implemented'); throw new Error('Flag must be implemented');
} }
} }

View File

@@ -1,10 +1,12 @@
import GroupChatReporter from './groupChatReporter'; import GroupChatReporter from './groupChatReporter';
import InboxChatReporter from './inboxChatReporter'; import InboxChatReporter from './inboxChatReporter';
export function chatReporterFactory (type, req, res) { export function chatReporterFactory (type, req, res) { // eslint-disable-line import/prefer-default-export, max-len
if (type === 'Group') { if (type === 'Group') {
return new GroupChatReporter(req, res); return new GroupChatReporter(req, res);
} if (type === 'Inbox') { } if (type === 'Inbox') {
return new InboxChatReporter(req, res); return new InboxChatReporter(req, res);
} }
throw new Error('Invalid chat reporter type.');
} }

View File

@@ -13,7 +13,10 @@ import { chatModel as Chat } from '../../models/message';
import apiError from '../apiError'; import apiError from '../apiError';
const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL'); const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL');
const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map(email => ({ email, canSend: true })); const FLAG_REPORT_EMAILS = nconf
.get('FLAG_REPORT_EMAIL')
.split(',')
.map(email => ({ email, canSend: true }));
const USER_AGE_FOR_FLAGGING = 3; // accounts less than this many days old don't increment flagCount const USER_AGE_FOR_FLAGGING = 3; // accounts less than this many days old don't increment flagCount
export default class GroupChatReporter extends ChatReporter { export default class GroupChatReporter extends ChatReporter {
@@ -79,7 +82,7 @@ export default class GroupChatReporter extends ChatReporter {
// Arbitrary amount, higher than 2 // Arbitrary amount, higher than 2
message.flagCount = 5; message.flagCount = 5;
} else if (increaseFlagCount) { } else if (increaseFlagCount) {
message.flagCount++; message.flagCount += 1;
} }
await message.save(); await message.save();

View File

@@ -12,7 +12,9 @@ import apiError from '../apiError';
import * as inboxLib from '../inbox'; import * as inboxLib from '../inbox';
import { getAuthorEmailFromMessage } from '../chat'; import { getAuthorEmailFromMessage } from '../chat';
const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map(email => ({ email, canSend: true })); const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL')
.split(',')
.map(email => ({ email, canSend: true }));
export default class InboxChatReporter extends ChatReporter { export default class InboxChatReporter extends ChatReporter {
constructor (req, res) { constructor (req, res) {
@@ -89,7 +91,7 @@ export default class InboxChatReporter extends ChatReporter {
]; ];
} }
updateMessageAndSave (message, ...changedFields) { updateMessageAndSave (message, ...changedFields) { // eslint-disable-line class-methods-use-this
for (const changedField of changedFields) { for (const changedField of changedFields) {
message.markModified(changedField); message.markModified(changedField);
} }

View File

@@ -1,5 +1,5 @@
const ROOT = `${__dirname}/../../../`; const ROOT = `${__dirname}/../../../`;
export function serveClient (expressRes) { export function serveClient (expressRes) { // eslint-disable-line import/prefer-default-export
return expressRes.sendFile('./dist-client/index.html', { root: ROOT }); return expressRes.sendFile('./dist-client/index.html', { root: ROOT });
} }

View File

@@ -1,7 +1,7 @@
import findIndex from 'lodash/findIndex'; import findIndex from 'lodash/findIndex';
import isPlainObject from 'lodash/isPlainObject'; import isPlainObject from 'lodash/isPlainObject';
export function removeFromArray (array, element) { export function removeFromArray (array, element) { // eslint-disable-line import/prefer-default-export, max-len
let elementIndex; let elementIndex;
if (isPlainObject(element)) { if (isPlainObject(element)) {

View File

@@ -1,6 +1,6 @@
import { model as Coupon } from '../../models/coupon'; import { model as Coupon } from '../../models/coupon';
export async function enterCode (req, res, user) { export async function enterCode (req, res, user) { // eslint-disable-line import/prefer-default-export, max-len
req.checkParams('code', res.t('couponCodeRequired')).notEmpty(); req.checkParams('code', res.t('couponCodeRequired')).notEmpty();
const validationErrors = req.validationErrors(); const validationErrors = req.validationErrors();

View File

@@ -36,7 +36,7 @@ export async function recoverCron (status, locals) {
if (!reloadedUser) { if (!reloadedUser) {
throw new Error(`User ${user._id} not found while recovering.`); throw new Error(`User ${user._id} not found while recovering.`);
} else if (reloadedUser._cronSignature !== 'NOT_RUNNING') { } else if (reloadedUser._cronSignature !== 'NOT_RUNNING') {
status.times++; status.times += 1;
if (status.times < 5) { if (status.times < 5) {
await recoverCron(status, locals); await recoverCron(status, locals);
@@ -45,7 +45,6 @@ export async function recoverCron (status, locals) {
} }
} else { } else {
locals.user = reloadedUser; locals.user = reloadedUser;
return null;
} }
} }
@@ -59,7 +58,8 @@ const CLEAR_BUFFS = {
}; };
function grantEndOfTheMonthPerks (user, now) { function grantEndOfTheMonthPerks (user, now) {
const SUBSCRIPTION_BASIC_BLOCK_LENGTH = 3; // multi-month subscriptions are for multiples of 3 months // multi-month subscriptions are for multiples of 3 months
const SUBSCRIPTION_BASIC_BLOCK_LENGTH = 3;
const { plan } = user.purchased; const { plan } = user.purchased;
const subscriptionEndDate = moment(plan.dateTerminated).isBefore() ? moment(plan.dateTerminated).startOf('month') : moment(now).startOf('month'); const subscriptionEndDate = moment(plan.dateTerminated).isBefore() ? moment(plan.dateTerminated).startOf('month') : moment(now).startOf('month');
const dateUpdatedMoment = moment(plan.dateUpdated).startOf('month'); const dateUpdatedMoment = moment(plan.dateUpdated).startOf('month');
@@ -67,54 +67,74 @@ function grantEndOfTheMonthPerks (user, now) {
if (elapsedMonths > 0) { if (elapsedMonths > 0) {
plan.dateUpdated = now; plan.dateUpdated = now;
// For every month, inc their "consecutive months" counter. Give perks based on consecutive blocks // For every month, inc their "consecutive months" counter.
// If they already got perks for those blocks (eg, 6mo subscription, subscription gifts, etc) - then dec the offset until it hits 0 // Give perks based on consecutive blocks
// If they already got perks for those blocks (eg, 6mo subscription,
// subscription gifts, etc) - then dec the offset until it hits 0
_.defaults(plan.consecutive, { _.defaults(plan.consecutive, {
count: 0, offset: 0, trinkets: 0, gemCapExtra: 0, count: 0, offset: 0, trinkets: 0, gemCapExtra: 0,
}); });
let planMonthsLength = 1; // 1 for one-month recurring or gift subscriptions; later set to 3 for 3-month recurring, etc. // 1 for one-month recurring or gift subscriptions; later set to 3 for 3-month recurring, etc.
let planMonthsLength = 1;
for (let i = 0; i < elapsedMonths; i++) { for (let i = 0; i < elapsedMonths; i += 1) {
plan.consecutive.count++; plan.consecutive.count += 1;
plan.consecutive.offset--; plan.consecutive.offset -= 1;
// If offset is now greater than 0, the user is within a period for which they have already been given the consecutive months perks. // If offset is now greater than 0, the user is within a period
// for which they have already been given the consecutive months perks.
// //
// If offset now equals 0, this is the final month for which the user has already been given the consecutive month perks. // If offset now equals 0, this is the final month for which
// We do not give them more perks yet because they might cancel the subscription before the next payment is taken. // the user has already been given the consecutive month perks.
// We do not give them more perks yet because they might cancel
// the subscription before the next payment is taken.
// //
// If offset is now less than 0, the user EITHER has a single-month recurring subscription and MIGHT be due for perks, // If offset is now less than 0, the user EITHER has
// OR has a multi-month subscription that renewed some time in the previous calendar month and so they are due for a new set of perks // a single-month recurring subscription and MIGHT be due for perks,
// (strictly speaking, they should have been given the perks at the time that next payment was taken, but we don't have support for // OR has a multi-month subscription that renewed some time
// in the previous calendar month and so they are due for a new set of perks
// (strictly speaking, they should have been given the perks
// at the time that next payment was taken, but we don't have support for
// tracking payments like that - giving the perks when offset is < 0 is a workaround). // tracking payments like that - giving the perks when offset is < 0 is a workaround).
if (plan.consecutive.offset < 0) { if (plan.consecutive.offset < 0) {
if (plan.planId) { if (plan.planId) {
// NB gift subscriptions don't have a planID (which doesn't matter because we don't need to reapply perks for them and by this point they should have expired anyway) // NB gift subscriptions don't have a planID
// (which doesn't matter because we don't need to reapply perks
// for them and by this point they should have expired anyway)
const planIdRegExp = new RegExp('_([0-9]+)mo'); // e.g., matches 'google_6mo' / 'basic_12mo' and captures '6' / '12' const planIdRegExp = new RegExp('_([0-9]+)mo'); // e.g., matches 'google_6mo' / 'basic_12mo' and captures '6' / '12'
const match = plan.planId.match(planIdRegExp); const match = plan.planId.match(planIdRegExp);
if (match !== null && match[0] !== null) { if (match !== null && match[0] !== null) {
planMonthsLength = match[1]; // 3 for 3-month recurring subscription, etc // 3 for 3-month recurring subscription, etc
planMonthsLength = match[1]; // eslint-disable-line prefer-destructuring
} }
} }
let perkAmountNeeded = 0; // every 3 months you get one set of perks - this variable records how many sets you need // every 3 months you get one set of perks - this variable records how many sets you need
let perkAmountNeeded = 0;
if (planMonthsLength === 1) { if (planMonthsLength === 1) {
// User has a single-month recurring subscription and are due for perks IF they've been subscribed for a multiple of 3 months. // User has a single-month recurring subscription and are due for perks
// IF they've been subscribed for a multiple of 3 months.
if (plan.consecutive.count % SUBSCRIPTION_BASIC_BLOCK_LENGTH === 0) { // every 3 months if (plan.consecutive.count % SUBSCRIPTION_BASIC_BLOCK_LENGTH === 0) { // every 3 months
perkAmountNeeded = 1; perkAmountNeeded = 1;
} }
plan.consecutive.offset = 0; // allow the same logic to be run next month plan.consecutive.offset = 0; // allow the same logic to be run next month
} else { } else {
// User has a multi-month recurring subscription and it renewed in the previous calendar month. // User has a multi-month recurring subscription
perkAmountNeeded = planMonthsLength / SUBSCRIPTION_BASIC_BLOCK_LENGTH; // e.g., for a 6-month subscription, give two sets of perks // and it renewed in the previous calendar month.
plan.consecutive.offset = planMonthsLength - 1; // don't need to check for perks again for this many months (subtract 1 because we should have run this when the payment was taken last month)
// e.g., for a 6-month subscription, give two sets of perks
perkAmountNeeded = planMonthsLength / SUBSCRIPTION_BASIC_BLOCK_LENGTH;
// don't need to check for perks again for this many months
// (subtract 1 because we should have run this when the payment was taken last month)
plan.consecutive.offset = planMonthsLength - 1;
} }
if (perkAmountNeeded > 0) { if (perkAmountNeeded > 0) {
plan.consecutive.trinkets += perkAmountNeeded; // one Hourglass every 3 months plan.consecutive.trinkets += perkAmountNeeded; // one Hourglass every 3 months
plan.consecutive.gemCapExtra += 5 * perkAmountNeeded; // 5 extra Gems every 3 months plan.consecutive.gemCapExtra += 5 * perkAmountNeeded; // 5 extra Gems every 3 months
if (plan.consecutive.gemCapExtra > 25) plan.consecutive.gemCapExtra = 25; // cap it at 50 (hard 25 limit + extra 25) // cap it at 50 (hard 25 limit + extra 25)
if (plan.consecutive.gemCapExtra > 25) plan.consecutive.gemCapExtra = 25;
} }
} }
} }
@@ -143,11 +163,13 @@ function resetHabitCounters (user, tasksByType, now, daysMissed) {
// check if we've passed a day on which we should reset the habit counters, including today // check if we've passed a day on which we should reset the habit counters, including today
let resetWeekly = false; let resetWeekly = false;
let resetMonthly = false; let resetMonthly = false;
for (let i = 0; i < daysMissed; i++) { for (let i = 0; i < daysMissed; i += 1) {
if (resetWeekly === true && resetMonthly === true) { if (resetWeekly === true && resetMonthly === true) {
break; break;
} }
const thatDay = moment(now).zone(user.preferences.timezoneOffset + user.preferences.dayStart * 60).subtract({ days: i }); const thatDay = moment(now)
.zone(user.preferences.timezoneOffset + user.preferences.dayStart * 60)
.subtract({ days: i });
if (thatDay.day() === 1) { if (thatDay.day() === 1) {
resetWeekly = true; resetWeekly = true;
} }
@@ -189,7 +211,10 @@ function trackCronAnalytics (analytics, user, _progress, options) {
loginIncentives: user.loginIncentives, loginIncentives: user.loginIncentives,
}); });
if (user.party && user.party.quest && !user.party.quest.RSVPNeeded && !user.party.quest.completed && user.party.quest.key && !user.preferences.sleep) { if (
user.party && user.party.quest && !user.party.quest.RSVPNeeded
&& !user.party.quest.completed && user.party.quest.key && !user.preferences.sleep
) {
analytics.track('quest participation', { analytics.track('quest participation', {
category: 'behavior', category: 'behavior',
uuid: user._id, uuid: user._id,
@@ -278,22 +303,28 @@ export function cron (options = {}) {
} }
const { plan } = user.purchased; const { plan } = user.purchased;
const userHasTerminatedSubscription = plan.dateTerminated && moment(plan.dateTerminated).isBefore(new Date()); const userHasTerminatedSubscription = plan.dateTerminated
&& moment(plan.dateTerminated).isBefore(new Date());
if (!CRON_SAFE_MODE && userHasTerminatedSubscription) removeTerminatedSubscription(user); if (!CRON_SAFE_MODE && userHasTerminatedSubscription) removeTerminatedSubscription(user);
// Login Incentives // Login Incentives
user.loginIncentives++; user.loginIncentives += 1;
awardLoginIncentives(user); awardLoginIncentives(user);
const multiDaysCountAsOneDay = true; const multiDaysCountAsOneDay = true;
// If the user does not log in for two or more days, cron (mostly) acts as if it were only one day. // If the user does not log in for two or more days,
// cron (mostly) acts as if it were only one day.
// When site-wide difficulty settings are introduced, this can be a user preference option. // When site-wide difficulty settings are introduced, this can be a user preference option.
// Tally each task // Tally each task
let todoTally = 0; let todoTally = 0;
tasksByType.todos.forEach(task => { // make uncompleted To-Dos redder (further incentive to complete them) // make uncompleted To-Dos redder (further incentive to complete them)
if (task.group.assignedDate && moment(task.group.assignedDate).isAfter(user.auth.timestamps.updated)) return; tasksByType.todos.forEach(task => {
if (
task.group.assignedDate
&& moment(task.group.assignedDate).isAfter(user.auth.timestamps.updated)
) return;
scoreTask({ scoreTask({
task, task,
user, user,
@@ -305,7 +336,8 @@ export function cron (options = {}) {
todoTally += task.value; todoTally += task.value;
}); });
// For incomplete Dailys, add value (further incentive), deduct health, keep records for later decreasing the nightly mana gain. // For incomplete Dailys, add value (further incentive),
// deduct health, keep records for later decreasing the nightly mana gain.
// The negative effects are not done when resting in the inn. // The negative effects are not done when resting in the inn.
let dailyChecked = 0; // how many dailies were checked? let dailyChecked = 0; // how many dailies were checked?
let dailyDueUnchecked = 0; // how many dailies were un-checked? let dailyDueUnchecked = 0; // how many dailies were un-checked?
@@ -313,7 +345,10 @@ export function cron (options = {}) {
if (!user.party.quest.progress.down) user.party.quest.progress.down = 0; if (!user.party.quest.progress.down) user.party.quest.progress.down = 0;
tasksByType.dailys.forEach(task => { tasksByType.dailys.forEach(task => {
if (task.group.assignedDate && moment(task.group.assignedDate).isAfter(user.auth.timestamps.updated)) return; if (
task.group.assignedDate
&& moment(task.group.assignedDate).isAfter(user.auth.timestamps.updated)
) return;
const { completed } = task; const { completed } = task;
// Deduct points for missed Daily tasks // Deduct points for missed Daily tasks
let EvadeTask = 0; let EvadeTask = 0;
@@ -329,29 +364,35 @@ export function cron (options = {}) {
// dailys repeat, so need to calculate how many they've missed according to their own schedule // dailys repeat, so need to calculate how many they've missed according to their own schedule
scheduleMisses = 0; scheduleMisses = 0;
for (let i = 0; i < daysMissed; i++) { for (let i = 0; i < daysMissed; i += 1) {
const thatDay = moment(now).subtract({ days: i + 1 }); const thatDay = moment(now).subtract({ days: i + 1 });
if (shouldDo(thatDay.toDate(), task, user.preferences)) { if (shouldDo(thatDay.toDate(), task, user.preferences)) {
atLeastOneDailyDue = true; atLeastOneDailyDue = true;
scheduleMisses++; scheduleMisses += 1;
if (user.stats.buffs.stealth) { if (user.stats.buffs.stealth) {
user.stats.buffs.stealth--; user.stats.buffs.stealth -= 1;
EvadeTask++; EvadeTask += 1;
} }
} }
if (multiDaysCountAsOneDay) break; if (multiDaysCountAsOneDay) break;
} }
if (scheduleMisses > EvadeTask) { if (scheduleMisses > EvadeTask) {
// The user did not complete this due Daily (but no penalty if cron is running in safe mode). // The user did not complete this due Daily
// (but no penalty if cron is running in safe mode).
if (CRON_SAFE_MODE) { if (CRON_SAFE_MODE) {
dailyChecked += 1; // allows full allotment of mp to be gained dailyChecked += 1; // allows full allotment of mp to be gained
} else { } else {
perfect = false; perfect = false;
if (task.checklist && task.checklist.length > 0) { // Partially completed checklists dock fewer mana points // Partially completed checklists dock fewer mana points
const fractionChecked = _.reduce(task.checklist, (m, i) => m + (i.completed ? 1 : 0), 0) / task.checklist.length; if (task.checklist && task.checklist.length > 0) {
const fractionChecked = _.reduce(
task.checklist,
(m, i) => m + (i.completed ? 1 : 0),
0,
) / task.checklist.length;
dailyDueUnchecked += 1 - fractionChecked; dailyDueUnchecked += 1 - fractionChecked;
dailyChecked += fractionChecked; dailyChecked += fractionChecked;
} else { } else {
@@ -370,7 +411,8 @@ export function cron (options = {}) {
if (!CRON_SEMI_SAFE_MODE) { if (!CRON_SEMI_SAFE_MODE) {
// Apply damage from a boss, less damage for Trivial priority (difficulty) // Apply damage from a boss, less damage for Trivial priority (difficulty)
user.party.quest.progress.down += delta * (task.priority < 1 ? task.priority : 1); user.party.quest.progress.down += delta * (task.priority < 1 ? task.priority : 1);
// NB: Medium and Hard priorities do not increase damage from boss. This was by accident // NB: Medium and Hard priorities do not increase damage from boss.
// This was by accident
// initially, and when we realised, we could not fix it because users are used to // initially, and when we realised, we could not fix it because users are used to
// their Medium and Hard Dailies doing an Easy amount of damage from boss. // their Medium and Hard Dailies doing an Easy amount of damage from boss.
// Easy is task.priority = 1. Anything < 1 will be Trivial (0.1) or any future // Easy is task.priority = 1. Anything < 1 will be Trivial (0.1) or any future
@@ -392,7 +434,7 @@ export function cron (options = {}) {
if (completed || scheduleMisses > 0) { if (completed || scheduleMisses > 0) {
if (task.checklist) { if (task.checklist) {
task.checklist.forEach(i => i.completed = false); task.checklist.forEach(i => { i.completed = false; });
} }
} }
@@ -427,14 +469,18 @@ export function cron (options = {}) {
let expTally = user.stats.exp; let expTally = user.stats.exp;
let lvl = 0; // iterator let lvl = 0; // iterator
while (lvl < user.stats.lvl - 1) { while (lvl < user.stats.lvl - 1) {
lvl++; lvl += 1;
expTally += common.tnl(lvl); expTally += common.tnl(lvl);
} }
user.history.exp.push({ date: now, value: expTally }); user.history.exp.push({ date: now, value: expTally });
// Remove any remaining completed todos from the list of active todos // Remove any remaining completed todos from the list of active todos
user.tasksOrder.todos = user.tasksOrder.todos.filter(taskOrderId => _.some(tasksByType.todos, taskType => taskType._id === taskOrderId && taskType.completed === false)); user.tasksOrder.todos = user.tasksOrder.todos
.filter(taskOrderId => _.some(
tasksByType.todos,
taskType => taskType._id === taskOrderId && taskType.completed === false,
));
// TODO also adjust tasksOrder arrays to remove deleted tasks of any kind (including rewards), ensure that all existing tasks are in the arrays, no tasks IDs are duplicated -- https://github.com/HabitRPG/habitica/issues/7645 // TODO also adjust tasksOrder arrays to remove deleted tasks of any kind (including rewards), ensure that all existing tasks are in the arrays, no tasks IDs are duplicated -- https://github.com/HabitRPG/habitica/issues/7645
// preen user history so that it doesn't become a performance problem // preen user history so that it doesn't become a performance problem
@@ -442,7 +488,7 @@ export function cron (options = {}) {
preenUserHistory(user, tasksByType); preenUserHistory(user, tasksByType);
if (perfect && atLeastOneDailyDue) { if (perfect && atLeastOneDailyDue) {
user.achievements.perfect++; user.achievements.perfect += 1;
const lvlDiv2 = Math.ceil(common.capByLevel(user.stats.lvl) / 2); const lvlDiv2 = Math.ceil(common.capByLevel(user.stats.lvl) / 2);
user.stats.buffs = { user.stats.buffs = {
str: lvlDiv2, str: lvlDiv2,
@@ -456,15 +502,19 @@ export function cron (options = {}) {
user.stats.buffs = _.cloneDeep(CLEAR_BUFFS); user.stats.buffs = _.cloneDeep(CLEAR_BUFFS);
} }
// Add 10 MP, or 10% of max MP if that'd be more. Perform this after Perfect Day for maximum benefit // Add 10 MP, or 10% of max MP if that'd be more.
// Perform this after Perfect Day for maximum benefit
// Adjust for fraction of dailies completed // Adjust for fraction of dailies completed
if (!user.preferences.sleep) { if (!user.preferences.sleep) {
if (dailyDueUnchecked === 0 && dailyChecked === 0) dailyChecked = 1; if (dailyDueUnchecked === 0 && dailyChecked === 0) dailyChecked = 1;
user.stats.mp += _.max([10, 0.1 * common.statsComputed(user).maxMP]) * dailyChecked / (dailyDueUnchecked + dailyChecked); user.stats.mp += (_.max([10, 0.1 * common.statsComputed(user).maxMP]) * dailyChecked) / (dailyDueUnchecked + dailyChecked); // eslint-disable-line max-len
if (user.stats.mp > common.statsComputed(user).maxMP) user.stats.mp = common.statsComputed(user).maxMP; if (user.stats.mp > common.statsComputed(user).maxMP) {
user.stats.mp = common.statsComputed(user).maxMP;
}
} }
// After all is said and done, progress up user's effect on quest, return those values & reset the user's // After all is said and done,
// progress up user's effect on quest, return those values & reset the user's
if (!user.preferences.sleep) { if (!user.preferences.sleep) {
const { progress } = user.party.quest; const { progress } = user.party.quest;
_progress = progress.toObject(); // clone the old progress object _progress = progress.toObject(); // clone the old progress object
@@ -488,7 +538,7 @@ export function cron (options = {}) {
}); });
// Analytics // Analytics
user.flags.cronCount++; user.flags.cronCount += 1;
trackCronAnalytics(analytics, user, _progress, options); trackCronAnalytics(analytics, user, _progress, options);
return _progress; return _progress;

View File

@@ -1,6 +1,6 @@
import nconf from 'nconf'; import nconf from 'nconf';
import got from 'got'; import got from 'got';
import { TAVERN_ID } from '../models/group'; import { TAVERN_ID } from '../models/group'; // eslint-disable-line import/no-cycle
import { encrypt } from './encryption'; import { encrypt } from './encryption';
import logger from './logger'; import logger from './logger';
import common from '../../common'; import common from '../../common';
@@ -27,7 +27,12 @@ export function getUserInfo (user, fields = []) {
info.email = user.auth.local.email; info.email = user.auth.local.email;
} else { } else {
common.constants.SUPPORTED_SOCIAL_NETWORKS.forEach(network => { common.constants.SUPPORTED_SOCIAL_NETWORKS.forEach(network => {
if (user.auth[network.key] && user.auth[network.key].emails && user.auth[network.key].emails[0] && user.auth[network.key].emails[0].value) { if (
user.auth[network.key]
&& user.auth[network.key].emails
&& user.auth[network.key].emails[0]
&& user.auth[network.key].emails[0].value
) {
info.email = user.auth[network.key].emails[0].value; info.email = user.auth[network.key].emails[0].value;
} }
}); });
@@ -62,23 +67,26 @@ export function getGroupUrl (group) {
// Send a transactional email using Mandrill through the external email server // Send a transactional email using Mandrill through the external email server
export function sendTxn (mailingInfoArray, emailType, variables, personalVariables) { export function sendTxn (mailingInfoArray, emailType, variables, personalVariables) {
mailingInfoArray = Array.isArray(mailingInfoArray) ? mailingInfoArray : [mailingInfoArray]; mailingInfoArray = Array.isArray(mailingInfoArray) ? mailingInfoArray : [mailingInfoArray]; // eslint-disable-line no-param-reassign, max-len
variables = [ variables = [ // eslint-disable-line no-param-reassign, max-len
{ name: 'BASE_URL', content: BASE_URL }, { name: 'BASE_URL', content: BASE_URL },
].concat(variables || []); ].concat(variables || []);
// It's important to pass at least a user with its `preferences` as we need to check if he unsubscribed // It's important to pass at least a user with its `preferences`
mailingInfoArray = mailingInfoArray.map(mailingInfo => (mailingInfo._id ? getUserInfo(mailingInfo, ['_id', 'email', 'name', 'canSend']) : mailingInfo)).filter(mailingInfo => // as we need to check if he unsubscribed
mailingInfoArray = mailingInfoArray // eslint-disable-line no-param-reassign, max-len
.map(mailingInfo => (mailingInfo._id ? getUserInfo(mailingInfo, ['_id', 'email', 'name', 'canSend']) : mailingInfo))
// Always send reset-password emails // Always send reset-password emails
// Don't check canSend for non registered users as already checked before // Don't check canSend for non registered users as already checked before
mailingInfo.email && (!mailingInfo._id || mailingInfo.canSend || emailType === 'reset-password')); .filter(mailingInfo => mailingInfo.email
&& (!mailingInfo._id || mailingInfo.canSend || emailType === 'reset-password'));
// Personal variables are personal to each email recipient, if they are missing // Personal variables are personal to each email recipient, if they are missing
// we manually create a structure for them with RECIPIENT_NAME and RECIPIENT_UNSUB_URL // we manually create a structure for them with RECIPIENT_NAME and RECIPIENT_UNSUB_URL
// otherwise we just add RECIPIENT_NAME and RECIPIENT_UNSUB_URL to the existing personal variables // otherwise we just add RECIPIENT_NAME and RECIPIENT_UNSUB_URL to the existing personal variables
if (!personalVariables || personalVariables.length === 0) { if (!personalVariables || personalVariables.length === 0) {
personalVariables = mailingInfoArray.map(mailingInfo => ({ personalVariables = mailingInfoArray.map(mailingInfo => ({ // eslint-disable-line no-param-reassign, max-len
rcpt: mailingInfo.email, rcpt: mailingInfo.email,
vars: [ vars: [
{ {

View File

@@ -1,4 +1,4 @@
import common from '../../common'; import common from '../../common'; // eslint-disable-line max-classes-per-file
export const { CustomError } = common.errors; export const { CustomError } = common.errors;

View File

@@ -1,6 +1,7 @@
/* eslint-disable no-multiple-empty-lines */ /* eslint-disable no-multiple-empty-lines */
// This file contains usernames that we do not want users to use, because they give the account more legitimacy and may deceive users. // This file contains usernames that we do not want users to use,
// because they give the account more legitimacy and may deceive users.
const bannedWords = [ const bannedWords = [
'TESTPLACEHOLDERSWEARWORDHERE', 'TESTPLACEHOLDERSWEARWORDHERE',
'TESTPLACEHOLDERSWEARWORDHERE1', 'TESTPLACEHOLDERSWEARWORDHERE1',

View File

@@ -3,10 +3,8 @@ import nconf from 'nconf';
const IS_PROD = nconf.get('IS_PROD'); const IS_PROD = nconf.get('IS_PROD');
const STACKDRIVER_TRACING_ENABLED = nconf.get('ENABLE_STACKDRIVER_TRACING') === 'true'; const STACKDRIVER_TRACING_ENABLED = nconf.get('ENABLE_STACKDRIVER_TRACING') === 'true';
let tracer = null; const tracer = IS_PROD && STACKDRIVER_TRACING_ENABLED
? require('@google-cloud/trace-agent').start()
if (IS_PROD && STACKDRIVER_TRACING_ENABLED) { : null;
tracer = require('@google-cloud/trace-agent').start(); // eslint-disable-line global-require
}
export default tracer; export default tracer;

View File

@@ -1,4 +1,4 @@
import * as Tasks from '../models/task'; import * as Tasks from '../models/task'; // eslint-disable-line import/no-cycle
const SHARED_COMPLETION = { const SHARED_COMPLETION = {
default: 'recurringCompletion', default: 'recurringCompletion',
@@ -28,7 +28,7 @@ async function _evaluateAllAssignedCompletion (masterTask) {
'group.taskId': masterTask._id, 'group.taskId': masterTask._id,
'group.approval.approved': true, 'group.approval.approved': true,
}).exec(); }).exec();
completions++; completions += 1;
} else { } else {
completions = await Tasks.Task.count({ completions = await Tasks.Task.count({
'group.taskId': masterTask._id, 'group.taskId': masterTask._id,

View File

@@ -3,7 +3,8 @@
// - literature guilds (quoting of passages containing words banned as oaths); // - literature guilds (quoting of passages containing words banned as oaths);
// - food/drink/lifestyle/perfume guilds (alcohol allowed); // - food/drink/lifestyle/perfume guilds (alcohol allowed);
// - guilds dealing with traumatic life events (must be allowed to describe them); // - guilds dealing with traumatic life events (must be allowed to describe them);
// - foreign language guilds using the Roman alphabet (avoid accidental banning of non-English words). // - foreign language guilds using the Roman alphabet
// (avoid accidental banning of non-English words).
// //
// This is for a short-term, partial solution to the need for swearword blocking in guilds. // This is for a short-term, partial solution to the need for swearword blocking in guilds.
// Later, it will be replaced with customised lists of disallowed words based on the guilds' tags. // Later, it will be replaced with customised lists of disallowed words based on the guilds' tags.

View File

@@ -61,7 +61,7 @@ function _loadTranslations (locale) {
if (path.extname(file) !== '.json') return; if (path.extname(file) !== '.json') return;
// We use require to load and parse a JSON file // We use require to load and parse a JSON file
_.merge(translations[locale], require(path.join(localePath, locale, file))); // eslint-disable-line global-require _.merge(translations[locale], require(path.join(localePath, locale, file))); // eslint-disable-line global-require, import/no-dynamic-require, max-len
}); });
} }
@@ -93,7 +93,8 @@ langCodes.forEach(code => {
lang.momentLangCode = momentLangsMapping[code] || code; lang.momentLangCode = momentLangsMapping[code] || code;
try { try {
// MomentJS lang files are JS files that has to be executed in the browser so we load them as plain text files // MomentJS lang files are JS files that has to be executed
// in the browser so we load them as plain text files
// We wrap everything in a try catch because the file might not exist // We wrap everything in a try catch because the file might not exist
const f = fs.readFileSync(path.join(__dirname, `/../../../node_modules/moment/locale/${lang.momentLangCode}.js`), 'utf8'); const f = fs.readFileSync(path.join(__dirname, `/../../../node_modules/moment/locale/${lang.momentLangCode}.js`), 'utf8');
@@ -104,7 +105,8 @@ langCodes.forEach(code => {
}); });
// Remove en_GB from langCodes checked by browser to avoid it being // Remove en_GB from langCodes checked by browser to avoid it being
// used in place of plain original 'en' (it's an optional language that can be enabled only in setting) // used in place of plain original 'en'
// (it's an optional language that can be enabled only in setting)
export const defaultLangCodes = _.without(langCodes, 'en_GB'); export const defaultLangCodes = _.without(langCodes, 'en_GB');
// A map of languages that have different versions and the relative versions // A map of languages that have different versions and the relative versions

View File

@@ -1,5 +1,5 @@
import { mapInboxMessage, inboxModel as Inbox } from '../../models/message'; import { mapInboxMessage, inboxModel as Inbox } from '../../models/message';
import { getUserInfo, sendTxn as sendTxnEmail } from '../email'; import { getUserInfo, sendTxn as sendTxnEmail } from '../email'; // eslint-disable-line import/no-cycle
import { sendNotification as sendPushNotification } from '../pushNotifications'; import { sendNotification as sendPushNotification } from '../pushNotifications';
const PM_PER_PAGE = 10; const PM_PER_PAGE = 10;
@@ -18,7 +18,11 @@ export async function sentMessage (sender, receiver, message, translate) {
sendPushNotification( sendPushNotification(
receiver, receiver,
{ {
title: translate('newPMNotificationTitle', { name: getUserInfo(sender, ['name']).name }, receiver.preferences.language), title: translate(
'newPMNotificationTitle',
{ name: getUserInfo(sender, ['name']).name },
receiver.preferences.language,
),
message, message,
identifier: 'newPM', identifier: 'newPM',
category: 'newPM', category: 'newPM',
@@ -71,7 +75,7 @@ export async function getUserInbox (user, options = {
return messages; return messages;
} }
const messagesObj = {}; const messagesObj = {};
messages.forEach(msg => messagesObj[msg._id] = msg); messages.forEach(msg => { messagesObj[msg._id] = msg; });
return messagesObj; return messagesObj;
} }

View File

@@ -121,6 +121,8 @@ async function addInvitationToUser (userToInvite, group, inviter, res) {
if (group.type === 'party') { if (group.type === 'party') {
return userInvited.invitations.parties[userToInvite.invitations.parties.length - 1]; return userInvited.invitations.parties[userToInvite.invitations.parties.length - 1];
} }
throw new Error('Invalid group type');
} }
async function inviteByUUID (uuid, group, inviter, req, res) { async function inviteByUUID (uuid, group, inviter, req, res) {
@@ -134,10 +136,13 @@ async function inviteByUUID (uuid, group, inviter, req, res) {
const objections = inviter.getObjectionsToInteraction('group-invitation', userToInvite); const objections = inviter.getObjectionsToInteraction('group-invitation', userToInvite);
if (objections.length > 0) { if (objections.length > 0) {
throw new NotAuthorized(res.t(objections[0], { userId: uuid, username: userToInvite.profile.name })); throw new NotAuthorized(res.t(
objections[0],
{ userId: uuid, username: userToInvite.profile.name },
));
} }
return await addInvitationToUser(userToInvite, group, inviter, res); return addInvitationToUser(userToInvite, group, inviter, res);
} }
async function inviteByEmail (invite, group, inviter, req, res) { async function inviteByEmail (invite, group, inviter, req, res) {
@@ -191,8 +196,8 @@ async function inviteByEmail (invite, group, inviter, req, res) {
} }
async function inviteByUserName (username, group, inviter, req, res) { async function inviteByUserName (username, group, inviter, req, res) {
if (username.indexOf('@') === 0) username = username.slice(1, username.length); if (username.indexOf('@') === 0) username = username.slice(1, username.length); // eslint-disable-line no-param-reassign
username = username.toLowerCase(); username = username.toLowerCase(); // eslint-disable-line no-param-reassign
const userToInvite = await User.findOne({ 'auth.local.lowerCaseUsername': username }).exec(); const userToInvite = await User.findOne({ 'auth.local.lowerCaseUsername': username }).exec();
if (!userToInvite) { if (!userToInvite) {
@@ -203,7 +208,7 @@ async function inviteByUserName (username, group, inviter, req, res) {
throw new BadRequest(res.t('cannotInviteSelfToGroup')); throw new BadRequest(res.t('cannotInviteSelfToGroup'));
} }
return await addInvitationToUser(userToInvite, group, inviter, res); return addInvitationToUser(userToInvite, group, inviter, res);
} }
export { export {

View File

@@ -1,6 +1,6 @@
import { last } from 'lodash'; import { last } from 'lodash';
import shared from '../../../common'; import shared from '../../../common';
import { model as User } from '../../models/user'; import { model as User } from '../../models/user'; // eslint-disable-line import/no-cycle
// Build a list of gear items owned by default // Build a list of gear items owned by default
const defaultOwnedGear = {}; const defaultOwnedGear = {};
@@ -54,6 +54,8 @@ export function validateItemPath (itemPath) {
if (itemPath.indexOf('items.quests') === 0) { if (itemPath.indexOf('items.quests') === 0) {
return Boolean(shared.content.quests[key]); return Boolean(shared.content.quests[key]);
} }
return false;
} }
// When passed a value of an item in the user object it'll convert the // When passed a value of an item in the user object it'll convert the

View File

@@ -28,7 +28,9 @@ if (IS_PROD) {
json: true, json: true,
}); });
} }
} else if (!IS_TEST || IS_TEST && ENABLE_LOGS_IN_TEST) { // Do not log anything when testing unless specified
// Do not log anything when testing unless specified
} else if (!IS_TEST || (IS_TEST && ENABLE_LOGS_IN_TEST)) {
logger logger
.add(winston.transports.Console, { .add(winston.transports.Console, {
timestamp: true, timestamp: true,
@@ -55,7 +57,8 @@ const loggerInterface = {
const stack = err.stack || err.message || err; const stack = err.stack || err.message || err;
if (_.isPlainObject(errorData) && !errorData.fullError) { if (_.isPlainObject(errorData) && !errorData.fullError) {
// If the error object has interesting data (not only httpCode, message and name from the CustomError class) // If the error object has interesting data
// (not only httpCode, message and name from the CustomError class)
// add it to the logs // add it to the logs
if (err instanceof CustomError) { if (err instanceof CustomError) {
const errWithoutCommonProps = _.omit(err, ['name', 'httpCode', 'message']); const errWithoutCommonProps = _.omit(err, ['name', 'httpCode', 'message']);

View File

@@ -46,9 +46,9 @@ export async function compare (user, passwordToCheck) {
const passwordSalt = user.auth.local.salt; // Only used for SHA1 const passwordSalt = user.auth.local.salt; // Only used for SHA1
if (passwordHashMethod === 'bcrypt') { if (passwordHashMethod === 'bcrypt') {
return await bcryptCompare(passwordToCheck, passwordHash); return bcryptCompare(passwordToCheck, passwordHash);
// default to sha1 if the user has a salt but no passwordHashMethod // default to sha1 if the user has a salt but no passwordHashMethod
} if (passwordHashMethod === 'sha1' || !passwordHashMethod && passwordSalt) { } if (passwordHashMethod === 'sha1' || (!passwordHashMethod && passwordSalt)) {
return passwordHash === sha1Encrypt(passwordToCheck, passwordSalt); return passwordHash === sha1Encrypt(passwordToCheck, passwordSalt);
} }
throw new Error('Invalid password hash method.'); throw new Error('Invalid password hash method.');
@@ -63,7 +63,7 @@ export async function convertToBcrypt (user, plainTextPassword) {
user.auth.local.salt = undefined; user.auth.local.salt = undefined;
user.auth.local.passwordHashMethod = 'bcrypt'; user.auth.local.passwordHashMethod = 'bcrypt';
user.auth.local.hashed_password = await bcryptHash(plainTextPassword); // eslint-disable-line camelcase user.auth.local.hashed_password = await bcryptHash(plainTextPassword); // eslint-disable-line camelcase, max-len
} }
// Returns the user if a valid password reset code is supplied, otherwise false // Returns the user if a valid password reset code is supplied, otherwise false

View File

@@ -11,9 +11,9 @@ import {
NotAuthorized, NotAuthorized,
NotFound, NotFound,
} from '../errors'; } from '../errors';
import payments from './payments'; import payments from './payments'; // eslint-disable-line import/no-cycle
import { model as User } from '../../models/user'; import { model as User } from '../../models/user'; // eslint-disable-line import/no-cycle
import { import { // eslint-disable-line import/no-cycle
model as Group, model as Group,
basicFields as basicGroupFields, basicFields as basicGroupFields,
} from '../../models/group'; } from '../../models/group';
@@ -52,14 +52,30 @@ api.constants = {
}; };
api.getTokenInfo = util.promisify(amzPayment.api.getTokenInfo).bind(amzPayment.api); api.getTokenInfo = util.promisify(amzPayment.api.getTokenInfo).bind(amzPayment.api);
api.createOrderReferenceId = util.promisify(amzPayment.offAmazonPayments.createOrderReferenceForId).bind(amzPayment.offAmazonPayments); api.createOrderReferenceId = util
api.setOrderReferenceDetails = util.promisify(amzPayment.offAmazonPayments.setOrderReferenceDetails).bind(amzPayment.offAmazonPayments); .promisify(amzPayment.offAmazonPayments.createOrderReferenceForId)
api.confirmOrderReference = util.promisify(amzPayment.offAmazonPayments.confirmOrderReference).bind(amzPayment.offAmazonPayments); .bind(amzPayment.offAmazonPayments);
api.closeOrderReference = util.promisify(amzPayment.offAmazonPayments.closeOrderReference).bind(amzPayment.offAmazonPayments); api.setOrderReferenceDetails = util
api.setBillingAgreementDetails = util.promisify(amzPayment.offAmazonPayments.setBillingAgreementDetails).bind(amzPayment.offAmazonPayments); .promisify(amzPayment.offAmazonPayments.setOrderReferenceDetails)
api.getBillingAgreementDetails = util.promisify(amzPayment.offAmazonPayments.getBillingAgreementDetails).bind(amzPayment.offAmazonPayments); .bind(amzPayment.offAmazonPayments);
api.confirmBillingAgreement = util.promisify(amzPayment.offAmazonPayments.confirmBillingAgreement).bind(amzPayment.offAmazonPayments); api.confirmOrderReference = util
api.closeBillingAgreement = util.promisify(amzPayment.offAmazonPayments.closeBillingAgreement).bind(amzPayment.offAmazonPayments); .promisify(amzPayment.offAmazonPayments.confirmOrderReference)
.bind(amzPayment.offAmazonPayments);
api.closeOrderReference = util
.promisify(amzPayment.offAmazonPayments.closeOrderReference)
.bind(amzPayment.offAmazonPayments);
api.setBillingAgreementDetails = util
.promisify(amzPayment.offAmazonPayments.setBillingAgreementDetails)
.bind(amzPayment.offAmazonPayments);
api.getBillingAgreementDetails = util
.promisify(amzPayment.offAmazonPayments.getBillingAgreementDetails)
.bind(amzPayment.offAmazonPayments);
api.confirmBillingAgreement = util
.promisify(amzPayment.offAmazonPayments.confirmBillingAgreement)
.bind(amzPayment.offAmazonPayments);
api.closeBillingAgreement = util
.promisify(amzPayment.offAmazonPayments.closeBillingAgreement)
.bind(amzPayment.offAmazonPayments);
api.authorizeOnBillingAgreement = function authorizeOnBillingAgreement (inputSet) { api.authorizeOnBillingAgreement = function authorizeOnBillingAgreement (inputSet) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -158,7 +174,9 @@ api.checkout = async function checkout (options = {}) {
}; };
if (gift) { if (gift) {
if (gift.type === this.constants.GIFT_TYPE_SUBSCRIPTION) method = this.constants.METHOD_CREATE_SUBSCRIPTION; if (gift.type === this.constants.GIFT_TYPE_SUBSCRIPTION) {
method = this.constants.METHOD_CREATE_SUBSCRIPTION;
}
gift.member = await User.findById(gift.uuid).exec(); gift.member = await User.findById(gift.uuid).exec();
data.gift = gift; data.gift = gift;
data.paymentMethod = this.constants.PAYMENT_METHOD_GIFT; data.paymentMethod = this.constants.PAYMENT_METHOD_GIFT;
@@ -217,8 +235,12 @@ api.cancelSubscription = async function cancelSubscription (options = {}) {
}).catch(err => err); }).catch(err => err);
const badBAStates = ['Canceled', 'Closed', 'Suspended']; const badBAStates = ['Canceled', 'Closed', 'Suspended'];
if (details && details.BillingAgreementDetails && details.BillingAgreementDetails.BillingAgreementStatus if (
&& badBAStates.indexOf(details.BillingAgreementDetails.BillingAgreementStatus.State) === -1) { details
&& details.BillingAgreementDetails
&& details.BillingAgreementDetails.BillingAgreementStatus
&& badBAStates.indexOf(details.BillingAgreementDetails.BillingAgreementStatus.State) === -1
) {
await this.closeBillingAgreement({ await this.closeBillingAgreement({
AmazonBillingAgreementId: billingAgreementId, AmazonBillingAgreementId: billingAgreementId,
}); });

View File

@@ -38,15 +38,16 @@ api.verifyGemPurchase = async function verifyGemPurchase (options) {
const isValidated = iap.isValidated(appleRes); const isValidated = iap.isValidated(appleRes);
if (!isValidated) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT); if (!isValidated) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT);
const purchaseDataList = iap.getPurchaseData(appleRes); const purchaseDataList = iap.getPurchaseData(appleRes);
if (purchaseDataList.length === 0) throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED); if (purchaseDataList.length === 0) {
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
}
let correctReceipt = false; let correctReceipt = false;
// Purchasing one item at a time (processing of await(s) below is sequential not parallel) // Purchasing one item at a time (processing of await(s) below is sequential not parallel)
for (const index in purchaseDataList) { for (const purchaseData of purchaseDataList) {
const purchaseData = purchaseDataList[index];
const token = purchaseData.transactionId; const token = purchaseData.transactionId;
const existingReceipt = await IapPurchaseReceipt.findOne({ // eslint-disable-line no-await-in-loop const existingReceipt = await IapPurchaseReceipt.findOne({ // eslint-disable-line no-await-in-loop, max-len
_id: token, _id: token,
}).exec(); }).exec();
@@ -59,7 +60,7 @@ api.verifyGemPurchase = async function verifyGemPurchase (options) {
}); });
let amount; let amount;
switch (purchaseData.productId) { switch (purchaseData.productId) { // eslint-disable-line default-case
case 'com.habitrpg.ios.Habitica.4gems': case 'com.habitrpg.ios.Habitica.4gems':
amount = 1; amount = 1;
break; break;
@@ -99,7 +100,7 @@ api.subscribe = async function subscribe (sku, user, receipt, headers, nextPayme
if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode')); if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode'));
let subCode; let subCode;
switch (sku) { switch (sku) { // eslint-disable-line default-case
case 'subscription1month': case 'subscription1month':
subCode = 'basic_earned'; subCode = 'basic_earned';
break; break;
@@ -122,13 +123,13 @@ api.subscribe = async function subscribe (sku, user, receipt, headers, nextPayme
if (!isValidated) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT); if (!isValidated) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT);
const purchaseDataList = iap.getPurchaseData(appleRes); const purchaseDataList = iap.getPurchaseData(appleRes);
if (purchaseDataList.length === 0) throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED); if (purchaseDataList.length === 0) {
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
}
let transactionId; let transactionId;
for (const index in purchaseDataList) { for (const purchaseData of purchaseDataList) {
const purchaseData = purchaseDataList[index];
const dateTerminated = new Date(Number(purchaseData.expirationDate)); const dateTerminated = new Date(Number(purchaseData.expirationDate));
if (purchaseData.productId === sku && dateTerminated > new Date()) { if (purchaseData.productId === sku && dateTerminated > new Date()) {
transactionId = purchaseData.transactionId; transactionId = purchaseData.transactionId;
@@ -142,7 +143,7 @@ api.subscribe = async function subscribe (sku, user, receipt, headers, nextPayme
}).exec(); }).exec();
if (existingUser) throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED); if (existingUser) throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
nextPaymentProcessing = nextPaymentProcessing || moment.utc().add({ days: 2 }); nextPaymentProcessing = nextPaymentProcessing || moment.utc().add({ days: 2 }); // eslint-disable-line max-len, no-param-reassign
await payments.createSubscription({ await payments.createSubscription({
user, user,
@@ -166,7 +167,7 @@ api.noRenewSubscribe = async function noRenewSubscribe (options) {
if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode')); if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode'));
let subCode; let subCode;
switch (sku) { switch (sku) { // eslint-disable-line default-case
case 'com.habitrpg.ios.habitica.norenew_subscription.1month': case 'com.habitrpg.ios.habitica.norenew_subscription.1month':
subCode = 'basic_earned'; subCode = 'basic_earned';
break; break;
@@ -189,13 +190,13 @@ api.noRenewSubscribe = async function noRenewSubscribe (options) {
if (!isValidated) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT); if (!isValidated) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT);
const purchaseDataList = iap.getPurchaseData(appleRes); const purchaseDataList = iap.getPurchaseData(appleRes);
if (purchaseDataList.length === 0) throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED); if (purchaseDataList.length === 0) {
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
}
let transactionId; let transactionId;
for (const index in purchaseDataList) { for (const purchaseData of purchaseDataList) {
const purchaseData = purchaseDataList[index];
const dateTerminated = new Date(Number(purchaseData.expirationDate)); const dateTerminated = new Date(Number(purchaseData.expirationDate));
if (purchaseData.productId === sku && dateTerminated > new Date()) { if (purchaseData.productId === sku && dateTerminated > new Date()) {
transactionId = purchaseData.transactionId; transactionId = purchaseData.transactionId;
@@ -204,7 +205,7 @@ api.noRenewSubscribe = async function noRenewSubscribe (options) {
} }
if (transactionId) { if (transactionId) {
const existingReceipt = await IapPurchaseReceipt.findOne({ // eslint-disable-line no-await-in-loop const existingReceipt = await IapPurchaseReceipt.findOne({
_id: transactionId, _id: transactionId,
}).exec(); }).exec();
if (existingReceipt) throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED); if (existingReceipt) throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
@@ -259,7 +260,10 @@ api.cancelSubscribe = async function cancelSubscribe (user, headers) {
if (dateTerminated > new Date()) throw new NotAuthorized(this.constants.RESPONSE_STILL_VALID); if (dateTerminated > new Date()) throw new NotAuthorized(this.constants.RESPONSE_STILL_VALID);
} catch (err) { } catch (err) {
// If we have an invalid receipt, cancel anyway // If we have an invalid receipt, cancel anyway
if (!err || !err.validatedData || err.validatedData.is_retryable === true || err.validatedData.status !== 21010) { if (
!err || !err.validatedData || err.validatedData.is_retryable === true
|| err.validatedData.status !== 21010
) {
throw err; throw err;
} }
} }

View File

@@ -1,5 +1,5 @@
import * as analytics from '../analyticsService'; import * as analytics from '../analyticsService';
import { import { // eslint-disable-line import/no-cycle
getUserInfo, getUserInfo,
sendTxn as txnEmail, sendTxn as txnEmail,
} from '../email'; } from '../email';
@@ -38,7 +38,10 @@ async function buyGemGift (data) {
} }
// Only send push notifications if sending to a user other than yourself // Only send push notifications if sending to a user other than yourself
if (data.gift.member._id !== data.user._id && data.gift.member.preferences.pushNotifications.giftedGems !== false) { if (
data.gift.member._id !== data.user._id
&& data.gift.member.preferences.pushNotifications.giftedGems !== false
) {
sendPushNotification( sendPushNotification(
data.gift.member, data.gift.member,
{ {
@@ -73,7 +76,7 @@ async function buyGems (data) {
const amt = getAmountForGems(data); const amt = getAmountForGems(data);
updateUserBalance(data, amt); updateUserBalance(data, amt);
data.user.purchased.txnCount++; data.user.purchased.txnCount += 1;
if (!data.gift) txnEmail(data.user, 'donation'); if (!data.gift) txnEmail(data.user, 'donation');
@@ -94,4 +97,4 @@ async function buyGems (data) {
await data.user.save(); await data.user.save();
} }
export { buyGems }; export { buyGems }; // eslint-disable-line import/prefer-default-export

View File

@@ -61,7 +61,7 @@ api.verifyGemPurchase = async function verifyGemPurchase (options) {
let amount; let amount;
switch (receiptObj.productId) { switch (receiptObj.productId) { // eslint-disable-line default-case
case 'com.habitrpg.android.habitica.iap.4gems': case 'com.habitrpg.android.habitica.iap.4gems':
amount = 1; amount = 1;
break; break;
@@ -89,10 +89,13 @@ api.verifyGemPurchase = async function verifyGemPurchase (options) {
return googleRes; return googleRes;
}; };
api.subscribe = async function subscribe (sku, user, receipt, signature, headers, nextPaymentProcessing = undefined) { api.subscribe = async function subscribe (
sku, user, receipt, signature,
headers, nextPaymentProcessing = undefined,
) {
if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode')); if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode'));
let subCode; let subCode;
switch (sku) { switch (sku) { // eslint-disable-line default-case
case 'com.habitrpg.android.habitica.subscription.1month': case 'com.habitrpg.android.habitica.subscription.1month':
subCode = 'basic_earned'; subCode = 'basic_earned';
break; break;
@@ -129,7 +132,7 @@ api.subscribe = async function subscribe (sku, user, receipt, signature, headers
const isValidated = iap.isValidated(googleRes); const isValidated = iap.isValidated(googleRes);
if (!isValidated) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT); if (!isValidated) throw new NotAuthorized(this.constants.RESPONSE_INVALID_RECEIPT);
nextPaymentProcessing = nextPaymentProcessing || moment.utc().add({ days: 2 }); nextPaymentProcessing = nextPaymentProcessing || moment.utc().add({ days: 2 }); // eslint-disable-line no-param-reassign, max-len
await payments.createSubscription({ await payments.createSubscription({
user, user,
@@ -148,7 +151,7 @@ api.noRenewSubscribe = async function noRenewSubscribe (options) {
} = options; } = options;
if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode')); if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode'));
let subCode; let subCode;
switch (sku) { switch (sku) { // eslint-disable-line default-case
case 'com.habitrpg.android.habitica.norenew_subscription.1month': case 'com.habitrpg.android.habitica.norenew_subscription.1month':
subCode = 'basic_earned'; subCode = 'basic_earned';
break; break;

View File

@@ -2,12 +2,12 @@ import nconf from 'nconf';
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { model as User } from '../../models/user'; import { model as User } from '../../models/user'; // eslint-disable-line import/no-cycle
import { import { // eslint-disable-line import/no-cycle
model as Group, model as Group,
basicFields as basicGroupFields, basicFields as basicGroupFields,
} from '../../models/group'; } from '../../models/group';
import { import { // eslint-disable-line import/no-cycle
getUserInfo, getUserInfo,
sendTxn as txnEmail, sendTxn as txnEmail,
} from '../email'; } from '../email';
@@ -61,8 +61,14 @@ async function addSubToGroupUser (member, group) {
// When changing customerIdsToIgnore or paymentMethodsToIgnore, the code blocks below for // When changing customerIdsToIgnore or paymentMethodsToIgnore, the code blocks below for
// the `group-member-join` email template will probably need to be changed. // the `group-member-join` email template will probably need to be changed.
const customerIdsToIgnore = [this.constants.GROUP_PLAN_CUSTOMER_ID, this.constants.UNLIMITED_CUSTOMER_ID]; const customerIdsToIgnore = [
const paymentMethodsToIgnore = [this.constants.GOOGLE_PAYMENT_METHOD, this.constants.IOS_PAYMENT_METHOD]; this.constants.GROUP_PLAN_CUSTOMER_ID,
this.constants.UNLIMITED_CUSTOMER_ID,
];
const paymentMethodsToIgnore = [
this.constants.GOOGLE_PAYMENT_METHOD,
this.constants.IOS_PAYMENT_METHOD,
];
let previousSubscriptionType = EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NONE; let previousSubscriptionType = EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_NONE;
const leader = await User.findById(group.leader).exec(); const leader = await User.findById(group.leader).exec();
@@ -96,7 +102,10 @@ async function addSubToGroupUser (member, group) {
const memberPlan = member.purchased.plan; const memberPlan = member.purchased.plan;
if (member.isSubscribed()) { if (member.isSubscribed()) {
const customerHasCancelledGroupPlan = memberPlan.customerId === this.constants.GROUP_PLAN_CUSTOMER_ID && !member.hasNotCancelled(); const customerHasCancelledGroupPlan = (
memberPlan.customerId === this.constants.GROUP_PLAN_CUSTOMER_ID
&& !member.hasNotCancelled()
);
const ignorePaymentPlan = paymentMethodsToIgnore.indexOf(memberPlan.paymentMethod) !== -1; const ignorePaymentPlan = paymentMethodsToIgnore.indexOf(memberPlan.paymentMethod) !== -1;
const ignoreCustomerId = customerIdsToIgnore.indexOf(memberPlan.customerId) !== -1; const ignoreCustomerId = customerIdsToIgnore.indexOf(memberPlan.customerId) !== -1;
@@ -108,7 +117,9 @@ async function addSubToGroupUser (member, group) {
{ name: 'PAYMENT_METHOD', content: memberPlan.paymentMethod }, { name: 'PAYMENT_METHOD', content: memberPlan.paymentMethod },
{ name: 'PURCHASED_PLAN', content: JSON.stringify(memberPlan) }, { name: 'PURCHASED_PLAN', content: JSON.stringify(memberPlan) },
{ name: 'ACTION_NEEDED', content: 'User has joined group plan and has been told to cancel their subscription then email us. Ensure they do that then give them free sub.' }, { name: 'ACTION_NEEDED', content: 'User has joined group plan and has been told to cancel their subscription then email us. Ensure they do that then give them free sub.' },
// TODO User won't get email instructions if they've opted out of all emails. See if we can make this email an exception and if not, report here whether they've opted out. // TODO User won't get email instructions if they've opted out of all emails.
// See if we can make this email an exception and if not,
// report here whether they've opted out.
]); ]);
} }

View File

@@ -1,14 +1,14 @@
import { import { // eslint-disable-line import/no-cycle
addSubscriptionToGroupUsers, addSubscriptionToGroupUsers,
addSubToGroupUser, addSubToGroupUser,
cancelGroupUsersSubscription, cancelGroupUsersSubscription,
cancelGroupSubscriptionForUser, cancelGroupSubscriptionForUser,
} from './groupPayments'; } from './groupPayments';
import { import { // eslint-disable-line import/no-cycle
createSubscription, createSubscription,
cancelSubscription, cancelSubscription,
} from './subscriptions'; } from './subscriptions';
import { import { // eslint-disable-line import/no-cycle
buyGems, buyGems,
} from './gems'; } from './gems';

View File

@@ -7,10 +7,10 @@ import ipn from 'paypal-ipn';
import paypal from 'paypal-rest-sdk'; import paypal from 'paypal-rest-sdk';
import cc from 'coupon-code'; import cc from 'coupon-code';
import shared from '../../../common'; import shared from '../../../common';
import payments from './payments'; import payments from './payments'; // eslint-disable-line import/no-cycle
import { model as Coupon } from '../../models/coupon'; import { model as Coupon } from '../../models/coupon';
import { model as User } from '../../models/user'; import { model as User } from '../../models/user'; // eslint-disable-line import/no-cycle
import { import { // eslint-disable-line import/no-cycle
model as Group, model as Group,
basicFields as basicGroupFields, basicFields as basicGroupFields,
} from '../../models/group'; } from '../../models/group';
@@ -24,8 +24,10 @@ import {
const BASE_URL = nconf.get('BASE_URL'); const BASE_URL = nconf.get('BASE_URL');
const { i18n } = shared; const { i18n } = shared;
// This is the plan.id for paypal subscriptions. You have to set up billing plans via their REST sdk (they don't have // This is the plan.id for paypal subscriptions.
// a web interface for billing-plan creation), see ./paypalBillingSetup.js for how. After the billing plan is created // You have to set up billing plans via their REST sdk (they don't have
// a web interface for billing-plan creation), see ./paypalBillingSetup.js for how.
// After the billing plan is created
// there, get it's plan.id and store it in config.json // there, get it's plan.id and store it in config.json
_.each(shared.content.subscriptionBlocks, block => { _.each(shared.content.subscriptionBlocks, block => {
block.paypalKey = nconf.get(`PAYPAL_BILLING_PLANS_${block.key}`); block.paypalKey = nconf.get(`PAYPAL_BILLING_PLANS_${block.key}`);
@@ -62,10 +64,14 @@ api.constants = {
api.paypalPaymentCreate = util.promisify(paypal.payment.create.bind(paypal.payment)); api.paypalPaymentCreate = util.promisify(paypal.payment.create.bind(paypal.payment));
api.paypalPaymentExecute = util.promisify(paypal.payment.execute.bind(paypal.payment)); api.paypalPaymentExecute = util.promisify(paypal.payment.execute.bind(paypal.payment));
api.paypalBillingAgreementCreate = util.promisify(paypal.billingAgreement.create.bind(paypal.billingAgreement)); api.paypalBillingAgreementCreate = util
api.paypalBillingAgreementExecute = util.promisify(paypal.billingAgreement.execute.bind(paypal.billingAgreement)); .promisify(paypal.billingAgreement.create.bind(paypal.billingAgreement));
api.paypalBillingAgreementGet = util.promisify(paypal.billingAgreement.get.bind(paypal.billingAgreement)); api.paypalBillingAgreementExecute = util
api.paypalBillingAgreementCancel = util.promisify(paypal.billingAgreement.cancel.bind(paypal.billingAgreement)); .promisify(paypal.billingAgreement.execute.bind(paypal.billingAgreement));
api.paypalBillingAgreementGet = util
.promisify(paypal.billingAgreement.get.bind(paypal.billingAgreement));
api.paypalBillingAgreementCancel = util
.promisify(paypal.billingAgreement.cancel.bind(paypal.billingAgreement));
api.ipnVerifyAsync = util.promisify(ipn.verify.bind(ipn)); api.ipnVerifyAsync = util.promisify(ipn.verify.bind(ipn));
@@ -272,7 +278,8 @@ api.ipn = async function ipnApi (options = {}) {
const user = await User.findOne({ 'purchased.plan.customerId': recurring_payment_id }).exec(); const user = await User.findOne({ 'purchased.plan.customerId': recurring_payment_id }).exec();
if (user) { if (user) {
// If the user has already cancelled the subscription, return // If the user has already cancelled the subscription, return
// Otherwise the subscription would be cancelled twice resulting in the loss of subscription credits // Otherwise the subscription would be cancelled twice
// resulting in the loss of subscription credits
if (user.hasCancelled()) return; if (user.hasCancelled()) return;
await payments.cancelSubscription({ user, paymentMethod: this.constants.PAYMENT_METHOD }); await payments.cancelSubscription({ user, paymentMethod: this.constants.PAYMENT_METHOD });
@@ -287,10 +294,14 @@ api.ipn = async function ipnApi (options = {}) {
if (group) { if (group) {
// If the group subscription has already been cancelled the subscription, return // If the group subscription has already been cancelled the subscription, return
// Otherwise the subscription would be cancelled twice resulting in the loss of subscription credits // Otherwise the subscription would be cancelled
// twice resulting in the loss of subscription credits
if (group.hasCancelled()) return; if (group.hasCancelled()) return;
await payments.cancelSubscription({ groupId: group._id, paymentMethod: this.constants.PAYMENT_METHOD }); await payments.cancelSubscription({
groupId: group._id,
paymentMethod: this.constants.PAYMENT_METHOD,
});
} }
}; };

View File

@@ -6,15 +6,15 @@ import {
NotAuthorized, NotAuthorized,
NotFound, NotFound,
} from '../errors'; } from '../errors';
import payments from './payments'; import payments from './payments'; // eslint-disable-line import/no-cycle
import { model as User } from '../../models/user'; import { model as User } from '../../models/user'; // eslint-disable-line import/no-cycle
import { import { // eslint-disable-line import/no-cycle
model as Group, model as Group,
basicFields as basicGroupFields, basicFields as basicGroupFields,
} from '../../models/group'; } from '../../models/group';
import shared from '../../../common'; import shared from '../../../common';
import stripeConstants from './stripe/constants'; import stripeConstants from './stripe/constants';
import { checkout } from './stripe/checkout'; import { checkout } from './stripe/checkout'; // eslint-disable-line import/no-cycle
import { getStripeApi, setStripeApi } from './stripe/api'; import { getStripeApi, setStripeApi } from './stripe/api';
const { i18n } = shared; const { i18n } = shared;
@@ -54,7 +54,8 @@ api.editSubscription = async function editSubscription (options, stripeInc) {
const { token, groupId, user } = options; const { token, groupId, user } = options;
let customerId; let customerId;
// @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? // @TODO: We need to mock this, but curently we don't have correct
// Dependency Injection. And the Stripe Api doesn't seem to be a singleton?
let stripeApi = getStripeApi(); let stripeApi = getStripeApi();
if (stripeInc) stripeApi = stripeInc; if (stripeInc) stripeApi = stripeInc;
@@ -81,7 +82,8 @@ api.editSubscription = async function editSubscription (options, stripeInc) {
if (!customerId) throw new NotAuthorized(i18n.t('missingSubscription')); if (!customerId) throw new NotAuthorized(i18n.t('missingSubscription'));
if (!token) throw new BadRequest('Missing req.body.id'); if (!token) throw new BadRequest('Missing req.body.id');
const subscriptions = await stripeApi.subscriptions.list({ customer: customerId }); // @TODO: Handle Stripe Error response // @TODO: Handle Stripe Error response
const subscriptions = await stripeApi.subscriptions.list({ customer: customerId });
const subscriptionId = subscriptions.data[0].id; const subscriptionId = subscriptions.data[0].id;
await stripeApi.subscriptions.update(subscriptionId, { card: token }); await stripeApi.subscriptions.update(subscriptionId, { card: token });
}; };
@@ -100,7 +102,8 @@ api.cancelSubscription = async function cancelSubscription (options, stripeInc)
const { groupId, user, cancellationReason } = options; const { groupId, user, cancellationReason } = options;
let customerId; let customerId;
// @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? // @TODO: We need to mock this, but curently we don't have correct
// Dependency Injection. And the Stripe Api doesn't seem to be a singleton?
let stripeApi = getStripeApi(); let stripeApi = getStripeApi();
if (stripeInc) stripeApi = stripeInc; if (stripeInc) stripeApi = stripeInc;
@@ -133,7 +136,7 @@ api.cancelSubscription = async function cancelSubscription (options, stripeInc)
if (customer && (customer.subscription || customer.subscriptions)) { if (customer && (customer.subscription || customer.subscriptions)) {
let { subscription } = customer; let { subscription } = customer;
if (!subscription && customer.subscriptions) { if (!subscription && customer.subscriptions) {
subscription = customer.subscriptions.data[0]; subscription = [customer.subscriptions.data];
} }
await stripeApi.customers.del(customerId); await stripeApi.customers.del(customerId);
@@ -178,7 +181,8 @@ api.chargeForAdditionalGroupMember = async function chargeForAdditionalGroupMemb
api.handleWebhooks = async function handleWebhooks (options, stripeInc) { api.handleWebhooks = async function handleWebhooks (options, stripeInc) {
const { requestBody } = options; const { requestBody } = options;
// @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? // @TODO: We need to mock this, but curently we don't have correct
// Dependency Injection. And the Stripe Api doesn't seem to be a singleton?
let stripeApi = getStripeApi(); let stripeApi = getStripeApi();
if (stripeInc) stripeApi = stripeInc; if (stripeInc) stripeApi = stripeInc;

View File

@@ -1,9 +1,9 @@
import cc from 'coupon-code'; import cc from 'coupon-code';
import { getStripeApi } from './api'; import { getStripeApi } from './api';
import { model as User } from '../../../models/user'; import { model as User } from '../../../models/user'; // eslint-disable-line import/no-cycle
import { model as Coupon } from '../../../models/coupon'; import { model as Coupon } from '../../../models/coupon';
import { import { // eslint-disable-line import/no-cycle
model as Group, model as Group,
basicFields as basicGroupFields, basicFields as basicGroupFields,
} from '../../../models/group'; } from '../../../models/group';
@@ -12,7 +12,7 @@ import {
BadRequest, BadRequest,
NotAuthorized, NotAuthorized,
} from '../../errors'; } from '../../errors';
import payments from '../payments'; import payments from '../payments'; // eslint-disable-line import/no-cycle
import stripeConstants from './constants'; import stripeConstants from './constants';
function getGiftAmount (gift) { function getGiftAmount (gift) {
@@ -24,7 +24,7 @@ function getGiftAmount (gift) {
throw new BadRequest(shared.i18n.t('badAmountOfGemsToPurchase')); throw new BadRequest(shared.i18n.t('badAmountOfGemsToPurchase'));
} }
return `${gift.gems.amount / 4 * 100}`; return `${(gift.gems.amount / 4) * 100}`;
} }
async function buyGems (gift, user, token, stripeApi) { async function buyGems (gift, user, token, stripeApi) {
@@ -50,7 +50,8 @@ async function buyGems (gift, user, token, stripeApi) {
async function buySubscription (sub, coupon, email, user, token, groupId, stripeApi) { async function buySubscription (sub, coupon, email, user, token, groupId, stripeApi) {
if (sub.discount) { if (sub.discount) {
if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired')); if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired'));
coupon = await Coupon.findOne({ _id: cc.validate(coupon), event: sub.key }).exec(); coupon = await Coupon // eslint-disable-line no-param-reassign
.findOne({ _id: cc.validate(coupon), event: sub.key }).exec();
if (!coupon) throw new BadRequest(shared.i18n.t('invalidCoupon')); if (!coupon) throw new BadRequest(shared.i18n.t('invalidCoupon'));
} }
@@ -110,7 +111,8 @@ async function checkout (options, stripeInc) {
let response; let response;
let subscriptionId; let subscriptionId;
// @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? // @TODO: We need to mock this, but curently we don't have correct
// Dependency Injection. And the Stripe Api doesn't seem to be a singleton?
let stripeApi = getStripeApi(); let stripeApi = getStripeApi();
if (stripeInc) stripeApi = stripeInc; if (stripeInc) stripeApi = stripeInc;
@@ -122,7 +124,9 @@ async function checkout (options, stripeInc) {
} }
if (sub) { if (sub) {
const { subId, subResponse } = await buySubscription(sub, coupon, email, user, token, groupId, stripeApi); const { subId, subResponse } = await buySubscription(
sub, coupon, email, user, token, groupId, stripeApi,
);
subscriptionId = subId; subscriptionId = subId;
response = subResponse; response = subResponse;
} else { } else {
@@ -145,4 +149,4 @@ async function checkout (options, stripeInc) {
await applyGemPayment(user, response, gift); await applyGemPayment(user, response, gift);
} }
export { checkout }; export { checkout }; // eslint-disable-line import/prefer-default-export

View File

@@ -2,12 +2,12 @@ import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import * as analytics from '../analyticsService'; import * as analytics from '../analyticsService';
import * as slack from '../slack'; import * as slack from '../slack'; // eslint-disable-line import/no-cycle
import { import { // eslint-disable-line import/no-cycle
getUserInfo, getUserInfo,
sendTxn as txnEmail, sendTxn as txnEmail,
} from '../email'; } from '../email';
import { import { // eslint-disable-line import/no-cycle
model as Group, model as Group,
basicFields as basicGroupFields, basicFields as basicGroupFields,
} from '../../models/group'; } from '../../models/group';
@@ -50,11 +50,12 @@ function _dateDiff (earlyDate, lateDate) {
async function createSubscription (data) { async function createSubscription (data) {
let recipient = data.gift ? data.gift.member : data.user; let recipient = data.gift ? data.gift.member : data.user;
const block = shared.content.subscriptionBlocks[data.gift ? data.gift.subscription.key : data.sub.key]; const block = shared.content.subscriptionBlocks[data.gift
? data.gift.subscription.key
: data.sub.key];
const autoRenews = data.autoRenews !== undefined ? data.autoRenews : true; const autoRenews = data.autoRenews !== undefined ? data.autoRenews : true;
const months = Number(block.months); const months = Number(block.months);
const today = new Date(); const today = new Date();
let plan;
let group; let group;
let groupId; let groupId;
let itemPurchased = 'Subscription'; let itemPurchased = 'Subscription';
@@ -86,7 +87,7 @@ async function createSubscription (data) {
await this.addSubscriptionToGroupUsers(group); await this.addSubscriptionToGroupUsers(group);
} }
plan = recipient.purchased.plan; const { plan } = recipient.purchased;
if (data.gift || !autoRenews) { if (data.gift || !autoRenews) {
if (plan.customerId && !plan.dateTerminated) { // User has active plan if (plan.customerId && !plan.dateTerminated) { // User has active plan
@@ -165,7 +166,7 @@ async function createSubscription (data) {
headers: data.headers, headers: data.headers,
}); });
if (!group) data.user.purchased.txnCount++; if (!group) data.user.purchased.txnCount += 1;
if (data.gift) { if (data.gift) {
const byUserName = getUserInfo(data.user, ['name']).name; const byUserName = getUserInfo(data.user, ['name']).name;
@@ -200,7 +201,8 @@ async function createSubscription (data) {
]); ]);
} }
if (data.gift.member._id !== data.user._id) { // Only send push notifications if sending to a user other than yourself // Only send push notifications if sending to a user other than yourself
if (data.gift.member._id !== data.user._id) {
if (data.gift.member.preferences.pushNotifications.giftedSubscription !== false) { if (data.gift.member.preferences.pushNotifications.giftedSubscription !== false) {
sendPushNotification(data.gift.member, sendPushNotification(data.gift.member,
{ {
@@ -293,7 +295,8 @@ async function cancelSubscription (data) {
.add({ days: extraDays }) .add({ days: extraDays })
.toDate(); .toDate();
plan.extraMonths = 0; // clear extra time. If they subscribe again, it'll be recalculated from p.dateTerminated // clear extra time. If they subscribe again, it'll be recalculated from p.dateTerminated
plan.extraMonths = 0;
if (group) { if (group) {
await group.save(); await group.save();

View File

@@ -15,7 +15,11 @@ function _aggregate (history, aggregateBy, timezoneOffset, dayStart) {
const entries = keyEntryPair[1]; // 1 is entry, 0 is key const entries = keyEntryPair[1]; // 1 is entry, 0 is key
return { return {
date: Number(entries[0].date), date: Number(entries[0].date),
value: _.reduce(entries, (previousValue, entry) => previousValue + entry.value, 0) / entries.length, value: _.reduce(
entries,
(previousValue, entry) => previousValue + entry.value,
0,
) / entries.length,
}; };
}) })
.value(); .value();

View File

@@ -36,7 +36,7 @@ function sendNotification (user, details = {}) {
payload.identifier = details.identifier; payload.identifier = details.identifier;
_.each(pushDevices, pushDevice => { _.each(pushDevices, pushDevice => {
switch (pushDevice.type) { switch (pushDevice.type) { // eslint-disable-line default-case
case 'android': case 'android':
// Required for fcm to be received in background // Required for fcm to be received in background
payload.title = details.title; payload.title = details.title;
@@ -82,5 +82,5 @@ function sendNotification (user, details = {}) {
} }
export { export {
sendNotification, sendNotification, // eslint-disable-line import/prefer-default-export
}; };

View File

@@ -12,8 +12,9 @@ const noop = (req, res, next) => next();
export function readController (router, controller, overrides = []) { export function readController (router, controller, overrides = []) {
_.each(controller, action => { _.each(controller, action => {
let { let {
method, url, middlewares = [], handler, method,
} = action; } = action;
const { url, middlewares = [], handler } = action;
// Allow to specify a list of routes (METHOD + URL) to skip // Allow to specify a list of routes (METHOD + URL) to skip
if (overrides.indexOf(`${method}-${url}`) !== -1) return; if (overrides.indexOf(`${method}-${url}`) !== -1) return;
@@ -30,7 +31,8 @@ export function readController (router, controller, overrides = []) {
const middlewaresToAdd = [getUserLanguage]; const middlewaresToAdd = [getUserLanguage];
if (action.noLanguage !== true) { if (action.noLanguage !== true) {
if (authMiddlewareIndex !== -1) { // the user will be authenticated, getUserLanguage after authentication // the user will be authenticated, getUserLanguage after authentication
if (authMiddlewareIndex !== -1) {
if (authMiddlewareIndex === middlewares.length - 1) { if (authMiddlewareIndex === middlewares.length - 1) {
middlewares.push(...middlewaresToAdd); middlewares.push(...middlewaresToAdd);
} else { } else {
@@ -55,7 +57,7 @@ export function walkControllers (router, filePath, overrides) {
if (!fs.statSync(filePath + fileName).isFile()) { if (!fs.statSync(filePath + fileName).isFile()) {
walkControllers(router, `${filePath}${fileName}/`, overrides); walkControllers(router, `${filePath}${fileName}/`, overrides);
} else if (fileName.match(/\.js$/)) { } else if (fileName.match(/\.js$/)) {
const controller = require(filePath + fileName).default; // eslint-disable-line global-require const controller = require(filePath + fileName).default; // eslint-disable-line global-require, import/no-dynamic-require, max-len
readController(router, controller, overrides); readController(router, controller, overrides);
} }
}); });

View File

@@ -14,7 +14,8 @@ passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((obj, done) => done(null, obj)); passport.deserializeUser((obj, done) => done(null, obj));
// TODO remove? // TODO remove?
// This auth strategy is no longer used. It's just kept around for auth.js#loginFacebook() (passport._strategies.facebook.userProfile) // This auth strategy is no longer used.
// It's just kept around for auth.js#loginFacebook() (passport._strategies.facebook.userProfile)
// The proper fix would be to move to a general OAuth module simply to verify accessTokens // The proper fix would be to move to a general OAuth module simply to verify accessTokens
passport.use(new FacebookStrategy({ passport.use(new FacebookStrategy({
clientID: nconf.get('FACEBOOK_KEY'), clientID: nconf.get('FACEBOOK_KEY'),

View File

@@ -3,7 +3,7 @@ import { IncomingWebhook } from '@slack/client';
import nconf from 'nconf'; import nconf from 'nconf';
import moment from 'moment'; import moment from 'moment';
import logger from './logger'; import logger from './logger';
import { TAVERN_ID } from '../models/group'; import { TAVERN_ID } from '../models/group'; // eslint-disable-line import/no-cycle
const SLACK_FLAGGING_URL = nconf.get('SLACK_FLAGGING_URL'); const SLACK_FLAGGING_URL = nconf.get('SLACK_FLAGGING_URL');
const SLACK_FLAGGING_FOOTER_LINK = nconf.get('SLACK_FLAGGING_FOOTER_LINK'); const SLACK_FLAGGING_FOOTER_LINK = nconf.get('SLACK_FLAGGING_FOOTER_LINK');
@@ -24,11 +24,12 @@ try {
logger.error(err); logger.error(err);
if (!IS_PRODUCTION) { if (!IS_PRODUCTION) {
flagSlack = subscriptionSlack = { subscriptionSlack = {
send (data) { send (data) {
logger.info('Data sent to slack', data); logger.info('Data sent to slack', data);
}, },
}; };
flagSlack = subscriptionSlack;
} }
} }
@@ -116,7 +117,6 @@ function sendInboxFlagNotification ({
return; return;
} }
const titleLink = ''; const titleLink = '';
let authorName;
const title = `Flag in ${flagger.profile.name}'s Inbox`; const title = `Flag in ${flagger.profile.name}'s Inbox`;
let text = `${flagger.profile.name} (${flagger.id}; language: ${flagger.preferences.language}) flagged a PM`; let text = `${flagger.profile.name} (${flagger.id}; language: ${flagger.preferences.language}) flagged a PM`;
const footer = ''; const footer = '';
@@ -150,7 +150,7 @@ function sendInboxFlagNotification ({
recipient = flaggerFormat; recipient = flaggerFormat;
} }
authorName = `${sender} wrote this message to ${recipient}.`; const authorName = `${sender} wrote this message to ${recipient}.`;
flagSlack.send({ flagSlack.send({
text, text,
@@ -203,18 +203,17 @@ function sendShadowMutedPostNotification ({
if (SKIP_FLAG_METHODS) { if (SKIP_FLAG_METHODS) {
return; return;
} }
let titleLink;
let authorName;
const title = `Shadow-Muted Post in ${group.name}`; const title = `Shadow-Muted Post in ${group.name}`;
const text = `@${author.auth.local.username} / ${author.profile.name} posted while shadow-muted`; const text = `@${author.auth.local.username} / ${author.profile.name} posted while shadow-muted`;
let titleLink;
if (group.id === TAVERN_ID) { if (group.id === TAVERN_ID) {
titleLink = `${BASE_URL}/groups/tavern`; titleLink = `${BASE_URL}/groups/tavern`;
} else { } else {
titleLink = `${BASE_URL}/groups/guild/${group.id}`; titleLink = `${BASE_URL}/groups/guild/${group.id}`;
} }
authorName = formatUser({ const authorName = formatUser({
name: author.auth.local.username, name: author.auth.local.username,
displayName: author.profile.name, displayName: author.profile.name,
email: authorEmail, email: authorEmail,
@@ -246,11 +245,11 @@ function sendSlurNotification ({
if (SKIP_FLAG_METHODS) { if (SKIP_FLAG_METHODS) {
return; return;
} }
let titleLink;
let authorName;
let title = `Slur in ${group.name}`;
const text = `${author.profile.name} (${author._id}) tried to post a slur`; const text = `${author.profile.name} (${author._id}) tried to post a slur`;
let titleLink;
let title = `Slur in ${group.name}`;
if (group.id === TAVERN_ID) { if (group.id === TAVERN_ID) {
titleLink = `${BASE_URL}/groups/tavern`; titleLink = `${BASE_URL}/groups/tavern`;
} else if (group.privacy === 'public') { } else if (group.privacy === 'public') {
@@ -259,7 +258,7 @@ function sendSlurNotification ({
title += ` - (${group.privacy} ${group.type})`; title += ` - (${group.privacy} ${group.type})`;
} }
authorName = formatUser({ const authorName = formatUser({
name: author.auth.local.username, name: author.auth.local.username,
displayName: author.profile.name, displayName: author.profile.name,
email: authorEmail, email: authorEmail,

View File

@@ -73,9 +73,10 @@ async function castSelfSpell (req, user, spell, quantity = 1) {
async function castPartySpell (req, party, partyMembers, user, spell, quantity = 1) { async function castPartySpell (req, party, partyMembers, user, spell, quantity = 1) {
if (!party) { if (!party) {
partyMembers = [user]; // Act as solo party // Act as solo party
partyMembers = [user]; // eslint-disable-line no-param-reassign
} else { } else {
partyMembers = await User partyMembers = await User // eslint-disable-line no-param-reassign
.find({ .find({
'party._id': party._id, 'party._id': party._id,
_id: { $ne: user._id }, // add separately _id: { $ne: user._id }, // add separately
@@ -96,11 +97,11 @@ async function castPartySpell (req, party, partyMembers, user, spell, quantity =
async function castUserSpell (res, req, party, partyMembers, targetId, user, spell, quantity = 1) { async function castUserSpell (res, req, party, partyMembers, targetId, user, spell, quantity = 1) {
if (!party && (!targetId || user._id === targetId)) { if (!party && (!targetId || user._id === targetId)) {
partyMembers = user; partyMembers = user; // eslint-disable-line no-param-reassign
} else { } else {
if (!targetId) throw new BadRequest(res.t('targetIdUUID')); if (!targetId) throw new BadRequest(res.t('targetIdUUID'));
if (!party) throw new NotFound(res.t('partyNotFound')); if (!party) throw new NotFound(res.t('partyNotFound'));
partyMembers = await User partyMembers = await User // eslint-disable-line no-param-reassign
.findOne({ _id: targetId, 'party._id': party._id }) .findOne({ _id: targetId, 'party._id': party._id })
.select(partyMembersFields) .select(partyMembersFields)
.exec(); .exec();
@@ -177,14 +178,21 @@ async function castSpell (req, res, { isV3 = false }) {
if (targetType === 'party') { if (targetType === 'party') {
partyMembers = await castPartySpell(req, party, partyMembers, user, spell, quantity); partyMembers = await castPartySpell(req, party, partyMembers, user, spell, quantity);
} else { } else {
partyMembers = await castUserSpell(res, req, party, partyMembers, targetId, user, spell, quantity); partyMembers = await castUserSpell(
res, req, party, partyMembers,
targetId, user, spell, quantity,
);
} }
let partyMembersRes = Array.isArray(partyMembers) ? partyMembers : [partyMembers]; let partyMembersRes = Array.isArray(partyMembers) ? partyMembers : [partyMembers];
// Only return some fields. // Only return some fields.
// We can't just return the selected fields because they're private // We can't just return the selected fields because they're private
partyMembersRes = partyMembersRes.map(partyMember => common.pickDeep(partyMember.toJSON(), common.$w(partyMembersPublicFields))); partyMembersRes = partyMembersRes
.map(partyMember => common.pickDeep(
partyMember.toJSON(),
common.$w(partyMembersPublicFields),
));
let userToJson = user; let userToJson = user;
if (isV3) userToJson = await userToJson.toJSONWithInbox(); if (isV3) userToJson = await userToJson.toJSONWithInbox();

View File

@@ -1,6 +1,6 @@
export function removePunctuationFromString (str) { export function removePunctuationFromString (str) {
return str.replace(/[.,\/#!@$%\^&;:{}=\-_`~()]/g, ' '); return str.replace(/[.,/#!@$%^&;:{}=\-_`~()]/g, ' ');
} }
export function getMatchesByWordArray (str, wordsToMatch) { export function getMatchesByWordArray (str, wordsToMatch) {

View File

@@ -33,7 +33,12 @@ export function setNextDue (task, user, dueDateOption) {
dateTaskIsDue = moment(dueDateOption); dateTaskIsDue = moment(dueDateOption);
// If not time is supplied. Let's assume we want start of Custom Day Start day. // If not time is supplied. Let's assume we want start of Custom Day Start day.
if (dateTaskIsDue.hour() === 0 && dateTaskIsDue.minute() === 0 && dateTaskIsDue.second() === 0 && dateTaskIsDue.millisecond() === 0) { if (
dateTaskIsDue.hour() === 0
&& dateTaskIsDue.minute() === 0
&& dateTaskIsDue.second() === 0
&& dateTaskIsDue.millisecond() === 0
) {
dateTaskIsDue.add(user.preferences.timezoneOffset, 'minutes'); dateTaskIsDue.add(user.preferences.timezoneOffset, 'minutes');
dateTaskIsDue.add(user.preferences.dayStart, 'hours'); dateTaskIsDue.add(user.preferences.dayStart, 'hours');
} }
@@ -103,7 +108,9 @@ export async function createTasks (req, res, options = {}) {
setNextDue(newTask, user); setNextDue(newTask, user);
// Validate that the task is valid and throw if it isn't // Validate that the task is valid and throw if it isn't
// otherwise since we're saving user/challenge/group and task in parallel it could save the user/challenge/group with a tasksOrder that doens't match reality // otherwise since we're saving user/challenge/group
// and task in parallel it could save the user/challenge/group
// with a tasksOrder that doens't match reality
const validationErrors = newTask.validateSync(); const validationErrors = newTask.validateSync();
if (validationErrors) throw validationErrors; if (validationErrors) throw validationErrors;
@@ -116,7 +123,7 @@ export async function createTasks (req, res, options = {}) {
// Push all task ids // Push all task ids
const taskOrderUpdateQuery = { $push: {} }; const taskOrderUpdateQuery = { $push: {} };
for (const taskType in taskOrderToAdd) { for (const taskType of Object.keys(taskOrderToAdd)) {
taskOrderUpdateQuery.$push[`tasksOrder.${taskType}`] = { taskOrderUpdateQuery.$push[`tasksOrder.${taskType}`] = {
$each: taskOrderToAdd[taskType], $each: taskOrderToAdd[taskType],
$position: 0, $position: 0,
@@ -128,7 +135,9 @@ export async function createTasks (req, res, options = {}) {
// tasks with aliases need to be validated asynchronously // tasks with aliases need to be validated asynchronously
await _validateTaskAlias(toSave, res); await _validateTaskAlias(toSave, res);
toSave = toSave.map(task => task.save({ // If all tasks are valid (this is why it's not in the previous .map()), save everything, withough running validation again // If all tasks are valid (this is why it's not in the previous .map()),
// save everything, withough running validation again
toSave = toSave.map(task => task.save({
validateBeforeSave: false, validateBeforeSave: false,
})); }));
@@ -239,7 +248,6 @@ export async function getTasks (req, res, options = {}) {
} }
// Takes a Task document and return a plain object of attributes that can be synced to the user // Takes a Task document and return a plain object of attributes that can be synced to the user
export function syncableAttrs (task) { export function syncableAttrs (task) {
const t = task.toObject(); // lodash doesn't seem to like _.omit on Document const t = task.toObject(); // lodash doesn't seem to like _.omit on Document
// only sync/compare important attrs // only sync/compare important attrs

View File

@@ -2,7 +2,7 @@ import got from 'got';
import { isURL } from 'validator'; import { isURL } from 'validator';
import nconf from 'nconf'; import nconf from 'nconf';
import logger from './logger'; import logger from './logger';
import { import { // eslint-disable-line import/no-cycle
model as User, model as User,
} from '../models/user'; } from '../models/user';

View File

@@ -16,9 +16,9 @@ function getUserFields (options, req) {
// Must be an array // Must be an array
if (options.userFieldsToExclude) { if (options.userFieldsToExclude) {
return options.userFieldsToExclude return options.userFieldsToExclude
.filter(field => !USER_FIELDS_ALWAYS_LOADED.find(fieldToInclude => field.startsWith(fieldToInclude))) .filter(field => !USER_FIELDS_ALWAYS_LOADED
.map(field => `-${field}`, // -${field} means exclude ${field} in mongodb .find(fieldToInclude => field.startsWith(fieldToInclude)))
) .map(field => `-${field}`) // -${field} means exclude ${field} in mongodb
.join(' '); .join(' ');
} }
@@ -26,7 +26,8 @@ function getUserFields (options, req) {
return options.userFieldsToInclude.concat(USER_FIELDS_ALWAYS_LOADED).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 // Allows GET requests to /user to specify a list
// of user fields to return instead of the entire doc
const urlPath = url.parse(req.url).pathname; const urlPath = url.parse(req.url).pathname;
const { userFields } = req.query; const { userFields } = req.query;
if (!userFields || urlPath !== '/user') return ''; if (!userFields || urlPath !== '/user') return '';

View File

@@ -8,12 +8,14 @@ import { recoverCron, cron } from '../libs/cron';
const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime(); const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime();
async function checkForActiveCron (user, now) { async function checkForActiveCron (user, now) {
// set _cronSignature to current time in ms since epoch time so we can make sure to wait at least CRONT_TIMEOUT_WAIT before attempting another cron // set _cronSignature to current time in ms since epoch time
// so we can make sure to wait at least CRONT_TIMEOUT_WAIT before attempting another cron
const _cronSignature = now.getTime(); const _cronSignature = now.getTime();
// Calculate how long ago cron must have been attempted to try again // Calculate how long ago cron must have been attempted to try again
const cronRetryTime = _cronSignature - CRON_TIMEOUT_WAIT; const cronRetryTime = _cronSignature - CRON_TIMEOUT_WAIT;
// To avoid double cron we first set _cronSignature and then check that it's not changed while processing // To avoid double cron we first set _cronSignature
// and then check that it's not changed while processing
const userUpdateResult = await User.update({ const userUpdateResult = await User.update({
_id: user._id, _id: user._id,
$or: [ // Make sure last cron was successful or failed before cronRetryTime $or: [ // Make sure last cron was successful or failed before cronRetryTime
@@ -60,7 +62,8 @@ async function cronAsync (req, res) {
try { try {
await checkForActiveCron(user, now); await checkForActiveCron(user, now);
user = res.locals.user = await User.findOne({ _id: user._id }).exec(); user = await User.findOne({ _id: user._id }).exec();
res.locals.user = user;
const { daysMissed, timezoneOffsetFromUserPrefs } = user.daysUserHasMissed(now, req); const { daysMissed, timezoneOffsetFromUserPrefs } = user.daysUserHasMissed(now, req);
await updateLastCron(user, now); await updateLastCron(user, now);
@@ -86,7 +89,13 @@ async function cronAsync (req, res) {
// Run cron // Run cron
const progress = cron({ const progress = cron({
user, tasksByType, now, daysMissed, analytics, timezoneOffsetFromUserPrefs, headers: req.headers, user,
tasksByType,
now,
daysMissed,
analytics,
timezoneOffsetFromUserPrefs,
headers: req.headers,
}); });
// Clear old completed todos - 30 days for free users, 90 for subscribers // Clear old completed todos - 30 days for free users, 90 for subscribers
@@ -117,7 +126,7 @@ async function cronAsync (req, res) {
}).exec(); }).exec();
if (groupTask) { if (groupTask) {
let delta = Math.pow(0.9747, task.value) * -1; let delta = (0.9747 ** task.value) * -1;
if (groupTask.group.assignedUsers) delta /= groupTask.group.assignedUsers.length; if (groupTask.group.assignedUsers) delta /= groupTask.group.assignedUsers.length;
await groupTask.scoreChallengeTask(delta, 'down'); await groupTask.scoreChallengeTask(delta, 'down');
} }
@@ -143,7 +152,8 @@ async function cronAsync (req, res) {
// If cron was aborted for a race condition try to recover from it // If cron was aborted for a race condition try to recover from it
if (err.message === 'CRON_ALREADY_RUNNING') { if (err.message === 'CRON_ALREADY_RUNNING') {
// Recovering after abort, wait 300ms and reload user // Recovering after abort, wait 300ms and reload user
// do it for max 5 times then reset _cronSignature so that it doesn't prevent cron from running // do it for max 5 times then reset _cronSignature
// so that it doesn't prevent cron from running
// at the next request // at the next request
const recoveryStatus = { const recoveryStatus = {
times: 0, times: 0,
@@ -151,7 +161,8 @@ async function cronAsync (req, res) {
await recoverCron(recoveryStatus, res.locals); await recoverCron(recoveryStatus, res.locals);
} else { } else {
// For any other error make sure to reset _cronSignature so that it doesn't prevent cron from running // For any other error make sure to reset _cronSignature
// so that it doesn't prevent cron from running
// at the next request // at the next request
await User.update({ await User.update({
_id: user._id, _id: user._id,
@@ -161,6 +172,8 @@ async function cronAsync (req, res) {
throw err; // re-throw the original error throw err; // re-throw the original error
} }
return null;
} }
} }

View File

@@ -10,7 +10,7 @@ export function ensureAdmin (req, res, next) {
return next(new NotAuthorized(res.t('noAdminAccess'))); return next(new NotAuthorized(res.t('noAdminAccess')));
} }
next(); return next();
} }
export function ensureSudo (req, res, next) { export function ensureSudo (req, res, next) {
@@ -20,5 +20,5 @@ export function ensureSudo (req, res, next) {
return next(new NotAuthorized(apiError('noSudoAccess'))); return next(new NotAuthorized(apiError('noSudoAccess')));
} }
next(); return next();
} }

View File

@@ -56,8 +56,8 @@ function _getFromUser (user, req) {
} }
export function attachTranslateFunction (req, res, next) { export function attachTranslateFunction (req, res, next) {
res.t = function reqTranslation () { res.t = function reqTranslation (...args) {
return i18n.t(...arguments, req.language); return i18n.t(...args, req.language);
}; };
next(); next();
@@ -67,7 +67,9 @@ export function getUserLanguage (req, res, next) {
if (req.query.lang) { // In case the language is specified in the request url, use it if (req.query.lang) { // In case the language is specified in the request url, use it
req.language = translations[req.query.lang] ? req.query.lang : 'en'; req.language = translations[req.query.lang] ? req.query.lang : 'en';
return next(); return next();
} if (req.locals && req.locals.user) { // If the request is authenticated, use the user's preferred language
// If the request is authenticated, use the user's preferred language
} if (req.locals && req.locals.user) {
req.language = _getFromUser(req.locals.user, req); req.language = _getFromUser(req.locals.user, req);
return next(); return next();
} if (req.session && req.session.userId) { // Same thing if the user has a valid session } if (req.session && req.session.userId) { // Same thing if the user has a valid session

View File

@@ -6,7 +6,7 @@ const MAINTENANCE_MODE = nconf.get('MAINTENANCE_MODE');
export default function maintenanceMode (req, res, next) { export default function maintenanceMode (req, res, next) {
if (MAINTENANCE_MODE !== 'true') return next(); if (MAINTENANCE_MODE !== 'true') return next();
getUserLanguage(req, res, err => { return getUserLanguage(req, res, err => {
if (err) return next(err); if (err) return next(err);
const pageVariables = { const pageVariables = {

View File

@@ -14,7 +14,8 @@ const TOP_LEVEL_ROUTES = [
'/export', '/export',
'/email', '/email',
'/qr-code', '/qr-code',
// logout, old-client and /static/user/auth/local/reset-password-set-new-one don't need the not found // logout, old-client
// and /static/user/auth/local/reset-password-set-new-one don't need the not found
// handler because they don't have any child route // handler because they don't have any child route
]; ];

View File

@@ -21,11 +21,14 @@ function isHTTP (req) {
export function forceSSL (req, res, next) { export function forceSSL (req, res, next) {
const { skipSSLCheck } = req.query; const { skipSSLCheck } = req.query;
if (isHTTP(req) && (!SKIP_SSL_CHECK_KEY || !skipSSLCheck || skipSSLCheck !== SKIP_SSL_CHECK_KEY)) { if (
isHTTP(req)
&& (!SKIP_SSL_CHECK_KEY || !skipSSLCheck || skipSSLCheck !== SKIP_SSL_CHECK_KEY)
) {
return res.redirect(BASE_URL + req.originalUrl); return res.redirect(BASE_URL + req.originalUrl);
} }
next(); return next();
} }
// Redirect to habitica for non-api urls // Redirect to habitica for non-api urls
@@ -39,5 +42,5 @@ export function forceHabitica (req, res, next) {
return res.redirect(301, BASE_URL + req.url); return res.redirect(301, BASE_URL + req.url);
} }
next(); return next();
} }

View File

@@ -4,13 +4,13 @@ import _ from 'lodash';
import { TaskQueue } from 'cwait'; import { TaskQueue } from 'cwait';
import baseModel from '../libs/baseModel'; import baseModel from '../libs/baseModel';
import * as Tasks from './task'; import * as Tasks from './task';
import { model as User } from './user'; import { model as User } from './user'; // eslint-disable-line import/no-cycle
import { import { // eslint-disable-line import/no-cycle
model as Group, model as Group,
} from './group'; } from './group';
import { removeFromArray } from '../libs/collectionManipulators'; import { removeFromArray } from '../libs/collectionManipulators';
import shared from '../../common'; import shared from '../../common';
import { sendTxn as txnEmail } from '../libs/email'; import { sendTxn as txnEmail } from '../libs/email'; // eslint-disable-line import/no-cycle
import { sendNotification as sendPushNotification } from '../libs/pushNotifications'; import { sendNotification as sendPushNotification } from '../libs/pushNotifications';
import { syncableAttrs, setNextDue } from '../libs/taskManager'; import { syncableAttrs, setNextDue } from '../libs/taskManager';
@@ -246,7 +246,7 @@ schema.methods.updateTask = async function challengeUpdateTask (task) {
const updateCmd = { $set: {} }; const updateCmd = { $set: {} };
const syncableTask = syncableAttrs(task); const syncableTask = syncableAttrs(task);
for (const key in syncableTask) { for (const key of Object.keys(syncableTask)) {
updateCmd.$set[key] = syncableTask[key]; updateCmd.$set[key] = syncableTask[key];
} }

View File

@@ -3,12 +3,12 @@ import mongoose from 'mongoose';
import _ from 'lodash'; import _ from 'lodash';
import validator from 'validator'; import validator from 'validator';
import nconf from 'nconf'; import nconf from 'nconf';
import { import { // eslint-disable-line import/no-cycle
model as User, model as User,
nameFields, nameFields,
} from './user'; } from './user';
import shared from '../../common'; import shared from '../../common';
import { model as Challenge } from './challenge'; import { model as Challenge } from './challenge'; // eslint-disable-line import/no-cycle
import { import {
chatModel as Chat, chatModel as Chat,
setUserStyles, setUserStyles,
@@ -16,8 +16,8 @@ import {
} from './message'; } from './message';
import * as Tasks from './task'; import * as Tasks from './task';
import { removeFromArray } from '../libs/collectionManipulators'; import { removeFromArray } from '../libs/collectionManipulators';
import payments from '../libs/payments/payments'; import payments from '../libs/payments/payments'; // eslint-disable-line import/no-cycle
import { import { // eslint-disable-line import/no-cycle
groupChatReceivedWebhook, groupChatReceivedWebhook,
questActivityWebhook, questActivityWebhook,
} from '../libs/webhook'; } from '../libs/webhook';
@@ -27,7 +27,7 @@ import {
NotAuthorized, NotAuthorized,
} from '../libs/errors'; } from '../libs/errors';
import baseModel from '../libs/baseModel'; import baseModel from '../libs/baseModel';
import { sendTxn as sendTxnEmail } from '../libs/email'; import { sendTxn as sendTxnEmail } from '../libs/email'; // eslint-disable-line import/no-cycle
import { sendNotification as sendPushNotification } from '../libs/pushNotifications'; import { sendNotification as sendPushNotification } from '../libs/pushNotifications';
import { import {
syncableAttrs, syncableAttrs,
@@ -35,11 +35,11 @@ import {
import { import {
schema as SubscriptionPlanSchema, schema as SubscriptionPlanSchema,
} from './subscriptionPlan'; } from './subscriptionPlan';
import amazonPayments from '../libs/payments/amazon'; import amazonPayments from '../libs/payments/amazon'; // eslint-disable-line import/no-cycle
import stripePayments from '../libs/payments/stripe'; import stripePayments from '../libs/payments/stripe'; // eslint-disable-line import/no-cycle
import { getGroupChat, translateMessage } from '../libs/chat/group-chat'; import { getGroupChat, translateMessage } from '../libs/chat/group-chat'; // eslint-disable-line import/no-cycle
import { model as UserNotification } from './userNotification'; import { model as UserNotification } from './userNotification';
import { sendChatPushNotifications } from '../libs/chat'; import { sendChatPushNotifications } from '../libs/chat'; // eslint-disable-line import/no-cycle
const questScrolls = shared.content.quests; const questScrolls = shared.content.quests;
const { questSeriesAchievements } = shared.content; const { questSeriesAchievements } = shared.content;
@@ -108,8 +108,10 @@ export const schema = new Schema({
rage: Number, // limit break / "energy stored in shell", for explosion-attacks rage: Number, // limit break / "energy stored in shell", for explosion-attacks
}, },
// Shows boolean for each party-member who has accepted the quest. Eg {UUID: true, UUID: false}. Once all users click // Shows boolean for each party-member who has accepted the quest.
// 'Accept', the quest begins. If a false user waits too long, probably a good sign to prod them or boot them. // Eg {UUID: true, UUID: false}. Once all users click
// 'Accept', the quest begins.
// If a false user waits too long, probably a good sign to prod them or boot them.
// TODO when booting user, remove from .joined and check again if we can now start the quest // TODO when booting user, remove from .joined and check again if we can now start the quest
members: { members: {
$type: Schema.Types.Mixed, $type: Schema.Types.Mixed,
@@ -836,7 +838,8 @@ async function _updateUserWithRetries (userId, updates, numTry = 1, query = {})
return await User.update(query, updates).exec(); return await User.update(query, updates).exec();
} catch (err) { } catch (err) {
if (numTry < MAX_UPDATE_RETRIES) { if (numTry < MAX_UPDATE_RETRIES) {
return _updateUserWithRetries(userId, updates, ++numTry, query); numTry += 1; // eslint-disable-line no-param-reassign
return _updateUserWithRetries(userId, updates, numTry, query);
} }
throw err; throw err;
} }
@@ -1391,7 +1394,7 @@ schema.methods.updateTask = async function updateTask (taskToSync, options = {})
const updateCmd = { $set: {} }; const updateCmd = { $set: {} };
const syncableAttributes = syncableAttrs(taskToSync); const syncableAttributes = syncableAttrs(taskToSync);
for (const key in syncableAttributes) { for (const key of Object.keys(syncableAttributes)) {
updateCmd.$set[key] = syncableAttributes[key]; updateCmd.$set[key] = syncableAttributes[key];
} }

View File

@@ -6,7 +6,7 @@ import shared from '../../common';
import baseModel from '../libs/baseModel'; import baseModel from '../libs/baseModel';
import { InternalServerError } from '../libs/errors'; import { InternalServerError } from '../libs/errors';
import { preenHistory } from '../libs/preening'; import { preenHistory } from '../libs/preening';
import { SHARED_COMPLETION } from '../libs/groupTasks'; import { SHARED_COMPLETION } from '../libs/groupTasks'; // eslint-disable-line import/no-cycle
const { Schema } = mongoose; const { Schema } = mongoose;

View File

@@ -6,13 +6,14 @@ import * as Tasks from '../task';
import { import {
model as UserNotification, model as UserNotification,
} from '../userNotification'; } from '../userNotification';
import { import { // eslint-disable-line import/no-cycle
userActivityWebhook, userActivityWebhook,
} from '../../libs/webhook'; } from '../../libs/webhook';
import schema from './schema'; import schema from './schema'; // eslint-disable-line import/no-cycle
schema.plugin(baseModel, { schema.plugin(baseModel, {
// noSet is not used as updating uses a whitelist and creating only accepts specific params (password, email, username, ...) // noSet is not used as updating uses a whitelist and creating only accepts
// specific params (password, email, username, ...)
noSet: [], noSet: [],
private: ['auth.local.hashed_password', 'auth.local.passwordHashMethod', 'auth.local.salt', '_cronSignature', '_ABtests'], private: ['auth.local.hashed_password', 'auth.local.passwordHashMethod', 'auth.local.salt', '_cronSignature', '_ABtests'],
toJSONTransform: function userToJSON (plainObj, originalDoc) { toJSONTransform: function userToJSON (plainObj, originalDoc) {
@@ -25,7 +26,8 @@ schema.plugin(baseModel, {
delete plainObj.filters; delete plainObj.filters;
if (originalDoc.notifications) { if (originalDoc.notifications) {
plainObj.notifications = UserNotification.convertNotificationsToSafeJson(originalDoc.notifications); plainObj.notifications = UserNotification
.convertNotificationsToSafeJson(originalDoc.notifications);
} }
return plainObj; return plainObj;
@@ -51,7 +53,8 @@ function _populateDefaultTasks (user, taskTypes) {
user.tags = _.map(defaultsData.tags, tag => { user.tags = _.map(defaultsData.tags, tag => {
const newTag = _.cloneDeep(tag); const newTag = _.cloneDeep(tag);
// tasks automatically get _id=helpers.uuid() from TaskSchema id.default, but tags are Schema.Types.Mixed - so we need to manually invoke here // tasks automatically get _id=helpers.uuid() from TaskSchema id.default,
// but tags are Schema.Types.Mixed - so we need to manually invoke here
newTag.id = common.uuid(); newTag.id = common.uuid();
// Render tag's name in user's language // Render tag's name in user's language
newTag.name = newTag.name(user.preferences.language); newTag.name = newTag.name(user.preferences.language);
@@ -59,13 +62,14 @@ function _populateDefaultTasks (user, taskTypes) {
}); });
} }
// @TODO: default tasks are handled differently now, and not during registration. We should move this code // @TODO: default tasks are handled differently now, and not during registration.
// We should move this code
const tasksToCreate = []; const tasksToCreate = [];
if (user.registeredThrough === 'habitica-web') return Promise.all(tasksToCreate); if (user.registeredThrough === 'habitica-web') return Promise.all(tasksToCreate);
if (tagsI !== -1) { if (tagsI !== -1) {
taskTypes = _.clone(taskTypes); taskTypes = _.clone(taskTypes); // eslint-disable-line no-param-reassign
taskTypes.splice(tagsI, 1); taskTypes.splice(tagsI, 1);
} }
@@ -212,7 +216,8 @@ schema.pre('save', true, function preSaveUser (next, done) {
// this.markModified('items.pets'); // this.markModified('items.pets');
} }
// Filter notifications, remove unvalid and not necessary, handle the ones that have special requirements // Filter notifications, remove unvalid and not necessary,
// handle the ones that have special requirements
if ( // Make sure all the data is loaded if ( // Make sure all the data is loaded
this.isDirectSelected('notifications') this.isDirectSelected('notifications')
&& this.isDirectSelected('stats') && this.isDirectSelected('stats')
@@ -239,10 +244,12 @@ schema.pre('save', true, function preSaveUser (next, done) {
const classNotEnabled = !this.flags.classSelected || this.preferences.disableClasses; const classNotEnabled = !this.flags.classSelected || this.preferences.disableClasses;
// Take the most recent notification // Take the most recent notification
const lastExistingNotification = unallocatedPointsNotifications[unallocatedPointsNotifications.length - 1]; const unallLengh = unallocatedPointsNotifications.length;
const lastExistingNotification = unallocatedPointsNotifications[unallLengh - 1];
// Decide if it's outdated or not // Decide if it's outdated or not
const outdatedNotification = !lastExistingNotification || lastExistingNotification.data.points !== pointsToAllocate; const outdatedNotification = !lastExistingNotification
|| lastExistingNotification.data.points !== pointsToAllocate;
// If there are points to allocate and the notification is outdated, add a new notifications // If there are points to allocate and the notification is outdated, add a new notifications
if (pointsToAllocate > 0 && !classNotEnabled) { if (pointsToAllocate > 0 && !classNotEnabled) {
@@ -265,7 +272,11 @@ schema.pre('save', true, function preSaveUser (next, done) {
} }
if (this.isDirectSelected('preferences')) { if (this.isDirectSelected('preferences')) {
if (_.isNaN(this.preferences.dayStart) || this.preferences.dayStart < 0 || this.preferences.dayStart > 23) { if (
_.isNaN(this.preferences.dayStart)
|| this.preferences.dayStart < 0
|| this.preferences.dayStart > 23
) {
this.preferences.dayStart = 0; this.preferences.dayStart = 0;
} }
} }
@@ -273,7 +284,7 @@ schema.pre('save', true, function preSaveUser (next, done) {
// our own version incrementer // our own version incrementer
if (this.isDirectSelected('_v')) { if (this.isDirectSelected('_v')) {
if (_.isNaN(this._v) || !_.isNumber(this._v)) this._v = 0; if (_.isNaN(this._v) || !_.isNumber(this._v)) this._v = 0;
this._v++; this._v += 1;
} }
// Populate new users with default content // Populate new users with default content

View File

@@ -1,11 +1,12 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import schema from './schema'; import schema from './schema'; // eslint-disable-line import/no-cycle
import './hooks'; import './hooks'; // eslint-disable-line import/no-cycle
import './methods'; import './methods'; // eslint-disable-line import/no-cycle
// A list of publicly accessible fields (not everything from preferences because there are also a lot of settings tha should remain private) // A list of publicly accessible fields (not everything from preferences
// because there are also a lot of settings tha should remain private)
export const publicFields = `preferences.size preferences.hair preferences.skin preferences.shirt export const publicFields = `preferences.size preferences.hair preferences.skin preferences.shirt
preferences.chair preferences.costume preferences.sleep preferences.background preferences.tasks preferences.disableClasses profile stats preferences.chair preferences.costume preferences.sleep preferences.background preferences.tasks preferences.disableClasses profile stats
achievements party backer contributor auth.timestamps items inbox.optOut loginIncentives flags.classSelected achievements party backer contributor auth.timestamps items inbox.optOut loginIncentives flags.classSelected

View File

@@ -4,7 +4,7 @@ import {
} from 'lodash'; } from 'lodash';
import common from '../../../common'; import common from '../../../common';
import { import { // eslint-disable-line import/no-cycle
TAVERN_ID, TAVERN_ID,
model as Group, model as Group,
} from '../group'; } from '../group';
@@ -16,19 +16,20 @@ import {
} from '../message'; } from '../message';
import { model as UserNotification } from '../userNotification'; import { model as UserNotification } from '../userNotification';
import schema from './schema'; import schema from './schema'; // eslint-disable-line import/no-cycle
import payments from '../../libs/payments/payments'; import payments from '../../libs/payments/payments'; // eslint-disable-line import/no-cycle
import * as inboxLib from '../../libs/inbox'; import * as inboxLib from '../../libs/inbox'; // eslint-disable-line import/no-cycle
import amazonPayments from '../../libs/payments/amazon'; import amazonPayments from '../../libs/payments/amazon'; // eslint-disable-line import/no-cycle
import stripePayments from '../../libs/payments/stripe'; import stripePayments from '../../libs/payments/stripe'; // eslint-disable-line import/no-cycle
import paypalPayments from '../../libs/payments/paypal'; import paypalPayments from '../../libs/payments/paypal'; // eslint-disable-line import/no-cycle
const { daysSince } = common; const { daysSince } = common;
schema.methods.isSubscribed = function isSubscribed () { schema.methods.isSubscribed = function isSubscribed () {
const now = new Date(); const now = new Date();
const { plan } = this.purchased; const { plan } = this.purchased;
return plan && plan.customerId && (!plan.dateTerminated || moment(plan.dateTerminated).isAfter(now)); return plan && plan.customerId
&& (!plan.dateTerminated || moment(plan.dateTerminated).isAfter(now));
}; };
schema.methods.hasNotCancelled = function hasNotCancelled () { schema.methods.hasNotCancelled = function hasNotCancelled () {
@@ -49,10 +50,12 @@ schema.methods.getGroups = function getUserGroups () {
return userGroups; return userGroups;
}; };
/* eslint-disable no-unused-vars */ // The checks below all get access to sndr and rcvr, but not all use both /* eslint-disable no-unused-vars */
// The checks below all get access to sndr and rcvr, but not all use both
const INTERACTION_CHECKS = Object.freeze({ const INTERACTION_CHECKS = Object.freeze({
always: [ always: [
// Revoked chat privileges block all interactions to prevent the evading of harassment protections // Revoked chat privileges block all interactions
// to prevent the evading of harassment protections
// See issue #7971 for some discussion // See issue #7971 for some discussion
(sndr, rcvr) => sndr.flags.chatRevoked && 'chatPrivilegesRevoked', (sndr, rcvr) => sndr.flags.chatRevoked && 'chatPrivilegesRevoked',
@@ -65,7 +68,8 @@ const INTERACTION_CHECKS = Object.freeze({
// Private messaging has an opt-out, which does not affect other interactions // Private messaging has an opt-out, which does not affect other interactions
(sndr, rcvr) => rcvr.inbox.optOut && 'notAuthorizedToSendMessageToThisUser', (sndr, rcvr) => rcvr.inbox.optOut && 'notAuthorizedToSendMessageToThisUser',
// We allow a player to message themselves so they can test how PMs work or send their own notes to themselves // We allow a player to message themselves so they can test how PMs work
// or send their own notes to themselves
], ],
'transfer-gems': [ 'transfer-gems': [
@@ -79,10 +83,11 @@ const INTERACTION_CHECKS = Object.freeze({
}); });
/* eslint-enable no-unused-vars */ /* eslint-enable no-unused-vars */
export const KNOWN_INTERACTIONS = Object.freeze(Object.keys(INTERACTION_CHECKS).filter(key => key !== 'always')); export const KNOWN_INTERACTIONS = Object.freeze(Object.keys(INTERACTION_CHECKS) // eslint-disable-line import/prefer-default-export, max-len
.filter(key => key !== 'always'));
// Get an array of error message keys that would be thrown if the given interaction was attempted // Get an array of error message keys that would be thrown if the given interaction was attempted
schema.methods.getObjectionsToInteraction = function getObjectionsToInteraction (interaction, receiver) { schema.methods.getObjectionsToInteraction = function getObjectionsToInteraction (interaction, receiver) { // eslint-disable-line max-len
if (!KNOWN_INTERACTIONS.includes(interaction)) { if (!KNOWN_INTERACTIONS.includes(interaction)) {
throw new Error(`Unknown kind of interaction: "${interaction}", expected one of ${KNOWN_INTERACTIONS.join(', ')}`); throw new Error(`Unknown kind of interaction: "${interaction}", expected one of ${KNOWN_INTERACTIONS.join(', ')}`);
} }
@@ -125,8 +130,8 @@ schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, o
Object.assign(newReceiverMessage, messageDefaults(options.receiverMsg, sender)); Object.assign(newReceiverMessage, messageDefaults(options.receiverMsg, sender));
setUserStyles(newReceiverMessage, sender); setUserStyles(newReceiverMessage, sender);
userToReceiveMessage.inbox.newMessages++; userToReceiveMessage.inbox.newMessages += 1;
userToReceiveMessage._v++; userToReceiveMessage._v += 1;
/* @TODO disabled until mobile is ready /* @TODO disabled until mobile is ready
@@ -178,10 +183,12 @@ schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, o
}; };
/** /**
* Creates a notification based on the input parameters and adds it to the local user notifications array. * Creates a notification based on the input parameters and adds
* it to the local user notifications array.
* This does not save the notification to the database or interact with the database in any way. * This does not save the notification to the database or interact with the database in any way.
* *
* @param type The type of notification to add to the this. Possible values are defined in the UserNotificaiton Schema * @param type The type of notification to add to the this.
* Possible values are defined in the UserNotificaiton Schema
* @param data The data to add to the notification * @param data The data to add to the notification
* @param seen If the notification should be marked as seen * @param seen If the notification should be marked as seen
*/ */
@@ -194,16 +201,22 @@ schema.methods.addNotification = function addUserNotification (type, data = {},
}; };
/** /**
* Creates a notification based on the type and data input parameters and saves that new notification * Creates a notification based on the type and data input parameters
* to the database directly using an update statement. The local copy of these users are not updated by and saves that new notification
* this operation. Use this function when you want to add a notification to a user(s), but do not have * to the database directly using an update statement.
* The local copy of these users are not updated by
* this operation. Use this function when you want to add a notification to a user(s),
* but do not have
* the user document(s) opened. * the user document(s) opened.
* *
* @param query A Mongoose query defining the users to add the notification to. * @param query A Mongoose query defining the users to add the notification to.
* @param type The type of notification to add to the this. Possible values are defined in the UserNotificaiton Schema * @param type The type of notification to add to the this.
* Possible values are defined in the UserNotificaiton Schema
* @param data The data to add to the notification * @param data The data to add to the notification
*/ */
schema.statics.pushNotification = async function pushNotification (query, type, data = {}, seen = false) { schema.statics.pushNotification = async function pushNotification (
query, type, data = {}, seen = false,
) {
const newNotification = new UserNotification({ type, data, seen }); const newNotification = new UserNotification({ type, data, seen });
const validationResult = newNotification.validateSync(); const validationResult = newNotification.validateSync();
@@ -211,7 +224,11 @@ schema.statics.pushNotification = async function pushNotification (query, type,
throw validationResult; throw validationResult;
} }
await this.update(query, { $push: { notifications: newNotification.toObject() } }, { multi: true }).exec(); await this.update(
query,
{ $push: { notifications: newNotification.toObject() } },
{ multi: true },
).exec();
}; };
// Static method to add/remove properties to a JSON User object, // Static method to add/remove properties to a JSON User object,
@@ -229,7 +246,10 @@ schema.statics.transformJSONUser = function transformJSONUser (jsonUser, addComp
// Add stats.toNextLevel, stats.maxMP and stats.maxHealth // Add stats.toNextLevel, stats.maxMP and stats.maxHealth
// to a JSONified User stats object // to a JSONified User stats object
schema.statics.addComputedStatsToJSONObj = function addComputedStatsToUserJSONObj (userStatsJSON, user) { schema.statics.addComputedStatsToJSONObj = function addComputedStatsToUserJSONObj (
userStatsJSON,
user,
) {
// NOTE: if an item is manually added to this.stats then // NOTE: if an item is manually added to this.stats then
// common/fns/predictableRandom must be tweaked so the new item is not considered. // common/fns/predictableRandom must be tweaked so the new item is not considered.
// Otherwise the client will have it while the server won't and the results will be different. // Otherwise the client will have it while the server won't and the results will be different.
@@ -251,25 +271,29 @@ schema.statics.addComputedStatsToJSONObj = function addComputedStatsToUserJSONOb
* *
* @return a Promise from api.cancelSubscription() * @return a Promise from api.cancelSubscription()
*/ */
// @TODO: There is currently a three way relation between the user, payment methods and the payment helper // @TODO: There is currently a three way relation between the user,
// This creates some odd Dependency Injection issues. To counter that, we use the user as the third layer // payment methods and the payment helper
// To negotiate between the payment providers and the payment helper (which probably has too many responsiblities) // This creates some odd Dependency Injection issues. To counter that,
// In summary, currently is is best practice to use this method to cancel a user subscription, rather than calling the // we use the user as the third layer
// To negotiate between the payment providers and the payment helper
// (which probably has too many responsiblities)
// In summary, currently is is best practice to use this method to cancel a user subscription,
// rather than calling the
// payment helper. // payment helper.
schema.methods.cancelSubscription = async function cancelSubscription (options = {}) { schema.methods.cancelSubscription = async function cancelSubscription (options = {}) {
const { plan } = this.purchased; const { plan } = this.purchased;
options.user = this; options.user = this;
if (plan.paymentMethod === amazonPayments.constants.PAYMENT_METHOD) { if (plan.paymentMethod === amazonPayments.constants.PAYMENT_METHOD) {
return await amazonPayments.cancelSubscription(options); return amazonPayments.cancelSubscription(options);
} if (plan.paymentMethod === stripePayments.constants.PAYMENT_METHOD) { } if (plan.paymentMethod === stripePayments.constants.PAYMENT_METHOD) {
return await stripePayments.cancelSubscription(options); return stripePayments.cancelSubscription(options);
} if (plan.paymentMethod === paypalPayments.constants.PAYMENT_METHOD) { } if (plan.paymentMethod === paypalPayments.constants.PAYMENT_METHOD) {
return await paypalPayments.subscribeCancel(options); return paypalPayments.subscribeCancel(options);
} }
// Android and iOS subscriptions cannot be cancelled by Habitica. // Android and iOS subscriptions cannot be cancelled by Habitica.
return await payments.cancelSubscription(options); return payments.cancelSubscription(options);
}; };
schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) { schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
@@ -278,9 +302,13 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
// both timezones to work out if cron should run. // both timezones to work out if cron should run.
// CDS = Custom Day Start time. // CDS = Custom Day Start time.
let timezoneOffsetFromUserPrefs = this.preferences.timezoneOffset; let timezoneOffsetFromUserPrefs = this.preferences.timezoneOffset;
const timezoneOffsetAtLastCron = isFinite(this.preferences.timezoneOffsetAtLastCron) ? this.preferences.timezoneOffsetAtLastCron : timezoneOffsetFromUserPrefs; const timezoneOffsetAtLastCron = Number.isFinite(this.preferences.timezoneOffsetAtLastCron)
? this.preferences.timezoneOffsetAtLastCron
: timezoneOffsetFromUserPrefs;
let timezoneOffsetFromBrowser = typeof req.header === 'function' && Number(req.header('x-user-timezoneoffset')); let timezoneOffsetFromBrowser = typeof req.header === 'function' && Number(req.header('x-user-timezoneoffset'));
timezoneOffsetFromBrowser = isFinite(timezoneOffsetFromBrowser) ? timezoneOffsetFromBrowser : timezoneOffsetFromUserPrefs; timezoneOffsetFromBrowser = Number.isFinite(timezoneOffsetFromBrowser)
? timezoneOffsetFromBrowser
: timezoneOffsetFromUserPrefs;
// NB: All timezone offsets can be 0, so can't use `... || ...` to apply non-zero defaults // NB: All timezone offsets can be 0, so can't use `... || ...` to apply non-zero defaults
if (timezoneOffsetFromBrowser !== timezoneOffsetFromUserPrefs) { if (timezoneOffsetFromBrowser !== timezoneOffsetFromUserPrefs) {
@@ -296,8 +324,8 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
if (timezoneOffsetAtLastCron !== timezoneOffsetFromUserPrefs) { if (timezoneOffsetAtLastCron !== timezoneOffsetFromUserPrefs) {
// Give the user extra time based on the difference in timezones // Give the user extra time based on the difference in timezones
if (timezoneOffsetAtLastCron < timezoneOffsetFromUserPrefs) { if (timezoneOffsetAtLastCron < timezoneOffsetFromUserPrefs) {
const differenceBetweenTimezonesInMinutes = timezoneOffsetFromUserPrefs - timezoneOffsetAtLastCron; const differenceBetweenTimezonesInMinutes = timezoneOffsetFromUserPrefs - timezoneOffsetAtLastCron; // eslint-disable-line max-len
now = moment(now).subtract(differenceBetweenTimezonesInMinutes, 'minutes'); now = moment(now).subtract(differenceBetweenTimezonesInMinutes, 'minutes'); // eslint-disable-line no-param-reassign, max-len
} }
// Since cron last ran, the user's timezone has changed. // Since cron last ran, the user's timezone has changed.
@@ -324,7 +352,10 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
// This should be impossible for this direction of timezone change, but // This should be impossible for this direction of timezone change, but
// just in case I'm wrong... // just in case I'm wrong...
// TODO // TODO
// console.log("zone has changed - old zone says run cron, NEW zone says no - stop cron now only -- SHOULD NOT HAVE GOT TO HERE", timezoneOffsetAtLastCron, timezoneOffsetFromUserPrefs, now); // used in production for confirming this never happens // console.log("zone has changed - old zone says run cron,
// NEW zone says no - stop cron now only -- SHOULD NOT HAVE GOT TO HERE",
// timezoneOffsetAtLastCron, timezoneOffsetFromUserPrefs, now);
// used in production for confirming this never happens
} else if (daysMissedNewZone > 0) { } else if (daysMissedNewZone > 0) {
// The old timezone says that cron should NOT run -- i.e., cron has // The old timezone says that cron should NOT run -- i.e., cron has
// already run today, from the old timezone's point of view. // already run today, from the old timezone's point of view.
@@ -341,7 +372,8 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
// e.g., for dangerous zone change: 240 - 300 = -60 or -660 - -600 = -60 // e.g., for dangerous zone change: 240 - 300 = -60 or -660 - -600 = -60
this.lastCron = moment(this.lastCron).subtract(timezoneOffsetDiff, 'minutes'); this.lastCron = moment(this.lastCron).subtract(timezoneOffsetDiff, 'minutes');
// NB: We don't change this.auth.timestamps.loggedin so that will still record the time that the previous cron actually ran. // NB: We don't change this.auth.timestamps.loggedin so that will still record
// the time that the previous cron actually ran.
// From now on we can ignore the old timezone: // From now on we can ignore the old timezone:
this.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs; this.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs;
} else { } else {
@@ -351,9 +383,14 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
} }
} else if (timezoneOffsetAtLastCron > timezoneOffsetFromUserPrefs) { } else if (timezoneOffsetAtLastCron > timezoneOffsetFromUserPrefs) {
daysMissed = daysMissedNewZone; daysMissed = daysMissedNewZone;
// TODO: Either confirm that there is nothing that could possibly go wrong here and remove the need for this else branch, or fix stuff. // TODO: Either confirm that there is nothing that could possibly go wrong
// There are probably situations where the Dailies do not reset early enough for a user who was expecting the zone change and wants to use all their Dailies immediately in the new zone; // here and remove the need for this else branch, or fix stuff.
// if so, we should provide an option for easy reset of Dailies (can't be automatic because there will be other situations where the user was not prepared). // There are probably situations where the Dailies do not reset early enough
// for a user who was expecting the zone change and wants to use all their Dailies
// immediately in the new zone;
// if so, we should provide an option for easy reset of Dailies
// (can't be automatic because there will be other situations where
// the user was not prepared).
} }
} }
@@ -386,7 +423,8 @@ schema.methods.canGetGems = async function canObtainGems () {
const groups = await getUserGroupData(user); const groups = await getUserGroupData(user);
return groups.every(g => !g.isSubscribed() || g.leader === user._id || g.leaderOnly.getGems !== true); return groups
.every(g => !g.isSubscribed() || g.leader === user._id || g.leaderOnly.getGems !== true);
}; };
schema.methods.isMemberOfGroupPlan = async function isMemberOfGroupPlan () { schema.methods.isMemberOfGroupPlan = async function isMemberOfGroupPlan () {

View File

@@ -10,7 +10,7 @@ import {
import { import {
schema as SubscriptionPlanSchema, schema as SubscriptionPlanSchema,
} from '../subscriptionPlan'; } from '../subscriptionPlan';
import { import { // eslint-disable-line import/no-cycle
getDefaultOwnedGear, getDefaultOwnedGear,
} from '../../libs/items/utils'; } from '../../libs/items/utils';
@@ -71,7 +71,8 @@ export default new Schema({
updated: { $type: Date, default: Date.now }, updated: { $type: Date, default: Date.now },
}, },
}, },
// We want to know *every* time an object updates. Mongoose uses __v to designate when an object contains arrays which // We want to know *every* time an object updates.
// Mongoose uses __v to designate when an object contains arrays which
// have been updated (http://goo.gl/gQLz41), but we want *every* update // have been updated (http://goo.gl/gQLz41), but we want *every* update
_v: { $type: Number, default: 0 }, _v: { $type: Number, default: 0 },
migration: String, migration: String,
@@ -138,7 +139,8 @@ export default new Schema({
}, },
contributor: { contributor: {
// 1-9, see https://trello.com/c/wkFzONhE/277-contributor-gear https://github.com/HabitRPG/habitica/issues/3801 // 1-9, see https://trello.com/c/wkFzONhE/277-contributor-gear
// https://github.com/HabitRPG/habitica/issues/3801
level: { level: {
$type: Number, $type: Number,
min: 0, min: 0,
@@ -186,7 +188,8 @@ export default new Schema({
customizationsNotification: { $type: Boolean, default: false }, customizationsNotification: { $type: Boolean, default: false },
showTour: { $type: Boolean, default: true }, showTour: { $type: Boolean, default: true },
tour: { tour: {
// -1 indicates "uninitiated", -2 means "complete", any other number is the current tour step (0-index) // -1 indicates "uninitiated", -2 means "complete",
// any other number is the current tour step (0-index)
intro: { $type: Number, default: -1 }, intro: { $type: Number, default: -1 },
classes: { $type: Number, default: -1 }, classes: { $type: Number, default: -1 },
stats: { $type: Number, default: -1 }, stats: { $type: Number, default: -1 },
@@ -398,11 +401,13 @@ export default new Schema({
challenges: [{ $type: String, ref: 'Challenge', validate: [v => validator.isUUID(v), 'Invalid uuid.'] }], challenges: [{ $type: String, ref: 'Challenge', validate: [v => validator.isUUID(v), 'Invalid uuid.'] }],
invitations: { invitations: {
// Using an array without validation because otherwise mongoose treat this as a subdocument and applies _id by default // Using an array without validation because otherwise mongoose
// treat this as a subdocument and applies _id by default
// Schema is (id, name, inviter, publicGuild) // Schema is (id, name, inviter, publicGuild)
// TODO one way to fix is http://mongoosejs.com/docs/guide.html#_id // TODO one way to fix is http://mongoosejs.com/docs/guide.html#_id
guilds: { $type: Array, default: () => [] }, guilds: { $type: Array, default: () => [] },
// Using a Mixed type because otherwise user.invitations.party = {} // to reset invitation, causes validation to fail TODO // Using a Mixed type because otherwise user.invitations.party = {}
// to reset invitation, causes validation to fail TODO
// schema is the same as for guild invitations (id, name, inviter) // schema is the same as for guild invitations (id, name, inviter)
party: { party: {
$type: Schema.Types.Mixed, $type: Schema.Types.Mixed,
@@ -445,8 +450,12 @@ export default new Schema({
}, },
collectedItems: { $type: Number, default: 0 }, collectedItems: { $type: Number, default: 0 },
}, },
completed: String, // When quest is done, we move it from key => completed, and it's a one-time flag (for modal) that they unset by clicking "ok" in browser // When quest is done, we move it from key => completed,
RSVPNeeded: { $type: Boolean, default: false }, // Set to true when invite is pending, set to false when quest invite is accepted or rejected, quest starts, or quest is cancelled // and it's a one-time flag (for modal) that they unset by clicking "ok" in browser
completed: String,
// Set to true when invite is pending, set to false when quest
// invite is accepted or rejected, quest starts, or quest is cancelled
RSVPNeeded: { $type: Boolean, default: false },
}, },
}, },
preferences: { preferences: {
@@ -489,7 +498,8 @@ export default new Schema({
$type: Schema.Types.Mixed, $type: Schema.Types.Mixed,
default: () => ({}), default: () => ({}),
}, },
// For the following fields make sure to use strict comparison when searching for falsey values (=== false) // For the following fields make sure to use strict
// comparison when searching for falsey values (=== false)
// As users who didn't login after these were introduced may have them undefined/null // As users who didn't login after these were introduced may have them undefined/null
emailNotifications: { emailNotifications: {
unsubscribeFromAll: { $type: Boolean, default: false }, unsubscribeFromAll: { $type: Boolean, default: false },
@@ -537,7 +547,8 @@ export default new Schema({
$type: Array, $type: Array,
validate: categories => { validate: categories => {
const validCategories = ['work', 'exercise', 'healthWellness', 'school', 'teams', 'chores', 'creativity']; const validCategories = ['work', 'exercise', 'healthWellness', 'school', 'teams', 'chores', 'creativity'];
const isValidCategory = categories.every(category => validCategories.indexOf(category) !== -1); const isValidCategory = categories
.every(category => validCategories.indexOf(category) !== -1);
return isValidCategory; return isValidCategory;
}, },
}, },
@@ -621,7 +632,8 @@ export default new Schema({
path: { $type: String }, path: { $type: String },
type: { $type: String }, type: { $type: String },
}], }],
// Ordered array of shown pinned items, necessary for sorting because seasonal items are not stored in pinnedItems // Ordered array of shown pinned items,
// necessary for sorting because seasonal items are not stored in pinnedItems
pinnedItemsOrder: [{ $type: String }], pinnedItemsOrder: [{ $type: String }],
// Items the user manually unpinned from the ones suggested by Habitica // Items the user manually unpinned from the ones suggested by Habitica
unpinnedItems: [{ unpinnedItems: [{