fix linting for server (except for length of apidoc)

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

View File

@@ -12,7 +12,6 @@ import {
} from '../../libs/errors';
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 = {

View File

@@ -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'));

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);
},

View File

@@ -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');

View File

@@ -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({

View File

@@ -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 {

View File

@@ -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'));

View File

@@ -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);
},

View File

@@ -285,7 +285,7 @@ api.exportUserAvatarPng = {
});
});
res.redirect(s3res.Location);
return res.redirect(s3res.Location);
},
};
@@ -317,11 +317,11 @@ api.exportUserPrivateMessages = {
const messageUser = message.user;
const 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>

View File

@@ -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')}`;

View File

@@ -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;

View File

@@ -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);
},

View File

@@ -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

View File

@@ -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');

View File

@@ -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),

View File

@@ -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;

View File

@@ -21,12 +21,13 @@ function _passportProfile (network, accessToken) {
});
}
export async function loginSocial (req, res) {
export async function loginSocial (req, res) { // eslint-disable-line import/prefer-default-export
const existingUser = res.locals.user;
const 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()

View File

@@ -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,

View File

@@ -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'),
});

View File

@@ -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.

View 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.

View 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;

View File

@@ -1,4 +1,5 @@
// Currently this holds helpers for challenge api, but we should break this up into submodules as it expands
// Currently this holds helpers for challenge api,
// but we should break this up into submodules as it expands
import omit from 'lodash/omit';
import 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;

View File

@@ -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,
},
},
);

View File

@@ -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;

View File

@@ -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');
}
}

View File

@@ -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.');
}

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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 });
}

View File

@@ -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)) {

View File

@@ -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();

View File

@@ -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;

View File

@@ -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: [
{

View File

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

View File

@@ -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',

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
import * as Tasks from '../models/task';
import * as Tasks from '../models/task'; // eslint-disable-line import/no-cycle
const SHARED_COMPLETION = {
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,

View File

@@ -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.

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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']);

View File

@@ -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

View File

@@ -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,
});

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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.
]);
}

View File

@@ -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';

View File

@@ -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,
});
}
};

View File

@@ -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;

View File

@@ -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

View File

@@ -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();

View File

@@ -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();

View File

@@ -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
};

View File

@@ -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);
}
});

View File

@@ -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'),

View File

@@ -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,

View File

@@ -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();

View File

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

View File

@@ -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

View File

@@ -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';

View File

@@ -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 '';

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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
];

View File

@@ -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();
}

View File

@@ -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];
}

View File

@@ -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];
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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 () {

View File

@@ -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: [{