mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-14 21:27:23 +01:00
fix linting for server (except for length of apidoc)
This commit is contained in:
@@ -12,7 +12,6 @@ import {
|
||||
} from '../../libs/errors';
|
||||
import * as passwordUtils from '../../libs/password';
|
||||
import { sendTxn as sendTxnEmail } from '../../libs/email';
|
||||
import { validatePasswordResetCodeAndFindUser, convertToBcrypt } from '../../libs/password';
|
||||
import { encrypt } from '../../libs/encryption';
|
||||
import {
|
||||
loginRes,
|
||||
@@ -125,7 +124,7 @@ api.loginLocal = {
|
||||
headers: req.headers,
|
||||
});
|
||||
|
||||
return loginRes(user, ...arguments);
|
||||
return loginRes(user, req, res);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -137,7 +136,7 @@ api.loginSocial = {
|
||||
})],
|
||||
url: '/user/auth/social',
|
||||
async handler (req, res) {
|
||||
return await loginSocial(req, res);
|
||||
await loginSocial(req, res);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -377,7 +376,7 @@ api.resetPasswordSetNewOne = {
|
||||
method: 'POST',
|
||||
url: '/user/auth/reset-password-set-new-one',
|
||||
async handler (req, res) {
|
||||
const user = await validatePasswordResetCodeAndFindUser(req.body.code);
|
||||
const user = await passwordUtils.validatePasswordResetCodeAndFindUser(req.body.code);
|
||||
const isValidCode = Boolean(user);
|
||||
|
||||
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
|
||||
await convertToBcrypt(user, String(newPassword));
|
||||
await passwordUtils.convertToBcrypt(user, String(newPassword));
|
||||
user.auth.local.passwordResetCode = undefined; // Reset saved password reset code
|
||||
await user.save();
|
||||
|
||||
@@ -418,7 +417,8 @@ api.deleteSocial = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
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 (!hasBackupAuth(user, network)) throw new NotAuthorized(res.t('cantDetachSocial'));
|
||||
const unset = {
|
||||
|
||||
@@ -413,8 +413,12 @@ api.getUserChallenges = {
|
||||
User.findById(chal.leader).select(`${nameFields} backer contributor`).exec(),
|
||||
Group.findById(chal.group).select(basicGroupFields).exec(),
|
||||
]).then(populatedData => {
|
||||
resChals[index].leader = populatedData[0] ? populatedData[0].toJSON({ minimize: true }) : null;
|
||||
resChals[index].group = populatedData[1] ? populatedData[1].toJSON({ minimize: true }) : null;
|
||||
resChals[index].leader = populatedData[0]
|
||||
? populatedData[0].toJSON({ minimize: true })
|
||||
: null;
|
||||
resChals[index].group = populatedData[1]
|
||||
? populatedData[1].toJSON({ minimize: true })
|
||||
: null;
|
||||
})));
|
||||
|
||||
res.respond(200, resChals);
|
||||
@@ -460,12 +464,17 @@ api.getGroupChallenges = {
|
||||
|
||||
const challenges = await Challenge.find({ group: groupId })
|
||||
.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();
|
||||
|
||||
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
|
||||
await Promise.all(resChals.map((chal, index) => User
|
||||
@@ -473,7 +482,9 @@ api.getGroupChallenges = {
|
||||
.select(nameFields)
|
||||
.exec()
|
||||
.then(populatedLeader => {
|
||||
resChals[index].leader = populatedLeader ? populatedLeader.toJSON({ minimize: true }) : null;
|
||||
resChals[index].leader = populatedLeader
|
||||
? populatedLeader.toJSON({ minimize: true })
|
||||
: null;
|
||||
})));
|
||||
|
||||
res.respond(200, resChals);
|
||||
@@ -559,7 +570,8 @@ api.exportChallengeCsv = {
|
||||
});
|
||||
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)
|
||||
|
||||
const [members, tasks] = await Promise.all([
|
||||
@@ -578,7 +590,8 @@ api.exportChallengeCsv = {
|
||||
.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 index = -1;
|
||||
@@ -594,8 +607,8 @@ api.exportChallengeCsv = {
|
||||
return;
|
||||
}
|
||||
while (task.userId !== lastUserId) {
|
||||
index++;
|
||||
lastUserId = resArray[index][0]; // resArray[index][0] is an user id
|
||||
index += 1;
|
||||
lastUserId = [resArray[index]]; // resArray[index][0] is an user id
|
||||
}
|
||||
|
||||
const streak = task.streak || 0;
|
||||
@@ -603,8 +616,12 @@ api.exportChallengeCsv = {
|
||||
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
|
||||
const challengeTasks = _.reduce(challenge.tasksOrder.toObject(), (result, array) => result.concat(array), []).sort();
|
||||
// The first row is going to be UUID name Task Value Notes
|
||||
// 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']);
|
||||
|
||||
_.times(challengeTasks.length, () => resArray[0].push('Task', 'Value', 'Notes', 'Streak'));
|
||||
|
||||
@@ -110,7 +110,6 @@ api.postChat = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
const { groupId } = req.params;
|
||||
let chatUpdated;
|
||||
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||
req.sanitize('message').trim();
|
||||
@@ -165,7 +164,8 @@ api.postChat = {
|
||||
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]) {
|
||||
const matchedBadWords = getBannedWordsFromText(req.body.message);
|
||||
if (matchedBadWords.length > 0) {
|
||||
@@ -175,7 +175,9 @@ api.postChat = {
|
||||
|
||||
const chatRes = await Group.toJSONCleanChat(group, user);
|
||||
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)) {
|
||||
throw new NotAuthorized(res.t('messageGroupChatSpam'));
|
||||
@@ -449,7 +451,8 @@ api.seenChat = {
|
||||
const validationErrors = req.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});
|
||||
// if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
@@ -476,7 +479,7 @@ api.seenChat = {
|
||||
// Update the user version field manually,
|
||||
// it cannot be updated in the pre update hook
|
||||
// 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();
|
||||
res.respond(200, {});
|
||||
@@ -529,7 +532,9 @@ api.deleteChat = {
|
||||
|
||||
const chatRes = await Group.toJSONCleanChat(group, user);
|
||||
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();
|
||||
|
||||
|
||||
@@ -18,13 +18,17 @@ const api = {};
|
||||
|
||||
function walkContent (obj, lang) {
|
||||
_.each(obj, (item, key, source) => {
|
||||
if (_.isPlainObject(item) || _.isArray(item)) return walkContent(item, lang);
|
||||
if (_.isFunction(item) && item.i18nLangFunc) source[key] = item(lang);
|
||||
if (_.isPlainObject(item) || _.isArray(item)) {
|
||||
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
|
||||
// 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
|
||||
const cachedContentResponses = {};
|
||||
|
||||
@@ -43,18 +47,21 @@ async function saveContentToDisk (language, content) {
|
||||
try {
|
||||
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');
|
||||
|
||||
cacheBeingWritten[language] = false;
|
||||
cachedContentResponses[language] = true;
|
||||
} 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);
|
||||
return saveContentToDisk(language, content);
|
||||
saveContentToDisk(language, content);
|
||||
} else {
|
||||
cacheBeingWritten[language] = false;
|
||||
logger.error(err);
|
||||
}
|
||||
cacheBeingWritten[language] = false;
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import common from '../../../common';
|
||||
import payments from '../../libs/payments/payments';
|
||||
import stripePayments from '../../libs/payments/stripe';
|
||||
import amzLib from '../../libs/payments/amazon';
|
||||
import shared from '../../../common';
|
||||
import apiError from '../../libs/apiError';
|
||||
|
||||
const MAX_EMAIL_INVITES_BY_USER = 200;
|
||||
@@ -122,7 +121,7 @@ api.createGroup = {
|
||||
|
||||
group.balance = 1;
|
||||
|
||||
user.balance--;
|
||||
user.balance -= 1;
|
||||
user.guilds.push(group._id);
|
||||
if (!user.achievements.joinedGuild) {
|
||||
user.achievements.joinedGuild = true;
|
||||
@@ -139,7 +138,8 @@ api.createGroup = {
|
||||
const savedGroup = results[1];
|
||||
|
||||
// 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();
|
||||
// the leader is the authenticated user
|
||||
response.leader = {
|
||||
@@ -210,7 +210,7 @@ api.createGroupPlan = {
|
||||
if (req.body.paymentType === 'Stripe') {
|
||||
const token = req.body.id;
|
||||
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 { email } = req.body;
|
||||
const { headers } = req;
|
||||
@@ -228,7 +228,9 @@ api.createGroupPlan = {
|
||||
});
|
||||
} else if (req.body.paymentType === 'Amazon') {
|
||||
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 groupId = savedGroup._id;
|
||||
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
|
||||
// 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();
|
||||
// the leader is the authenticated user
|
||||
response.leader = {
|
||||
@@ -514,7 +517,9 @@ api.joinGroup = {
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
// 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'));
|
||||
|
||||
let isUserInvited = false;
|
||||
@@ -565,7 +570,8 @@ api.joinGroup = {
|
||||
}
|
||||
|
||||
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'));
|
||||
}
|
||||
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'));
|
||||
|
||||
// @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;
|
||||
|
||||
@@ -600,7 +608,7 @@ api.joinGroup = {
|
||||
if (!inviter.items.quests.basilist) {
|
||||
inviter.items.quests.basilist = 0;
|
||||
}
|
||||
inviter.items.quests.basilist++;
|
||||
inviter.items.quests.basilist += 1;
|
||||
inviter.markModified('items.quests');
|
||||
}
|
||||
promises.push(inviter.save());
|
||||
@@ -686,7 +694,9 @@ api.rejectGroupInvite = {
|
||||
|
||||
const hasPartyInvitation = removeFromArray(user.invitations.parties, { id: groupId });
|
||||
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');
|
||||
isUserInvited = true;
|
||||
} else {
|
||||
@@ -772,7 +782,10 @@ api.leaveGroup = {
|
||||
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'));
|
||||
}
|
||||
}
|
||||
@@ -909,7 +922,9 @@ api.removeGroupMember = {
|
||||
}
|
||||
if (isInvited === 'party') {
|
||||
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');
|
||||
}
|
||||
} else {
|
||||
@@ -1064,7 +1079,8 @@ api.inviteToGroup = {
|
||||
}
|
||||
|
||||
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);
|
||||
results.push(...usernameResults);
|
||||
}
|
||||
|
||||
@@ -168,10 +168,9 @@ api.getHero = {
|
||||
url: '/hall/heroes/:heroId',
|
||||
middlewares: [authWithHeaders(), ensureAdmin],
|
||||
async handler (req, res) {
|
||||
let validationErrors;
|
||||
req.checkParams('heroId', res.t('heroIdRequired')).notEmpty();
|
||||
|
||||
validationErrors = req.validationErrors();
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
const { heroId } = req.params;
|
||||
@@ -256,22 +255,26 @@ api.updateHero = {
|
||||
if (updateData.balance) hero.balance = updateData.balance;
|
||||
|
||||
// give them gems if they got an higher level
|
||||
let newTier = updateData.contributor && updateData.contributor.level; // tier = level in this context
|
||||
const oldTier = hero.contributor && hero.contributor.level || 0;
|
||||
// tier = level in this context
|
||||
let newTier = updateData.contributor && updateData.contributor.level;
|
||||
|
||||
const oldTier = (hero.contributor && hero.contributor.level) || 0;
|
||||
if (newTier > oldTier) {
|
||||
hero.flags.contributor = true;
|
||||
let tierDiff = newTier - oldTier; // can be 2+ tier increases at once
|
||||
while (tierDiff) {
|
||||
hero.balance += gemsPerTier[newTier] / 4; // balance is in $
|
||||
tierDiff--;
|
||||
newTier--; // give them gems for the next tier down if they weren't aready that tier
|
||||
tierDiff -= 1;
|
||||
newTier -= 1; // give them gems for the next tier down if they weren't aready that tier
|
||||
}
|
||||
|
||||
hero.addNotification('NEW_CONTRIBUTOR_LEVEL');
|
||||
}
|
||||
|
||||
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
|
||||
if (hero.contributor.level >= 6) {
|
||||
@@ -279,7 +282,8 @@ api.updateHero = {
|
||||
hero.markModified('items.pets');
|
||||
}
|
||||
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) {
|
||||
@@ -290,8 +294,12 @@ api.updateHero = {
|
||||
hero.auth.blocked = false;
|
||||
}
|
||||
|
||||
if (updateData.flags && _.isBoolean(updateData.flags.chatRevoked)) hero.flags.chatRevoked = updateData.flags.chatRevoked;
|
||||
if (updateData.flags && _.isBoolean(updateData.flags.chatShadowMuted)) hero.flags.chatShadowMuted = updateData.flags.chatShadowMuted;
|
||||
if (updateData.flags && _.isBoolean(updateData.flags.chatRevoked)) {
|
||||
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 heroJSON = savedHero.toJSON();
|
||||
|
||||
@@ -259,7 +259,8 @@ api.getMemberAchievements = {
|
||||
|
||||
// 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) {
|
||||
// check for allowed `type`
|
||||
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();
|
||||
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
|
||||
group = await Group.getGroup({
|
||||
user,
|
||||
@@ -305,7 +307,8 @@ function _getMembersForItem (type) {
|
||||
|
||||
const query = {};
|
||||
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') {
|
||||
query.challenges = challenge._id;
|
||||
@@ -349,7 +352,8 @@ function _getMembersForItem (type) {
|
||||
}
|
||||
} else {
|
||||
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') {
|
||||
fields = memberFields;
|
||||
addComputedStats = true;
|
||||
@@ -554,7 +558,8 @@ api.getChallengeMemberProgress = {
|
||||
const challenge = await Challenge.findById(challengeId).exec();
|
||||
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
|
||||
const group = await Group.getGroup({
|
||||
user, groupId: challenge.group, fields: '_id type privacy', optionalMembership: true,
|
||||
|
||||
@@ -2,7 +2,8 @@ import { authWithHeaders } from '../../middlewares/auth';
|
||||
|
||||
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 worldDmg = { // @TODO
|
||||
bailey: false,
|
||||
|
||||
@@ -43,7 +43,7 @@ api.readNotification = {
|
||||
// Update the user version field manually,
|
||||
// it cannot be updated in the pre update hook
|
||||
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
|
||||
user._v++;
|
||||
user._v += 1;
|
||||
|
||||
await user.update({
|
||||
$pull: { notifications: { id: req.params.notificationId } },
|
||||
@@ -90,7 +90,7 @@ api.readNotifications = {
|
||||
// Update the user version field manually,
|
||||
// it cannot be updated in the pre update hook
|
||||
// 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));
|
||||
},
|
||||
@@ -140,7 +140,7 @@ api.seeNotification = {
|
||||
// Update the user version field manually,
|
||||
// it cannot be updated in the pre update hook
|
||||
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
|
||||
user._v++;
|
||||
user._v += 1;
|
||||
|
||||
res.respond(200, notification);
|
||||
},
|
||||
|
||||
@@ -305,7 +305,9 @@ api.forceStart = {
|
||||
if (group.type !== 'party') throw new NotAuthorized(res.t('guildQuestsNotSupported'));
|
||||
if (!group.quest.key) throw new NotFound(res.t('questNotPending'));
|
||||
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');
|
||||
|
||||
@@ -352,7 +354,8 @@ api.cancelQuest = {
|
||||
async handler (req, res) {
|
||||
// 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.
|
||||
// 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 { groupId } = req.params;
|
||||
|
||||
@@ -366,7 +369,9 @@ api.cancelQuest = {
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
if (group.type !== 'party') throw new NotAuthorized(res.t('guildQuestsNotSupported'));
|
||||
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'));
|
||||
|
||||
const questName = questScrolls[group.quest.key].text('en');
|
||||
|
||||
@@ -227,7 +227,7 @@ api.deleteTag = {
|
||||
// Update the user version field manually,
|
||||
// it cannot be updated in the pre update hook
|
||||
// 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
|
||||
await Tasks.Task.update({
|
||||
|
||||
@@ -383,12 +383,23 @@ api.getTask = {
|
||||
|
||||
if (!task) {
|
||||
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();
|
||||
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'));
|
||||
}
|
||||
} 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'));
|
||||
}
|
||||
|
||||
@@ -451,16 +462,21 @@ api.updateTask = {
|
||||
group = await Group.getGroup({ user, groupId: task.group.id, fields });
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
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();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
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'));
|
||||
}
|
||||
|
||||
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);
|
||||
// Sanitize differently user tasks linked to a challenge
|
||||
let sanitizedObj;
|
||||
@@ -476,7 +492,8 @@ api.updateTask = {
|
||||
_.assign(task, sanitizedObj);
|
||||
|
||||
// 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
|
||||
|
||||
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
|
||||
|
||||
// @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 = [];
|
||||
managers.forEach(manager => {
|
||||
manager.addNotification('GROUP_TASK_APPROVAL', {
|
||||
@@ -594,7 +612,8 @@ api.scoreTask = {
|
||||
taskName: task.text,
|
||||
}, manager.preferences.language),
|
||||
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,
|
||||
groupTaskId: task.group.taskId, // the original task id
|
||||
direction,
|
||||
@@ -612,7 +631,8 @@ api.scoreTask = {
|
||||
const wasCompleted = task.completed;
|
||||
|
||||
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 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 },
|
||||
}).exec();
|
||||
// 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({
|
||||
$push: { 'tasksOrder.todos': task._id },
|
||||
}).exec();
|
||||
@@ -649,7 +673,9 @@ api.scoreTask = {
|
||||
}).exec();
|
||||
|
||||
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);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -675,7 +701,8 @@ api.scoreTask = {
|
||||
});
|
||||
|
||||
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 {
|
||||
const chalTask = await Tasks.Task.findOne({
|
||||
_id: task.challenge.taskId,
|
||||
@@ -763,7 +790,7 @@ api.moveTask = {
|
||||
// Update the user version field manually,
|
||||
// it cannot be updated in the pre update hook
|
||||
// See https://github.com/HabitRPG/habitica/pull/9321#issuecomment-354187666 for more info
|
||||
user._v++;
|
||||
user._v += 1;
|
||||
|
||||
res.respond(200, order);
|
||||
},
|
||||
@@ -811,11 +838,15 @@ api.addChecklistItem = {
|
||||
const fields = requiredGroupFields.concat(' managers');
|
||||
group = await Group.getGroup({ user, groupId: task.group.id, fields });
|
||||
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();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
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'));
|
||||
}
|
||||
|
||||
@@ -927,11 +958,15 @@ api.updateChecklistItem = {
|
||||
group = await Group.getGroup({ user, groupId: task.group.id, fields });
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
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();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
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'));
|
||||
}
|
||||
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 });
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
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();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
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'));
|
||||
}
|
||||
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 (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
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();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
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'));
|
||||
} else if (task.userId && task.challenge.id && !task.challenge.broken) {
|
||||
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'));
|
||||
}
|
||||
|
||||
@@ -1332,7 +1379,7 @@ api.deleteTask = {
|
||||
// Update the user version field manually,
|
||||
// it cannot be updated in the pre update hook
|
||||
// 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()]);
|
||||
} else {
|
||||
|
||||
@@ -18,7 +18,8 @@ import apiError from '../../../libs/apiError';
|
||||
const requiredGroupFields = '_id leader tasksOrder name';
|
||||
// @TODO: abstract to task lib
|
||||
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) {
|
||||
const isNotGroupLeader = group.leader !== user._id;
|
||||
@@ -96,7 +97,11 @@ api.getGroupTasks = {
|
||||
|
||||
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'));
|
||||
|
||||
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'));
|
||||
|
||||
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.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
|
||||
@@ -123,7 +123,10 @@ api.getBuyList = {
|
||||
// return text and notes strings
|
||||
_.each(list, item => {
|
||||
_.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
|
||||
_.each(list, item => {
|
||||
_.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) {
|
||||
const { user } = res.locals;
|
||||
|
||||
let buyRes;
|
||||
// @TODO: Remove this when mobile passes type in body
|
||||
const type = req.params.key;
|
||||
if (buySpecialKeys.indexOf(type) !== -1) {
|
||||
@@ -470,7 +475,7 @@ api.buy = {
|
||||
let quantity = 1;
|
||||
if (req.body.quantity) quantity = req.body.quantity;
|
||||
req.quantity = quantity;
|
||||
buyRes = common.ops.buy(user, req, res.analytics);
|
||||
const buyRes = common.ops.buy(user, req, res.analytics);
|
||||
|
||||
await user.save();
|
||||
res.respond(200, ...buyRes);
|
||||
@@ -1003,7 +1008,12 @@ api.userPurchaseHourglass = {
|
||||
const { user } = res.locals;
|
||||
const quantity = req.body.quantity || 1;
|
||||
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();
|
||||
res.respond(200, ...purchaseHourglassRes);
|
||||
},
|
||||
|
||||
@@ -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 timestamp = moment.utc(message.timestamp).zone(timezoneOffset).format(`${dateFormat} HH:mm:ss`);
|
||||
const text = md.render(message.text);
|
||||
index = `(${index + 1}/${inbox.length})`;
|
||||
const pageIndex = `(${index + 1}/${inbox.length})`;
|
||||
messages += `
|
||||
<p>
|
||||
${recipientLabel} <strong>${messageUser}</strong> ${timestamp}
|
||||
${index}
|
||||
${pageIndex}
|
||||
<br />
|
||||
${text}
|
||||
</p>
|
||||
|
||||
@@ -45,7 +45,8 @@ api.unsubscribe = {
|
||||
|
||||
res.send(`<h1>${res.t('unsubscribedSuccessfully')}</h1> ${res.t('unsubscribedTextUsers')}`);
|
||||
} 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() });
|
||||
|
||||
const okResponse = `<h1>${res.t('unsubscribedSuccessfully')}</h1> ${res.t('unsubscribedTextOthers')}`;
|
||||
|
||||
@@ -102,7 +102,9 @@ api.subscribe = {
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
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 { user } = res.locals;
|
||||
const { groupId } = req.body;
|
||||
|
||||
@@ -46,7 +46,13 @@ api.iapSubscriptionAndroid = {
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
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);
|
||||
},
|
||||
|
||||
@@ -155,8 +155,9 @@ api.subscribeCancel = {
|
||||
},
|
||||
};
|
||||
|
||||
// General IPN handler. We catch cancelled Habitica subscriptions 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
|
||||
// General IPN handler. We catch cancelled Habitica subscriptions
|
||||
// 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
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// 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
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
require('@babel/register');
|
||||
require('@babel/register'); // eslint-disable-line import/no-extraneous-dependencies
|
||||
}
|
||||
|
||||
const cluster = require('cluster');
|
||||
|
||||
@@ -15,7 +15,10 @@ const AMPLITUDE_TOKEN = nconf.get('AMPLITUDE_KEY');
|
||||
const GA_TOKEN = nconf.get('GA_ID');
|
||||
const GA_POSSIBLE_LABELS = ['gaLabel', 'itemKey'];
|
||||
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({
|
||||
'habitica-web': 'Web',
|
||||
@@ -31,7 +34,7 @@ const ga = googleAnalytics(GA_TOKEN);
|
||||
const Content = common.content;
|
||||
|
||||
function _lookUpItemName (itemKey) {
|
||||
if (!itemKey) return;
|
||||
if (!itemKey) return null;
|
||||
|
||||
const gear = Content.gear.flat[itemKey];
|
||||
const egg = Content.eggs[itemKey];
|
||||
@@ -186,6 +189,8 @@ function _generateLabelForGoogleAnalytics (data) {
|
||||
label = data[key];
|
||||
return false; // exit each early
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return label;
|
||||
@@ -199,6 +204,8 @@ function _generateValueForGoogleAnalytics (data) {
|
||||
value = data[key];
|
||||
return false; // exit each early
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return value;
|
||||
@@ -225,7 +232,7 @@ function _sendDataToGoogle (eventType, data) {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
ga.event(eventData, err => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -264,7 +271,7 @@ function _sendPurchaseDataToGoogle (data) {
|
||||
const eventPromise = new Promise((resolve, reject) => {
|
||||
ga.event(eventData, err => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -273,7 +280,7 @@ function _sendPurchaseDataToGoogle (data) {
|
||||
.item(price, qty, sku, itemKey, variation)
|
||||
.send(err => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -308,7 +315,8 @@ async function track (eventType, data) {
|
||||
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) {
|
||||
return Promise.all([
|
||||
_sendPurchaseDataToAmplitude(data),
|
||||
|
||||
@@ -12,19 +12,22 @@ import common from '../../../common';
|
||||
import logger from '../logger';
|
||||
import { decrypt } from '../encryption';
|
||||
import { model as Group } from '../../models/group';
|
||||
import { loginSocial } from './social.js';
|
||||
import { loginSocial } from './social';
|
||||
import { loginRes } from './utils';
|
||||
import { verifyUsername } from '../user/validation';
|
||||
|
||||
const USERNAME_LENGTH_MIN = 1;
|
||||
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) {
|
||||
// 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
|
||||
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)
|
||||
if (sentAt && moment().subtract(7, 'days').isAfter(sentAt)) {
|
||||
@@ -66,7 +69,8 @@ function hasBackupAuth (user, networkToRemove) {
|
||||
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;
|
||||
}
|
||||
@@ -100,10 +104,12 @@ async function registerLocal (req, res, { isV3 = false }) {
|
||||
const issues = verifyUsername(req.body.username, res);
|
||||
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
|
||||
// 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();
|
||||
username = username.trim();
|
||||
const lowerCaseUsername = username.toLowerCase();
|
||||
@@ -147,9 +153,10 @@ async function registerLocal (req, res, { isV3 = false }) {
|
||||
|
||||
if (existingUser) {
|
||||
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 false;
|
||||
});
|
||||
if (!hasSocialAuth) throw new NotAuthorized(res.t('onlySocialAttachLocal'));
|
||||
existingUser.auth.local = newUser.auth.local;
|
||||
|
||||
@@ -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 accessToken = req.body.authResponse.access_token;
|
||||
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'));
|
||||
|
||||
const profile = await _passportProfile(network, accessToken);
|
||||
@@ -37,7 +38,7 @@ export async function loginSocial (req, res) {
|
||||
|
||||
// User already signed up
|
||||
if (user) {
|
||||
return loginRes(user, ...arguments);
|
||||
return loginRes(user, req, res);
|
||||
}
|
||||
|
||||
const generatedUsername = generateUsername();
|
||||
@@ -78,10 +79,14 @@ export async function loginSocial (req, res) {
|
||||
user.newUser = true;
|
||||
}
|
||||
|
||||
loginRes(user, ...arguments);
|
||||
loginRes(user, req, res);
|
||||
|
||||
// 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
|
||||
.remove({ email: savedUser.auth[network].emails[0].value.toLowerCase() })
|
||||
.exec()
|
||||
|
||||
@@ -12,7 +12,12 @@ export function generateUsername () {
|
||||
}
|
||||
|
||||
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 = {
|
||||
id: user._id,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import AWS from 'aws-sdk';
|
||||
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'),
|
||||
secretAccessKey: nconf.get('S3_SECRET_ACCESS_KEY'),
|
||||
});
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
// CONTENT WARNING:
|
||||
// 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.
|
||||
// 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.
|
||||
//
|
||||
//
|
||||
@@ -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.
|
||||
|
||||
// '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.
|
||||
|
||||
@@ -55,15 +55,20 @@
|
||||
// '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.
|
||||
// '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 surnames: 'Christ', 'Mead'
|
||||
// Legitimate place names: 'Dyke'
|
||||
//
|
||||
// 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'.
|
||||
// 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.
|
||||
|
||||
@@ -4,7 +4,7 @@ import nconf from 'nconf';
|
||||
|
||||
const MANIFEST_FILE_PATH = path.join(__dirname, '/../../client-old/manifest.json');
|
||||
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 buildFiles = [];
|
||||
@@ -23,7 +23,7 @@ function _walk (folder) {
|
||||
|
||||
if (relFolder) {
|
||||
original = `${relFolder}/${original}`;
|
||||
fileName = `${relFolder}/${fileName}`;
|
||||
fileName = `${relFolder}/${fileName}`; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
buildFiles[original] = fileName;
|
||||
|
||||
@@ -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 uuid from 'uuid';
|
||||
import { model as Challenge } from '../../models/challenge';
|
||||
@@ -11,7 +12,10 @@ import {
|
||||
NotAuthorized,
|
||||
} 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) {
|
||||
if (user.achievements.joinedChallenge) return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { model as User } from '../models/user';
|
||||
import { getUserInfo } from './email';
|
||||
import { model as User } from '../models/user'; // eslint-disable-line import/no-cycle
|
||||
import { getUserInfo } from './email'; // eslint-disable-line import/no-cycle
|
||||
import { sendNotification as sendPushNotification } from './pushNotifications';
|
||||
|
||||
export async function getAuthorEmailFromMessage (message) {
|
||||
@@ -34,7 +34,12 @@ export async function sendChatPushNotifications (user, group, message, translate
|
||||
identifier: 'groupActivity',
|
||||
category: 'groupActivity',
|
||||
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,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import _ from 'lodash';
|
||||
import { chatModel as Chat } from '../../models/message';
|
||||
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;
|
||||
|
||||
@@ -31,14 +34,17 @@ export function translateMessage (lang, info) {
|
||||
const { spells } = shared.content;
|
||||
const { quests } = shared.content;
|
||||
|
||||
switch (info.type) {
|
||||
switch (info.type) { // eslint-disable-line default-case
|
||||
case 'quest_start':
|
||||
msg = shared.i18n.t('chatQuestStarted', { questName: questScrolls[info.quest].text(lang) }, lang);
|
||||
break;
|
||||
|
||||
case 'boss_damage':
|
||||
msg = shared.i18n.t('chatBossDamage', {
|
||||
username: info.user, bossName: questScrolls[info.quest].boss.name(lang), userDamage: info.userDamage, bossDamage: info.bossDamage,
|
||||
username: info.user,
|
||||
bossName: questScrolls[info.quest].boss.name(lang),
|
||||
userDamage: info.userDamage,
|
||||
bossDamage: info.bossDamage,
|
||||
}, lang);
|
||||
break;
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ export default class ChatReporter {
|
||||
this.res = res;
|
||||
}
|
||||
|
||||
async validate () {}
|
||||
async validate () { // eslint-disable-line class-methods-use-this
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async getMessageVariables (group, message) {
|
||||
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,
|
||||
}) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import GroupChatReporter from './groupChatReporter';
|
||||
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') {
|
||||
return new GroupChatReporter(req, res);
|
||||
} if (type === 'Inbox') {
|
||||
return new InboxChatReporter(req, res);
|
||||
}
|
||||
|
||||
throw new Error('Invalid chat reporter type.');
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ import { chatModel as Chat } from '../../models/message';
|
||||
import apiError from '../apiError';
|
||||
|
||||
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
|
||||
|
||||
export default class GroupChatReporter extends ChatReporter {
|
||||
@@ -79,7 +82,7 @@ export default class GroupChatReporter extends ChatReporter {
|
||||
// Arbitrary amount, higher than 2
|
||||
message.flagCount = 5;
|
||||
} else if (increaseFlagCount) {
|
||||
message.flagCount++;
|
||||
message.flagCount += 1;
|
||||
}
|
||||
|
||||
await message.save();
|
||||
|
||||
@@ -12,7 +12,9 @@ import apiError from '../apiError';
|
||||
import * as inboxLib from '../inbox';
|
||||
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 {
|
||||
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) {
|
||||
message.markModified(changedField);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import findIndex from 'lodash/findIndex';
|
||||
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;
|
||||
|
||||
if (isPlainObject(element)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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();
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function recoverCron (status, locals) {
|
||||
if (!reloadedUser) {
|
||||
throw new Error(`User ${user._id} not found while recovering.`);
|
||||
} else if (reloadedUser._cronSignature !== 'NOT_RUNNING') {
|
||||
status.times++;
|
||||
status.times += 1;
|
||||
|
||||
if (status.times < 5) {
|
||||
await recoverCron(status, locals);
|
||||
@@ -45,7 +45,6 @@ export async function recoverCron (status, locals) {
|
||||
}
|
||||
} else {
|
||||
locals.user = reloadedUser;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +58,8 @@ const CLEAR_BUFFS = {
|
||||
};
|
||||
|
||||
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 subscriptionEndDate = moment(plan.dateTerminated).isBefore() ? moment(plan.dateTerminated).startOf('month') : moment(now).startOf('month');
|
||||
const dateUpdatedMoment = moment(plan.dateUpdated).startOf('month');
|
||||
@@ -67,54 +67,74 @@ function grantEndOfTheMonthPerks (user, now) {
|
||||
|
||||
if (elapsedMonths > 0) {
|
||||
plan.dateUpdated = now;
|
||||
// For every month, inc their "consecutive months" counter. 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
|
||||
// For every month, inc their "consecutive months" counter.
|
||||
// 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, {
|
||||
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++) {
|
||||
plan.consecutive.count++;
|
||||
for (let i = 0; i < elapsedMonths; i += 1) {
|
||||
plan.consecutive.count += 1;
|
||||
|
||||
plan.consecutive.offset--;
|
||||
// If offset is now greater than 0, the user is within a period for which they have already been given the consecutive months perks.
|
||||
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 now equals 0, this is the final month for which 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 now equals 0, this is the final month for which
|
||||
// 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,
|
||||
// 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
|
||||
// If offset is now less than 0, the user EITHER has
|
||||
// a single-month recurring subscription and MIGHT be due for perks,
|
||||
// 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).
|
||||
|
||||
if (plan.consecutive.offset < 0) {
|
||||
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 match = plan.planId.match(planIdRegExp);
|
||||
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) {
|
||||
// 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
|
||||
perkAmountNeeded = 1;
|
||||
}
|
||||
plan.consecutive.offset = 0; // allow the same logic to be run next month
|
||||
} else {
|
||||
// User has a multi-month recurring subscription and it renewed in the previous calendar month.
|
||||
perkAmountNeeded = planMonthsLength / SUBSCRIPTION_BASIC_BLOCK_LENGTH; // e.g., for a 6-month subscription, give two sets of perks
|
||||
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)
|
||||
// User has a multi-month recurring subscription
|
||||
// and it renewed in the previous calendar 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) {
|
||||
plan.consecutive.trinkets += perkAmountNeeded; // one Hourglass 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
|
||||
let resetWeekly = false;
|
||||
let resetMonthly = false;
|
||||
for (let i = 0; i < daysMissed; i++) {
|
||||
for (let i = 0; i < daysMissed; i += 1) {
|
||||
if (resetWeekly === true && resetMonthly === true) {
|
||||
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) {
|
||||
resetWeekly = true;
|
||||
}
|
||||
@@ -189,7 +211,10 @@ function trackCronAnalytics (analytics, user, _progress, options) {
|
||||
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', {
|
||||
category: 'behavior',
|
||||
uuid: user._id,
|
||||
@@ -278,22 +303,28 @@ export function cron (options = {}) {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Login Incentives
|
||||
user.loginIncentives++;
|
||||
user.loginIncentives += 1;
|
||||
awardLoginIncentives(user);
|
||||
|
||||
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.
|
||||
|
||||
// Tally each task
|
||||
let todoTally = 0;
|
||||
|
||||
tasksByType.todos.forEach(task => { // make uncompleted To-Dos redder (further incentive to complete them)
|
||||
if (task.group.assignedDate && moment(task.group.assignedDate).isAfter(user.auth.timestamps.updated)) return;
|
||||
// make uncompleted To-Dos redder (further incentive to complete them)
|
||||
tasksByType.todos.forEach(task => {
|
||||
if (
|
||||
task.group.assignedDate
|
||||
&& moment(task.group.assignedDate).isAfter(user.auth.timestamps.updated)
|
||||
) return;
|
||||
scoreTask({
|
||||
task,
|
||||
user,
|
||||
@@ -305,7 +336,8 @@ export function cron (options = {}) {
|
||||
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.
|
||||
let dailyChecked = 0; // how many dailies were 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;
|
||||
|
||||
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;
|
||||
// Deduct points for missed Daily tasks
|
||||
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
|
||||
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 });
|
||||
|
||||
if (shouldDo(thatDay.toDate(), task, user.preferences)) {
|
||||
atLeastOneDailyDue = true;
|
||||
scheduleMisses++;
|
||||
scheduleMisses += 1;
|
||||
if (user.stats.buffs.stealth) {
|
||||
user.stats.buffs.stealth--;
|
||||
EvadeTask++;
|
||||
user.stats.buffs.stealth -= 1;
|
||||
EvadeTask += 1;
|
||||
}
|
||||
}
|
||||
if (multiDaysCountAsOneDay) break;
|
||||
}
|
||||
|
||||
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) {
|
||||
dailyChecked += 1; // allows full allotment of mp to be gained
|
||||
} else {
|
||||
perfect = false;
|
||||
|
||||
if (task.checklist && task.checklist.length > 0) { // Partially completed checklists dock fewer mana points
|
||||
const fractionChecked = _.reduce(task.checklist, (m, i) => m + (i.completed ? 1 : 0), 0) / task.checklist.length;
|
||||
// Partially completed checklists dock fewer mana points
|
||||
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;
|
||||
dailyChecked += fractionChecked;
|
||||
} else {
|
||||
@@ -370,7 +411,8 @@ export function cron (options = {}) {
|
||||
if (!CRON_SEMI_SAFE_MODE) {
|
||||
// Apply damage from a boss, less damage for Trivial priority (difficulty)
|
||||
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
|
||||
// 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
|
||||
@@ -392,7 +434,7 @@ export function cron (options = {}) {
|
||||
|
||||
if (completed || scheduleMisses > 0) {
|
||||
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 lvl = 0; // iterator
|
||||
while (lvl < user.stats.lvl - 1) {
|
||||
lvl++;
|
||||
lvl += 1;
|
||||
expTally += common.tnl(lvl);
|
||||
}
|
||||
|
||||
user.history.exp.push({ date: now, value: expTally });
|
||||
|
||||
// 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
|
||||
|
||||
// preen user history so that it doesn't become a performance problem
|
||||
@@ -442,7 +488,7 @@ export function cron (options = {}) {
|
||||
preenUserHistory(user, tasksByType);
|
||||
|
||||
if (perfect && atLeastOneDailyDue) {
|
||||
user.achievements.perfect++;
|
||||
user.achievements.perfect += 1;
|
||||
const lvlDiv2 = Math.ceil(common.capByLevel(user.stats.lvl) / 2);
|
||||
user.stats.buffs = {
|
||||
str: lvlDiv2,
|
||||
@@ -456,15 +502,19 @@ export function cron (options = {}) {
|
||||
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
|
||||
if (!user.preferences.sleep) {
|
||||
if (dailyDueUnchecked === 0 && dailyChecked === 0) dailyChecked = 1;
|
||||
user.stats.mp += _.max([10, 0.1 * common.statsComputed(user).maxMP]) * dailyChecked / (dailyDueUnchecked + dailyChecked);
|
||||
if (user.stats.mp > common.statsComputed(user).maxMP) user.stats.mp = common.statsComputed(user).maxMP;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const { progress } = user.party.quest;
|
||||
_progress = progress.toObject(); // clone the old progress object
|
||||
@@ -488,7 +538,7 @@ export function cron (options = {}) {
|
||||
});
|
||||
|
||||
// Analytics
|
||||
user.flags.cronCount++;
|
||||
user.flags.cronCount += 1;
|
||||
trackCronAnalytics(analytics, user, _progress, options);
|
||||
|
||||
return _progress;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import nconf from 'nconf';
|
||||
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 logger from './logger';
|
||||
import common from '../../common';
|
||||
@@ -27,7 +27,12 @@ export function getUserInfo (user, fields = []) {
|
||||
info.email = user.auth.local.email;
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
});
|
||||
@@ -62,23 +67,26 @@ export function getGroupUrl (group) {
|
||||
|
||||
// Send a transactional email using Mandrill through the external email server
|
||||
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 },
|
||||
].concat(variables || []);
|
||||
|
||||
// It's important to pass at least a user with its `preferences` as we need to check if he unsubscribed
|
||||
mailingInfoArray = mailingInfoArray.map(mailingInfo => (mailingInfo._id ? getUserInfo(mailingInfo, ['_id', 'email', 'name', 'canSend']) : mailingInfo)).filter(mailingInfo =>
|
||||
// It's important to pass at least a user with its `preferences`
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
if (!personalVariables || personalVariables.length === 0) {
|
||||
personalVariables = mailingInfoArray.map(mailingInfo => ({
|
||||
personalVariables = mailingInfoArray.map(mailingInfo => ({ // eslint-disable-line no-param-reassign, max-len
|
||||
rcpt: mailingInfo.email,
|
||||
vars: [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import common from '../../common';
|
||||
import common from '../../common'; // eslint-disable-line max-classes-per-file
|
||||
|
||||
export const { CustomError } = common.errors;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* 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 = [
|
||||
'TESTPLACEHOLDERSWEARWORDHERE',
|
||||
'TESTPLACEHOLDERSWEARWORDHERE1',
|
||||
|
||||
@@ -3,10 +3,8 @@ import nconf from 'nconf';
|
||||
const IS_PROD = nconf.get('IS_PROD');
|
||||
const STACKDRIVER_TRACING_ENABLED = nconf.get('ENABLE_STACKDRIVER_TRACING') === 'true';
|
||||
|
||||
let tracer = null;
|
||||
|
||||
if (IS_PROD && STACKDRIVER_TRACING_ENABLED) {
|
||||
tracer = require('@google-cloud/trace-agent').start(); // eslint-disable-line global-require
|
||||
}
|
||||
const tracer = IS_PROD && STACKDRIVER_TRACING_ENABLED
|
||||
? require('@google-cloud/trace-agent').start()
|
||||
: null;
|
||||
|
||||
export default tracer;
|
||||
|
||||
@@ -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 = {
|
||||
default: 'recurringCompletion',
|
||||
@@ -28,7 +28,7 @@ async function _evaluateAllAssignedCompletion (masterTask) {
|
||||
'group.taskId': masterTask._id,
|
||||
'group.approval.approved': true,
|
||||
}).exec();
|
||||
completions++;
|
||||
completions += 1;
|
||||
} else {
|
||||
completions = await Tasks.Task.count({
|
||||
'group.taskId': masterTask._id,
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
// - literature guilds (quoting of passages containing words banned as oaths);
|
||||
// - food/drink/lifestyle/perfume guilds (alcohol allowed);
|
||||
// - 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.
|
||||
// Later, it will be replaced with customised lists of disallowed words based on the guilds' tags.
|
||||
|
||||
@@ -61,7 +61,7 @@ function _loadTranslations (locale) {
|
||||
if (path.extname(file) !== '.json') return;
|
||||
|
||||
// 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;
|
||||
|
||||
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
|
||||
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
|
||||
// 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');
|
||||
|
||||
// A map of languages that have different versions and the relative versions
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
const PM_PER_PAGE = 10;
|
||||
@@ -18,7 +18,11 @@ export async function sentMessage (sender, receiver, message, translate) {
|
||||
sendPushNotification(
|
||||
receiver,
|
||||
{
|
||||
title: translate('newPMNotificationTitle', { name: getUserInfo(sender, ['name']).name }, receiver.preferences.language),
|
||||
title: translate(
|
||||
'newPMNotificationTitle',
|
||||
{ name: getUserInfo(sender, ['name']).name },
|
||||
receiver.preferences.language,
|
||||
),
|
||||
message,
|
||||
identifier: 'newPM',
|
||||
category: 'newPM',
|
||||
@@ -71,7 +75,7 @@ export async function getUserInbox (user, options = {
|
||||
return messages;
|
||||
}
|
||||
const messagesObj = {};
|
||||
messages.forEach(msg => messagesObj[msg._id] = msg);
|
||||
messages.forEach(msg => { messagesObj[msg._id] = msg; });
|
||||
|
||||
return messagesObj;
|
||||
}
|
||||
|
||||
@@ -121,6 +121,8 @@ async function addInvitationToUser (userToInvite, group, inviter, res) {
|
||||
if (group.type === 'party') {
|
||||
return userInvited.invitations.parties[userToInvite.invitations.parties.length - 1];
|
||||
}
|
||||
|
||||
throw new Error('Invalid group type');
|
||||
}
|
||||
|
||||
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);
|
||||
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) {
|
||||
@@ -191,8 +196,8 @@ async function inviteByEmail (invite, group, inviter, req, res) {
|
||||
}
|
||||
|
||||
async function inviteByUserName (username, group, inviter, req, res) {
|
||||
if (username.indexOf('@') === 0) username = username.slice(1, username.length);
|
||||
username = username.toLowerCase();
|
||||
if (username.indexOf('@') === 0) username = username.slice(1, username.length); // eslint-disable-line no-param-reassign
|
||||
username = username.toLowerCase(); // eslint-disable-line no-param-reassign
|
||||
const userToInvite = await User.findOne({ 'auth.local.lowerCaseUsername': username }).exec();
|
||||
|
||||
if (!userToInvite) {
|
||||
@@ -203,7 +208,7 @@ async function inviteByUserName (username, group, inviter, req, res) {
|
||||
throw new BadRequest(res.t('cannotInviteSelfToGroup'));
|
||||
}
|
||||
|
||||
return await addInvitationToUser(userToInvite, group, inviter, res);
|
||||
return addInvitationToUser(userToInvite, group, inviter, res);
|
||||
}
|
||||
|
||||
export {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { last } from 'lodash';
|
||||
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
|
||||
const defaultOwnedGear = {};
|
||||
@@ -54,6 +54,8 @@ export function validateItemPath (itemPath) {
|
||||
if (itemPath.indexOf('items.quests') === 0) {
|
||||
return Boolean(shared.content.quests[key]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// When passed a value of an item in the user object it'll convert the
|
||||
|
||||
@@ -28,7 +28,9 @@ if (IS_PROD) {
|
||||
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
|
||||
.add(winston.transports.Console, {
|
||||
timestamp: true,
|
||||
@@ -55,7 +57,8 @@ const loggerInterface = {
|
||||
const stack = err.stack || err.message || err;
|
||||
|
||||
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
|
||||
if (err instanceof CustomError) {
|
||||
const errWithoutCommonProps = _.omit(err, ['name', 'httpCode', 'message']);
|
||||
|
||||
@@ -46,9 +46,9 @@ export async function compare (user, passwordToCheck) {
|
||||
const passwordSalt = user.auth.local.salt; // Only used for SHA1
|
||||
|
||||
if (passwordHashMethod === 'bcrypt') {
|
||||
return await bcryptCompare(passwordToCheck, passwordHash);
|
||||
return bcryptCompare(passwordToCheck, passwordHash);
|
||||
// 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);
|
||||
}
|
||||
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.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
|
||||
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
} from '../errors';
|
||||
import payments from './payments';
|
||||
import { model as User } from '../../models/user';
|
||||
import {
|
||||
import payments from './payments'; // eslint-disable-line import/no-cycle
|
||||
import { model as User } from '../../models/user'; // eslint-disable-line import/no-cycle
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
model as Group,
|
||||
basicFields as basicGroupFields,
|
||||
} from '../../models/group';
|
||||
@@ -52,14 +52,30 @@ api.constants = {
|
||||
};
|
||||
|
||||
api.getTokenInfo = util.promisify(amzPayment.api.getTokenInfo).bind(amzPayment.api);
|
||||
api.createOrderReferenceId = util.promisify(amzPayment.offAmazonPayments.createOrderReferenceForId).bind(amzPayment.offAmazonPayments);
|
||||
api.setOrderReferenceDetails = util.promisify(amzPayment.offAmazonPayments.setOrderReferenceDetails).bind(amzPayment.offAmazonPayments);
|
||||
api.confirmOrderReference = util.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.createOrderReferenceId = util
|
||||
.promisify(amzPayment.offAmazonPayments.createOrderReferenceForId)
|
||||
.bind(amzPayment.offAmazonPayments);
|
||||
api.setOrderReferenceDetails = util
|
||||
.promisify(amzPayment.offAmazonPayments.setOrderReferenceDetails)
|
||||
.bind(amzPayment.offAmazonPayments);
|
||||
api.confirmOrderReference = util
|
||||
.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) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -158,7 +174,9 @@ api.checkout = async function checkout (options = {}) {
|
||||
};
|
||||
|
||||
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();
|
||||
data.gift = gift;
|
||||
data.paymentMethod = this.constants.PAYMENT_METHOD_GIFT;
|
||||
@@ -217,8 +235,12 @@ api.cancelSubscription = async function cancelSubscription (options = {}) {
|
||||
}).catch(err => err);
|
||||
|
||||
const badBAStates = ['Canceled', 'Closed', 'Suspended'];
|
||||
if (details && details.BillingAgreementDetails && details.BillingAgreementDetails.BillingAgreementStatus
|
||||
&& badBAStates.indexOf(details.BillingAgreementDetails.BillingAgreementStatus.State) === -1) {
|
||||
if (
|
||||
details
|
||||
&& details.BillingAgreementDetails
|
||||
&& details.BillingAgreementDetails.BillingAgreementStatus
|
||||
&& badBAStates.indexOf(details.BillingAgreementDetails.BillingAgreementStatus.State) === -1
|
||||
) {
|
||||
await this.closeBillingAgreement({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
});
|
||||
|
||||
@@ -38,15 +38,16 @@ api.verifyGemPurchase = async function verifyGemPurchase (options) {
|
||||
const isValidated = iap.isValidated(appleRes);
|
||||
if (!isValidated) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT);
|
||||
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;
|
||||
|
||||
// Purchasing one item at a time (processing of await(s) below is sequential not parallel)
|
||||
for (const index in purchaseDataList) {
|
||||
const purchaseData = purchaseDataList[index];
|
||||
for (const purchaseData of purchaseDataList) {
|
||||
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,
|
||||
}).exec();
|
||||
|
||||
@@ -59,7 +60,7 @@ api.verifyGemPurchase = async function verifyGemPurchase (options) {
|
||||
});
|
||||
|
||||
let amount;
|
||||
switch (purchaseData.productId) {
|
||||
switch (purchaseData.productId) { // eslint-disable-line default-case
|
||||
case 'com.habitrpg.ios.Habitica.4gems':
|
||||
amount = 1;
|
||||
break;
|
||||
@@ -99,7 +100,7 @@ api.subscribe = async function subscribe (sku, user, receipt, headers, nextPayme
|
||||
if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode'));
|
||||
|
||||
let subCode;
|
||||
switch (sku) {
|
||||
switch (sku) { // eslint-disable-line default-case
|
||||
case 'subscription1month':
|
||||
subCode = 'basic_earned';
|
||||
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);
|
||||
|
||||
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;
|
||||
|
||||
for (const index in purchaseDataList) {
|
||||
const purchaseData = purchaseDataList[index];
|
||||
|
||||
for (const purchaseData of purchaseDataList) {
|
||||
const dateTerminated = new Date(Number(purchaseData.expirationDate));
|
||||
if (purchaseData.productId === sku && dateTerminated > new Date()) {
|
||||
transactionId = purchaseData.transactionId;
|
||||
@@ -142,7 +143,7 @@ api.subscribe = async function subscribe (sku, user, receipt, headers, nextPayme
|
||||
}).exec();
|
||||
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({
|
||||
user,
|
||||
@@ -166,7 +167,7 @@ api.noRenewSubscribe = async function noRenewSubscribe (options) {
|
||||
if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode'));
|
||||
|
||||
let subCode;
|
||||
switch (sku) {
|
||||
switch (sku) { // eslint-disable-line default-case
|
||||
case 'com.habitrpg.ios.habitica.norenew_subscription.1month':
|
||||
subCode = 'basic_earned';
|
||||
break;
|
||||
@@ -189,13 +190,13 @@ api.noRenewSubscribe = async function noRenewSubscribe (options) {
|
||||
if (!isValidated) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT);
|
||||
|
||||
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;
|
||||
|
||||
for (const index in purchaseDataList) {
|
||||
const purchaseData = purchaseDataList[index];
|
||||
|
||||
for (const purchaseData of purchaseDataList) {
|
||||
const dateTerminated = new Date(Number(purchaseData.expirationDate));
|
||||
if (purchaseData.productId === sku && dateTerminated > new Date()) {
|
||||
transactionId = purchaseData.transactionId;
|
||||
@@ -204,7 +205,7 @@ api.noRenewSubscribe = async function noRenewSubscribe (options) {
|
||||
}
|
||||
|
||||
if (transactionId) {
|
||||
const existingReceipt = await IapPurchaseReceipt.findOne({ // eslint-disable-line no-await-in-loop
|
||||
const existingReceipt = await IapPurchaseReceipt.findOne({
|
||||
_id: transactionId,
|
||||
}).exec();
|
||||
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);
|
||||
} catch (err) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as analytics from '../analyticsService';
|
||||
import {
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
getUserInfo,
|
||||
sendTxn as txnEmail,
|
||||
} from '../email';
|
||||
@@ -38,7 +38,10 @@ async function buyGemGift (data) {
|
||||
}
|
||||
|
||||
// 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(
|
||||
data.gift.member,
|
||||
{
|
||||
@@ -73,7 +76,7 @@ async function buyGems (data) {
|
||||
const amt = getAmountForGems(data);
|
||||
|
||||
updateUserBalance(data, amt);
|
||||
data.user.purchased.txnCount++;
|
||||
data.user.purchased.txnCount += 1;
|
||||
|
||||
if (!data.gift) txnEmail(data.user, 'donation');
|
||||
|
||||
@@ -94,4 +97,4 @@ async function buyGems (data) {
|
||||
await data.user.save();
|
||||
}
|
||||
|
||||
export { buyGems };
|
||||
export { buyGems }; // eslint-disable-line import/prefer-default-export
|
||||
|
||||
@@ -61,7 +61,7 @@ api.verifyGemPurchase = async function verifyGemPurchase (options) {
|
||||
|
||||
let amount;
|
||||
|
||||
switch (receiptObj.productId) {
|
||||
switch (receiptObj.productId) { // eslint-disable-line default-case
|
||||
case 'com.habitrpg.android.habitica.iap.4gems':
|
||||
amount = 1;
|
||||
break;
|
||||
@@ -89,10 +89,13 @@ api.verifyGemPurchase = async function verifyGemPurchase (options) {
|
||||
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'));
|
||||
let subCode;
|
||||
switch (sku) {
|
||||
switch (sku) { // eslint-disable-line default-case
|
||||
case 'com.habitrpg.android.habitica.subscription.1month':
|
||||
subCode = 'basic_earned';
|
||||
break;
|
||||
@@ -129,7 +132,7 @@ api.subscribe = async function subscribe (sku, user, receipt, signature, headers
|
||||
const isValidated = iap.isValidated(googleRes);
|
||||
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({
|
||||
user,
|
||||
@@ -148,7 +151,7 @@ api.noRenewSubscribe = async function noRenewSubscribe (options) {
|
||||
} = options;
|
||||
if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode'));
|
||||
let subCode;
|
||||
switch (sku) {
|
||||
switch (sku) { // eslint-disable-line default-case
|
||||
case 'com.habitrpg.android.habitica.norenew_subscription.1month':
|
||||
subCode = 'basic_earned';
|
||||
break;
|
||||
|
||||
@@ -2,12 +2,12 @@ import nconf from 'nconf';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import { model as User } from '../../models/user';
|
||||
import {
|
||||
import { model as User } from '../../models/user'; // eslint-disable-line import/no-cycle
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
model as Group,
|
||||
basicFields as basicGroupFields,
|
||||
} from '../../models/group';
|
||||
import {
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
getUserInfo,
|
||||
sendTxn as txnEmail,
|
||||
} from '../email';
|
||||
@@ -61,8 +61,14 @@ async function addSubToGroupUser (member, group) {
|
||||
|
||||
// When changing customerIdsToIgnore or paymentMethodsToIgnore, the code blocks below for
|
||||
// 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 paymentMethodsToIgnore = [this.constants.GOOGLE_PAYMENT_METHOD, this.constants.IOS_PAYMENT_METHOD];
|
||||
const customerIdsToIgnore = [
|
||||
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;
|
||||
const leader = await User.findById(group.leader).exec();
|
||||
|
||||
@@ -96,7 +102,10 @@ async function addSubToGroupUser (member, group) {
|
||||
|
||||
const memberPlan = member.purchased.plan;
|
||||
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 ignoreCustomerId = customerIdsToIgnore.indexOf(memberPlan.customerId) !== -1;
|
||||
|
||||
@@ -108,7 +117,9 @@ async function addSubToGroupUser (member, group) {
|
||||
{ name: 'PAYMENT_METHOD', content: memberPlan.paymentMethod },
|
||||
{ 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.' },
|
||||
// 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.
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import {
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
addSubscriptionToGroupUsers,
|
||||
addSubToGroupUser,
|
||||
cancelGroupUsersSubscription,
|
||||
cancelGroupSubscriptionForUser,
|
||||
} from './groupPayments';
|
||||
import {
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
createSubscription,
|
||||
cancelSubscription,
|
||||
} from './subscriptions';
|
||||
import {
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
buyGems,
|
||||
} from './gems';
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ import ipn from 'paypal-ipn';
|
||||
import paypal from 'paypal-rest-sdk';
|
||||
import cc from 'coupon-code';
|
||||
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 User } from '../../models/user';
|
||||
import {
|
||||
import { model as User } from '../../models/user'; // eslint-disable-line import/no-cycle
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
model as Group,
|
||||
basicFields as basicGroupFields,
|
||||
} from '../../models/group';
|
||||
@@ -24,8 +24,10 @@ import {
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
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
|
||||
// a web interface for billing-plan creation), see ./paypalBillingSetup.js for how. After the billing plan is created
|
||||
// This is the plan.id for paypal subscriptions.
|
||||
// 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
|
||||
_.each(shared.content.subscriptionBlocks, block => {
|
||||
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.paypalPaymentExecute = util.promisify(paypal.payment.execute.bind(paypal.payment));
|
||||
api.paypalBillingAgreementCreate = util.promisify(paypal.billingAgreement.create.bind(paypal.billingAgreement));
|
||||
api.paypalBillingAgreementExecute = util.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.paypalBillingAgreementCreate = util
|
||||
.promisify(paypal.billingAgreement.create.bind(paypal.billingAgreement));
|
||||
api.paypalBillingAgreementExecute = util
|
||||
.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));
|
||||
|
||||
@@ -272,7 +278,8 @@ api.ipn = async function ipnApi (options = {}) {
|
||||
const user = await User.findOne({ 'purchased.plan.customerId': recurring_payment_id }).exec();
|
||||
if (user) {
|
||||
// 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;
|
||||
|
||||
await payments.cancelSubscription({ user, paymentMethod: this.constants.PAYMENT_METHOD });
|
||||
@@ -287,10 +294,14 @@ api.ipn = async function ipnApi (options = {}) {
|
||||
|
||||
if (group) {
|
||||
// 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;
|
||||
|
||||
await payments.cancelSubscription({ groupId: group._id, paymentMethod: this.constants.PAYMENT_METHOD });
|
||||
await payments.cancelSubscription({
|
||||
groupId: group._id,
|
||||
paymentMethod: this.constants.PAYMENT_METHOD,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,15 +6,15 @@ import {
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
} from '../errors';
|
||||
import payments from './payments';
|
||||
import { model as User } from '../../models/user';
|
||||
import {
|
||||
import payments from './payments'; // eslint-disable-line import/no-cycle
|
||||
import { model as User } from '../../models/user'; // eslint-disable-line import/no-cycle
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
model as Group,
|
||||
basicFields as basicGroupFields,
|
||||
} from '../../models/group';
|
||||
import shared from '../../../common';
|
||||
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';
|
||||
|
||||
const { i18n } = shared;
|
||||
@@ -54,7 +54,8 @@ api.editSubscription = async function editSubscription (options, stripeInc) {
|
||||
const { token, groupId, user } = options;
|
||||
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();
|
||||
if (stripeInc) stripeApi = stripeInc;
|
||||
|
||||
@@ -81,7 +82,8 @@ api.editSubscription = async function editSubscription (options, stripeInc) {
|
||||
if (!customerId) throw new NotAuthorized(i18n.t('missingSubscription'));
|
||||
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;
|
||||
await stripeApi.subscriptions.update(subscriptionId, { card: token });
|
||||
};
|
||||
@@ -100,7 +102,8 @@ api.cancelSubscription = async function cancelSubscription (options, stripeInc)
|
||||
const { groupId, user, cancellationReason } = options;
|
||||
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();
|
||||
if (stripeInc) stripeApi = stripeInc;
|
||||
|
||||
@@ -133,7 +136,7 @@ api.cancelSubscription = async function cancelSubscription (options, stripeInc)
|
||||
if (customer && (customer.subscription || customer.subscriptions)) {
|
||||
let { subscription } = customer;
|
||||
if (!subscription && customer.subscriptions) {
|
||||
subscription = customer.subscriptions.data[0];
|
||||
subscription = [customer.subscriptions.data];
|
||||
}
|
||||
await stripeApi.customers.del(customerId);
|
||||
|
||||
@@ -178,7 +181,8 @@ api.chargeForAdditionalGroupMember = async function chargeForAdditionalGroupMemb
|
||||
api.handleWebhooks = async function handleWebhooks (options, stripeInc) {
|
||||
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();
|
||||
if (stripeInc) stripeApi = stripeInc;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import cc from 'coupon-code';
|
||||
|
||||
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 {
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
model as Group,
|
||||
basicFields as basicGroupFields,
|
||||
} from '../../../models/group';
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../errors';
|
||||
import payments from '../payments';
|
||||
import payments from '../payments'; // eslint-disable-line import/no-cycle
|
||||
import stripeConstants from './constants';
|
||||
|
||||
function getGiftAmount (gift) {
|
||||
@@ -24,7 +24,7 @@ function getGiftAmount (gift) {
|
||||
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) {
|
||||
@@ -50,7 +50,8 @@ async function buyGems (gift, user, token, stripeApi) {
|
||||
async function buySubscription (sub, coupon, email, user, token, groupId, stripeApi) {
|
||||
if (sub.discount) {
|
||||
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'));
|
||||
}
|
||||
|
||||
@@ -110,7 +111,8 @@ async function checkout (options, stripeInc) {
|
||||
let response;
|
||||
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();
|
||||
if (stripeInc) stripeApi = stripeInc;
|
||||
|
||||
@@ -122,7 +124,9 @@ async function checkout (options, stripeInc) {
|
||||
}
|
||||
|
||||
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;
|
||||
response = subResponse;
|
||||
} else {
|
||||
@@ -145,4 +149,4 @@ async function checkout (options, stripeInc) {
|
||||
await applyGemPayment(user, response, gift);
|
||||
}
|
||||
|
||||
export { checkout };
|
||||
export { checkout }; // eslint-disable-line import/prefer-default-export
|
||||
|
||||
@@ -2,12 +2,12 @@ import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import * as analytics from '../analyticsService';
|
||||
import * as slack from '../slack';
|
||||
import {
|
||||
import * as slack from '../slack'; // eslint-disable-line import/no-cycle
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
getUserInfo,
|
||||
sendTxn as txnEmail,
|
||||
} from '../email';
|
||||
import {
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
model as Group,
|
||||
basicFields as basicGroupFields,
|
||||
} from '../../models/group';
|
||||
@@ -50,11 +50,12 @@ function _dateDiff (earlyDate, lateDate) {
|
||||
|
||||
async function createSubscription (data) {
|
||||
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 months = Number(block.months);
|
||||
const today = new Date();
|
||||
let plan;
|
||||
let group;
|
||||
let groupId;
|
||||
let itemPurchased = 'Subscription';
|
||||
@@ -86,7 +87,7 @@ async function createSubscription (data) {
|
||||
await this.addSubscriptionToGroupUsers(group);
|
||||
}
|
||||
|
||||
plan = recipient.purchased.plan;
|
||||
const { plan } = recipient.purchased;
|
||||
|
||||
if (data.gift || !autoRenews) {
|
||||
if (plan.customerId && !plan.dateTerminated) { // User has active plan
|
||||
@@ -165,7 +166,7 @@ async function createSubscription (data) {
|
||||
headers: data.headers,
|
||||
});
|
||||
|
||||
if (!group) data.user.purchased.txnCount++;
|
||||
if (!group) data.user.purchased.txnCount += 1;
|
||||
|
||||
if (data.gift) {
|
||||
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) {
|
||||
sendPushNotification(data.gift.member,
|
||||
{
|
||||
@@ -293,7 +295,8 @@ async function cancelSubscription (data) {
|
||||
.add({ days: extraDays })
|
||||
.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) {
|
||||
await group.save();
|
||||
|
||||
@@ -15,7 +15,11 @@ function _aggregate (history, aggregateBy, timezoneOffset, dayStart) {
|
||||
const entries = keyEntryPair[1]; // 1 is entry, 0 is key
|
||||
return {
|
||||
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();
|
||||
|
||||
@@ -36,7 +36,7 @@ function sendNotification (user, details = {}) {
|
||||
payload.identifier = details.identifier;
|
||||
|
||||
_.each(pushDevices, pushDevice => {
|
||||
switch (pushDevice.type) {
|
||||
switch (pushDevice.type) { // eslint-disable-line default-case
|
||||
case 'android':
|
||||
// Required for fcm to be received in background
|
||||
payload.title = details.title;
|
||||
@@ -82,5 +82,5 @@ function sendNotification (user, details = {}) {
|
||||
}
|
||||
|
||||
export {
|
||||
sendNotification,
|
||||
sendNotification, // eslint-disable-line import/prefer-default-export
|
||||
};
|
||||
|
||||
@@ -12,8 +12,9 @@ const noop = (req, res, next) => next();
|
||||
export function readController (router, controller, overrides = []) {
|
||||
_.each(controller, action => {
|
||||
let {
|
||||
method, url, middlewares = [], handler,
|
||||
method,
|
||||
} = action;
|
||||
const { url, middlewares = [], handler } = action;
|
||||
|
||||
// Allow to specify a list of routes (METHOD + URL) to skip
|
||||
if (overrides.indexOf(`${method}-${url}`) !== -1) return;
|
||||
@@ -30,7 +31,8 @@ export function readController (router, controller, overrides = []) {
|
||||
const middlewaresToAdd = [getUserLanguage];
|
||||
|
||||
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) {
|
||||
middlewares.push(...middlewaresToAdd);
|
||||
} else {
|
||||
@@ -55,7 +57,7 @@ export function walkControllers (router, filePath, overrides) {
|
||||
if (!fs.statSync(filePath + fileName).isFile()) {
|
||||
walkControllers(router, `${filePath}${fileName}/`, overrides);
|
||||
} 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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,7 +14,8 @@ passport.serializeUser((user, done) => done(null, user));
|
||||
passport.deserializeUser((obj, done) => done(null, obj));
|
||||
|
||||
// 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
|
||||
passport.use(new FacebookStrategy({
|
||||
clientID: nconf.get('FACEBOOK_KEY'),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { IncomingWebhook } from '@slack/client';
|
||||
import nconf from 'nconf';
|
||||
import moment from 'moment';
|
||||
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_FOOTER_LINK = nconf.get('SLACK_FLAGGING_FOOTER_LINK');
|
||||
@@ -24,11 +24,12 @@ try {
|
||||
logger.error(err);
|
||||
|
||||
if (!IS_PRODUCTION) {
|
||||
flagSlack = subscriptionSlack = {
|
||||
subscriptionSlack = {
|
||||
send (data) {
|
||||
logger.info('Data sent to slack', data);
|
||||
},
|
||||
};
|
||||
flagSlack = subscriptionSlack;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +117,6 @@ function sendInboxFlagNotification ({
|
||||
return;
|
||||
}
|
||||
const titleLink = '';
|
||||
let authorName;
|
||||
const title = `Flag in ${flagger.profile.name}'s Inbox`;
|
||||
let text = `${flagger.profile.name} (${flagger.id}; language: ${flagger.preferences.language}) flagged a PM`;
|
||||
const footer = '';
|
||||
@@ -150,7 +150,7 @@ function sendInboxFlagNotification ({
|
||||
recipient = flaggerFormat;
|
||||
}
|
||||
|
||||
authorName = `${sender} wrote this message to ${recipient}.`;
|
||||
const authorName = `${sender} wrote this message to ${recipient}.`;
|
||||
|
||||
flagSlack.send({
|
||||
text,
|
||||
@@ -203,18 +203,17 @@ function sendShadowMutedPostNotification ({
|
||||
if (SKIP_FLAG_METHODS) {
|
||||
return;
|
||||
}
|
||||
let titleLink;
|
||||
let authorName;
|
||||
const title = `Shadow-Muted Post in ${group.name}`;
|
||||
const text = `@${author.auth.local.username} / ${author.profile.name} posted while shadow-muted`;
|
||||
|
||||
let titleLink;
|
||||
if (group.id === TAVERN_ID) {
|
||||
titleLink = `${BASE_URL}/groups/tavern`;
|
||||
} else {
|
||||
titleLink = `${BASE_URL}/groups/guild/${group.id}`;
|
||||
}
|
||||
|
||||
authorName = formatUser({
|
||||
const authorName = formatUser({
|
||||
name: author.auth.local.username,
|
||||
displayName: author.profile.name,
|
||||
email: authorEmail,
|
||||
@@ -246,11 +245,11 @@ function sendSlurNotification ({
|
||||
if (SKIP_FLAG_METHODS) {
|
||||
return;
|
||||
}
|
||||
let titleLink;
|
||||
let authorName;
|
||||
let title = `Slur in ${group.name}`;
|
||||
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) {
|
||||
titleLink = `${BASE_URL}/groups/tavern`;
|
||||
} else if (group.privacy === 'public') {
|
||||
@@ -259,7 +258,7 @@ function sendSlurNotification ({
|
||||
title += ` - (${group.privacy} ${group.type})`;
|
||||
}
|
||||
|
||||
authorName = formatUser({
|
||||
const authorName = formatUser({
|
||||
name: author.auth.local.username,
|
||||
displayName: author.profile.name,
|
||||
email: authorEmail,
|
||||
|
||||
@@ -73,9 +73,10 @@ async function castSelfSpell (req, user, spell, quantity = 1) {
|
||||
|
||||
async function castPartySpell (req, party, partyMembers, user, spell, quantity = 1) {
|
||||
if (!party) {
|
||||
partyMembers = [user]; // Act as solo party
|
||||
// Act as solo party
|
||||
partyMembers = [user]; // eslint-disable-line no-param-reassign
|
||||
} else {
|
||||
partyMembers = await User
|
||||
partyMembers = await User // eslint-disable-line no-param-reassign
|
||||
.find({
|
||||
'party._id': party._id,
|
||||
_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) {
|
||||
if (!party && (!targetId || user._id === targetId)) {
|
||||
partyMembers = user;
|
||||
partyMembers = user; // eslint-disable-line no-param-reassign
|
||||
} else {
|
||||
if (!targetId) throw new BadRequest(res.t('targetIdUUID'));
|
||||
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 })
|
||||
.select(partyMembersFields)
|
||||
.exec();
|
||||
@@ -177,14 +178,21 @@ async function castSpell (req, res, { isV3 = false }) {
|
||||
if (targetType === 'party') {
|
||||
partyMembers = await castPartySpell(req, party, partyMembers, user, spell, quantity);
|
||||
} 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];
|
||||
|
||||
// Only return some fields.
|
||||
// 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;
|
||||
if (isV3) userToJson = await userToJson.toJSONWithInbox();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
export function removePunctuationFromString (str) {
|
||||
return str.replace(/[.,\/#!@$%\^&;:{}=\-_`~()]/g, ' ');
|
||||
return str.replace(/[.,/#!@$%^&;:{}=\-_`~()]/g, ' ');
|
||||
}
|
||||
|
||||
export function getMatchesByWordArray (str, wordsToMatch) {
|
||||
|
||||
@@ -33,7 +33,12 @@ export function setNextDue (task, user, dueDateOption) {
|
||||
dateTaskIsDue = moment(dueDateOption);
|
||||
|
||||
// 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.dayStart, 'hours');
|
||||
}
|
||||
@@ -103,7 +108,9 @@ export async function createTasks (req, res, options = {}) {
|
||||
setNextDue(newTask, user);
|
||||
|
||||
// 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();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
@@ -116,7 +123,7 @@ export async function createTasks (req, res, options = {}) {
|
||||
|
||||
// Push all task ids
|
||||
const taskOrderUpdateQuery = { $push: {} };
|
||||
for (const taskType in taskOrderToAdd) {
|
||||
for (const taskType of Object.keys(taskOrderToAdd)) {
|
||||
taskOrderUpdateQuery.$push[`tasksOrder.${taskType}`] = {
|
||||
$each: taskOrderToAdd[taskType],
|
||||
$position: 0,
|
||||
@@ -128,7 +135,9 @@ export async function createTasks (req, res, options = {}) {
|
||||
// tasks with aliases need to be validated asynchronously
|
||||
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,
|
||||
}));
|
||||
|
||||
@@ -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
|
||||
|
||||
export function syncableAttrs (task) {
|
||||
const t = task.toObject(); // lodash doesn't seem to like _.omit on Document
|
||||
// only sync/compare important attrs
|
||||
|
||||
@@ -2,7 +2,7 @@ import got from 'got';
|
||||
import { isURL } from 'validator';
|
||||
import nconf from 'nconf';
|
||||
import logger from './logger';
|
||||
import {
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
model as User,
|
||||
} from '../models/user';
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ function getUserFields (options, req) {
|
||||
// Must be an array
|
||||
if (options.userFieldsToExclude) {
|
||||
return options.userFieldsToExclude
|
||||
.filter(field => !USER_FIELDS_ALWAYS_LOADED.find(fieldToInclude => field.startsWith(fieldToInclude)))
|
||||
.map(field => `-${field}`, // -${field} means exclude ${field} in mongodb
|
||||
)
|
||||
.filter(field => !USER_FIELDS_ALWAYS_LOADED
|
||||
.find(fieldToInclude => field.startsWith(fieldToInclude)))
|
||||
.map(field => `-${field}`) // -${field} means exclude ${field} in mongodb
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ function getUserFields (options, req) {
|
||||
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 { userFields } = req.query;
|
||||
if (!userFields || urlPath !== '/user') return '';
|
||||
|
||||
@@ -8,12 +8,14 @@ import { recoverCron, cron } from '../libs/cron';
|
||||
const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime();
|
||||
|
||||
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();
|
||||
// Calculate how long ago cron must have been attempted to try again
|
||||
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({
|
||||
_id: user._id,
|
||||
$or: [ // Make sure last cron was successful or failed before cronRetryTime
|
||||
@@ -60,7 +62,8 @@ async function cronAsync (req, res) {
|
||||
try {
|
||||
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);
|
||||
|
||||
await updateLastCron(user, now);
|
||||
@@ -86,7 +89,13 @@ async function cronAsync (req, res) {
|
||||
|
||||
// Run 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
|
||||
@@ -117,7 +126,7 @@ async function cronAsync (req, res) {
|
||||
}).exec();
|
||||
|
||||
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;
|
||||
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 (err.message === 'CRON_ALREADY_RUNNING') {
|
||||
// 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
|
||||
const recoveryStatus = {
|
||||
times: 0,
|
||||
@@ -151,7 +161,8 @@ async function cronAsync (req, res) {
|
||||
|
||||
await recoverCron(recoveryStatus, res.locals);
|
||||
} 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
|
||||
await User.update({
|
||||
_id: user._id,
|
||||
@@ -161,6 +172,8 @@ async function cronAsync (req, res) {
|
||||
|
||||
throw err; // re-throw the original error
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export function ensureAdmin (req, res, next) {
|
||||
return next(new NotAuthorized(res.t('noAdminAccess')));
|
||||
}
|
||||
|
||||
next();
|
||||
return next();
|
||||
}
|
||||
|
||||
export function ensureSudo (req, res, next) {
|
||||
@@ -20,5 +20,5 @@ export function ensureSudo (req, res, next) {
|
||||
return next(new NotAuthorized(apiError('noSudoAccess')));
|
||||
}
|
||||
|
||||
next();
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -56,8 +56,8 @@ function _getFromUser (user, req) {
|
||||
}
|
||||
|
||||
export function attachTranslateFunction (req, res, next) {
|
||||
res.t = function reqTranslation () {
|
||||
return i18n.t(...arguments, req.language);
|
||||
res.t = function reqTranslation (...args) {
|
||||
return i18n.t(...args, req.language);
|
||||
};
|
||||
|
||||
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
|
||||
req.language = translations[req.query.lang] ? req.query.lang : 'en';
|
||||
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);
|
||||
return next();
|
||||
} if (req.session && req.session.userId) { // Same thing if the user has a valid session
|
||||
|
||||
@@ -6,7 +6,7 @@ const MAINTENANCE_MODE = nconf.get('MAINTENANCE_MODE');
|
||||
export default function maintenanceMode (req, res, next) {
|
||||
if (MAINTENANCE_MODE !== 'true') return next();
|
||||
|
||||
getUserLanguage(req, res, err => {
|
||||
return getUserLanguage(req, res, err => {
|
||||
if (err) return next(err);
|
||||
|
||||
const pageVariables = {
|
||||
|
||||
@@ -14,7 +14,8 @@ const TOP_LEVEL_ROUTES = [
|
||||
'/export',
|
||||
'/email',
|
||||
'/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
|
||||
];
|
||||
|
||||
|
||||
@@ -21,11 +21,14 @@ function isHTTP (req) {
|
||||
|
||||
export function forceSSL (req, res, next) {
|
||||
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);
|
||||
}
|
||||
|
||||
next();
|
||||
return next();
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
next();
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import _ from 'lodash';
|
||||
import { TaskQueue } from 'cwait';
|
||||
import baseModel from '../libs/baseModel';
|
||||
import * as Tasks from './task';
|
||||
import { model as User } from './user';
|
||||
import {
|
||||
import { model as User } from './user'; // eslint-disable-line import/no-cycle
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
model as Group,
|
||||
} from './group';
|
||||
import { removeFromArray } from '../libs/collectionManipulators';
|
||||
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 { syncableAttrs, setNextDue } from '../libs/taskManager';
|
||||
|
||||
@@ -246,7 +246,7 @@ schema.methods.updateTask = async function challengeUpdateTask (task) {
|
||||
const updateCmd = { $set: {} };
|
||||
|
||||
const syncableTask = syncableAttrs(task);
|
||||
for (const key in syncableTask) {
|
||||
for (const key of Object.keys(syncableTask)) {
|
||||
updateCmd.$set[key] = syncableTask[key];
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ import mongoose from 'mongoose';
|
||||
import _ from 'lodash';
|
||||
import validator from 'validator';
|
||||
import nconf from 'nconf';
|
||||
import {
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
model as User,
|
||||
nameFields,
|
||||
} from './user';
|
||||
import shared from '../../common';
|
||||
import { model as Challenge } from './challenge';
|
||||
import { model as Challenge } from './challenge'; // eslint-disable-line import/no-cycle
|
||||
import {
|
||||
chatModel as Chat,
|
||||
setUserStyles,
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
} from './message';
|
||||
import * as Tasks from './task';
|
||||
import { removeFromArray } from '../libs/collectionManipulators';
|
||||
import payments from '../libs/payments/payments';
|
||||
import {
|
||||
import payments from '../libs/payments/payments'; // eslint-disable-line import/no-cycle
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
groupChatReceivedWebhook,
|
||||
questActivityWebhook,
|
||||
} from '../libs/webhook';
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
NotAuthorized,
|
||||
} from '../libs/errors';
|
||||
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 {
|
||||
syncableAttrs,
|
||||
@@ -35,11 +35,11 @@ import {
|
||||
import {
|
||||
schema as SubscriptionPlanSchema,
|
||||
} from './subscriptionPlan';
|
||||
import amazonPayments from '../libs/payments/amazon';
|
||||
import stripePayments from '../libs/payments/stripe';
|
||||
import { getGroupChat, translateMessage } from '../libs/chat/group-chat';
|
||||
import amazonPayments from '../libs/payments/amazon'; // eslint-disable-line import/no-cycle
|
||||
import stripePayments from '../libs/payments/stripe'; // eslint-disable-line import/no-cycle
|
||||
import { getGroupChat, translateMessage } from '../libs/chat/group-chat'; // eslint-disable-line import/no-cycle
|
||||
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 { questSeriesAchievements } = shared.content;
|
||||
@@ -108,8 +108,10 @@ export const schema = new Schema({
|
||||
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
|
||||
// 'Accept', the quest begins. If a false user waits too long, probably a good sign to prod them or boot them.
|
||||
// Shows boolean for each party-member who has accepted the quest.
|
||||
// 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
|
||||
members: {
|
||||
$type: Schema.Types.Mixed,
|
||||
@@ -836,7 +838,8 @@ async function _updateUserWithRetries (userId, updates, numTry = 1, query = {})
|
||||
return await User.update(query, updates).exec();
|
||||
} catch (err) {
|
||||
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;
|
||||
}
|
||||
@@ -1391,7 +1394,7 @@ schema.methods.updateTask = async function updateTask (taskToSync, options = {})
|
||||
const updateCmd = { $set: {} };
|
||||
|
||||
const syncableAttributes = syncableAttrs(taskToSync);
|
||||
for (const key in syncableAttributes) {
|
||||
for (const key of Object.keys(syncableAttributes)) {
|
||||
updateCmd.$set[key] = syncableAttributes[key];
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import shared from '../../common';
|
||||
import baseModel from '../libs/baseModel';
|
||||
import { InternalServerError } from '../libs/errors';
|
||||
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;
|
||||
|
||||
|
||||
@@ -6,13 +6,14 @@ import * as Tasks from '../task';
|
||||
import {
|
||||
model as UserNotification,
|
||||
} from '../userNotification';
|
||||
import {
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
userActivityWebhook,
|
||||
} from '../../libs/webhook';
|
||||
import schema from './schema';
|
||||
import schema from './schema'; // eslint-disable-line import/no-cycle
|
||||
|
||||
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: [],
|
||||
private: ['auth.local.hashed_password', 'auth.local.passwordHashMethod', 'auth.local.salt', '_cronSignature', '_ABtests'],
|
||||
toJSONTransform: function userToJSON (plainObj, originalDoc) {
|
||||
@@ -25,7 +26,8 @@ schema.plugin(baseModel, {
|
||||
delete plainObj.filters;
|
||||
|
||||
if (originalDoc.notifications) {
|
||||
plainObj.notifications = UserNotification.convertNotificationsToSafeJson(originalDoc.notifications);
|
||||
plainObj.notifications = UserNotification
|
||||
.convertNotificationsToSafeJson(originalDoc.notifications);
|
||||
}
|
||||
|
||||
return plainObj;
|
||||
@@ -51,7 +53,8 @@ function _populateDefaultTasks (user, taskTypes) {
|
||||
user.tags = _.map(defaultsData.tags, 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();
|
||||
// Render tag's name in user's 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 = [];
|
||||
if (user.registeredThrough === 'habitica-web') return Promise.all(tasksToCreate);
|
||||
|
||||
if (tagsI !== -1) {
|
||||
taskTypes = _.clone(taskTypes);
|
||||
taskTypes = _.clone(taskTypes); // eslint-disable-line no-param-reassign
|
||||
taskTypes.splice(tagsI, 1);
|
||||
}
|
||||
|
||||
@@ -212,7 +216,8 @@ schema.pre('save', true, function preSaveUser (next, done) {
|
||||
// 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
|
||||
this.isDirectSelected('notifications')
|
||||
&& this.isDirectSelected('stats')
|
||||
@@ -239,10 +244,12 @@ schema.pre('save', true, function preSaveUser (next, done) {
|
||||
const classNotEnabled = !this.flags.classSelected || this.preferences.disableClasses;
|
||||
|
||||
// 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
|
||||
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 (pointsToAllocate > 0 && !classNotEnabled) {
|
||||
@@ -265,7 +272,11 @@ schema.pre('save', true, function preSaveUser (next, done) {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -273,7 +284,7 @@ schema.pre('save', true, function preSaveUser (next, done) {
|
||||
// our own version incrementer
|
||||
if (this.isDirectSelected('_v')) {
|
||||
if (_.isNaN(this._v) || !_.isNumber(this._v)) this._v = 0;
|
||||
this._v++;
|
||||
this._v += 1;
|
||||
}
|
||||
|
||||
// Populate new users with default content
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
import schema from './schema';
|
||||
import schema from './schema'; // eslint-disable-line import/no-cycle
|
||||
|
||||
import './hooks';
|
||||
import './methods';
|
||||
import './hooks'; // eslint-disable-line import/no-cycle
|
||||
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
|
||||
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
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from 'lodash';
|
||||
import common from '../../../common';
|
||||
|
||||
import {
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
TAVERN_ID,
|
||||
model as Group,
|
||||
} from '../group';
|
||||
@@ -16,19 +16,20 @@ import {
|
||||
} from '../message';
|
||||
|
||||
import { model as UserNotification } from '../userNotification';
|
||||
import schema from './schema';
|
||||
import payments from '../../libs/payments/payments';
|
||||
import * as inboxLib from '../../libs/inbox';
|
||||
import amazonPayments from '../../libs/payments/amazon';
|
||||
import stripePayments from '../../libs/payments/stripe';
|
||||
import paypalPayments from '../../libs/payments/paypal';
|
||||
import schema from './schema'; // eslint-disable-line import/no-cycle
|
||||
import payments from '../../libs/payments/payments'; // eslint-disable-line import/no-cycle
|
||||
import * as inboxLib from '../../libs/inbox'; // eslint-disable-line import/no-cycle
|
||||
import amazonPayments from '../../libs/payments/amazon'; // eslint-disable-line import/no-cycle
|
||||
import stripePayments from '../../libs/payments/stripe'; // eslint-disable-line import/no-cycle
|
||||
import paypalPayments from '../../libs/payments/paypal'; // eslint-disable-line import/no-cycle
|
||||
|
||||
const { daysSince } = common;
|
||||
|
||||
schema.methods.isSubscribed = function isSubscribed () {
|
||||
const now = new Date();
|
||||
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 () {
|
||||
@@ -49,10 +50,12 @@ schema.methods.getGroups = function getUserGroups () {
|
||||
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({
|
||||
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
|
||||
(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
|
||||
(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': [
|
||||
@@ -79,10 +83,11 @@ const INTERACTION_CHECKS = Object.freeze({
|
||||
});
|
||||
/* 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
|
||||
schema.methods.getObjectionsToInteraction = function getObjectionsToInteraction (interaction, receiver) {
|
||||
schema.methods.getObjectionsToInteraction = function getObjectionsToInteraction (interaction, receiver) { // eslint-disable-line max-len
|
||||
if (!KNOWN_INTERACTIONS.includes(interaction)) {
|
||||
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));
|
||||
setUserStyles(newReceiverMessage, sender);
|
||||
|
||||
userToReceiveMessage.inbox.newMessages++;
|
||||
userToReceiveMessage._v++;
|
||||
userToReceiveMessage.inbox.newMessages += 1;
|
||||
userToReceiveMessage._v += 1;
|
||||
|
||||
/* @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.
|
||||
*
|
||||
* @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 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
|
||||
* 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
|
||||
* Creates a notification based on the type and data input parameters
|
||||
and saves that new notification
|
||||
* 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.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
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 validationResult = newNotification.validateSync();
|
||||
@@ -211,7 +224,11 @@ schema.statics.pushNotification = async function pushNotification (query, type,
|
||||
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,
|
||||
@@ -229,7 +246,10 @@ schema.statics.transformJSONUser = function transformJSONUser (jsonUser, addComp
|
||||
|
||||
// Add stats.toNextLevel, stats.maxMP and stats.maxHealth
|
||||
// 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
|
||||
// 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.
|
||||
@@ -251,25 +271,29 @@ schema.statics.addComputedStatsToJSONObj = function addComputedStatsToUserJSONOb
|
||||
*
|
||||
* @return a Promise from api.cancelSubscription()
|
||||
*/
|
||||
// @TODO: There is currently a three way relation between the user, payment methods and the payment helper
|
||||
// This creates some odd Dependency Injection issues. To counter that, 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
|
||||
// @TODO: There is currently a three way relation between the user,
|
||||
// payment methods and the payment helper
|
||||
// This creates some odd Dependency Injection issues. To counter that,
|
||||
// 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.
|
||||
schema.methods.cancelSubscription = async function cancelSubscription (options = {}) {
|
||||
const { plan } = this.purchased;
|
||||
|
||||
options.user = this;
|
||||
if (plan.paymentMethod === amazonPayments.constants.PAYMENT_METHOD) {
|
||||
return await amazonPayments.cancelSubscription(options);
|
||||
return amazonPayments.cancelSubscription(options);
|
||||
} if (plan.paymentMethod === stripePayments.constants.PAYMENT_METHOD) {
|
||||
return await stripePayments.cancelSubscription(options);
|
||||
return stripePayments.cancelSubscription(options);
|
||||
} 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.
|
||||
|
||||
return await payments.cancelSubscription(options);
|
||||
return payments.cancelSubscription(options);
|
||||
};
|
||||
|
||||
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.
|
||||
// CDS = Custom Day Start time.
|
||||
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'));
|
||||
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
|
||||
|
||||
if (timezoneOffsetFromBrowser !== timezoneOffsetFromUserPrefs) {
|
||||
@@ -296,8 +324,8 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
|
||||
if (timezoneOffsetAtLastCron !== timezoneOffsetFromUserPrefs) {
|
||||
// Give the user extra time based on the difference in timezones
|
||||
if (timezoneOffsetAtLastCron < timezoneOffsetFromUserPrefs) {
|
||||
const differenceBetweenTimezonesInMinutes = timezoneOffsetFromUserPrefs - timezoneOffsetAtLastCron;
|
||||
now = moment(now).subtract(differenceBetweenTimezonesInMinutes, 'minutes');
|
||||
const differenceBetweenTimezonesInMinutes = timezoneOffsetFromUserPrefs - timezoneOffsetAtLastCron; // eslint-disable-line max-len
|
||||
now = moment(now).subtract(differenceBetweenTimezonesInMinutes, 'minutes'); // eslint-disable-line no-param-reassign, max-len
|
||||
}
|
||||
|
||||
// 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
|
||||
// just in case I'm wrong...
|
||||
// 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) {
|
||||
// The old timezone says that cron should NOT run -- i.e., cron has
|
||||
// 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
|
||||
|
||||
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:
|
||||
this.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs;
|
||||
} else {
|
||||
@@ -351,9 +383,14 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
|
||||
}
|
||||
} else if (timezoneOffsetAtLastCron > timezoneOffsetFromUserPrefs) {
|
||||
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.
|
||||
// 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).
|
||||
// TODO: Either confirm that there is nothing that could possibly go wrong
|
||||
// here and remove the need for this else branch, or fix stuff.
|
||||
// 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);
|
||||
|
||||
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 () {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import {
|
||||
schema as SubscriptionPlanSchema,
|
||||
} from '../subscriptionPlan';
|
||||
import {
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
getDefaultOwnedGear,
|
||||
} from '../../libs/items/utils';
|
||||
|
||||
@@ -71,7 +71,8 @@ export default new Schema({
|
||||
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
|
||||
_v: { $type: Number, default: 0 },
|
||||
migration: String,
|
||||
@@ -138,7 +139,8 @@ export default new Schema({
|
||||
},
|
||||
|
||||
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: {
|
||||
$type: Number,
|
||||
min: 0,
|
||||
@@ -186,7 +188,8 @@ export default new Schema({
|
||||
customizationsNotification: { $type: Boolean, default: false },
|
||||
showTour: { $type: Boolean, default: true },
|
||||
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 },
|
||||
classes: { $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.'] }],
|
||||
|
||||
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)
|
||||
// TODO one way to fix is http://mongoosejs.com/docs/guide.html#_id
|
||||
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)
|
||||
party: {
|
||||
$type: Schema.Types.Mixed,
|
||||
@@ -445,8 +450,12 @@ export default new Schema({
|
||||
},
|
||||
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
|
||||
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
|
||||
// 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
|
||||
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: {
|
||||
@@ -489,7 +498,8 @@ export default new Schema({
|
||||
$type: Schema.Types.Mixed,
|
||||
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
|
||||
emailNotifications: {
|
||||
unsubscribeFromAll: { $type: Boolean, default: false },
|
||||
@@ -537,7 +547,8 @@ export default new Schema({
|
||||
$type: Array,
|
||||
validate: categories => {
|
||||
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;
|
||||
},
|
||||
},
|
||||
@@ -621,7 +632,8 @@ export default new Schema({
|
||||
path: { $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 }],
|
||||
// Items the user manually unpinned from the ones suggested by Habitica
|
||||
unpinnedItems: [{
|
||||
|
||||
Reference in New Issue
Block a user