Merge branch 'release' into sabrecat/more-api-sunset

This commit is contained in:
Sabe Jones
2024-06-28 22:20:07 -05:00
825 changed files with 61553 additions and 43485 deletions

View File

@@ -432,6 +432,7 @@ api.updateEmail = {
}
user.auth.local.email = req.body.newEmail.toLowerCase();
user.auth.local.passwordResetCode = undefined;
await user.save();
return res.respond(200, { email: user.auth.local.email });

View File

@@ -33,7 +33,7 @@ import {
cleanUpTask,
createChallengeQuery,
} from '../../libs/challenges';
import apiError from '../../libs/apiError';
import { apiError } from '../../libs/apiError';
import common from '../../../common';
import {
clearFlags,

View File

@@ -17,7 +17,10 @@ import { removeFromArray } from '../../libs/collectionManipulators';
import { getUserInfo } from '../../libs/email';
import * as slack from '../../libs/slack';
import { chatReporterFactory } from '../../libs/chatReporting/chatReporterFactory';
import apiError from '../../libs/apiError';
import bannedWords from '../../libs/bannedWords';
import { getMatchesByWordArray } from '../../libs/stringUtils';
import bannedSlurs from '../../libs/bannedSlurs';
import { apiError } from '../../libs/apiError';
import highlightMentions from '../../libs/highlightMentions';
import { getAnalyticsServiceByEnvironment } from '../../libs/analyticsService';
@@ -47,6 +50,11 @@ const ACCOUNT_MIN_CHAT_AGE = Number(nconf.get('ACCOUNT_MIN_CHAT_AGE'));
const api = {};
function textContainsBannedSlur (message) {
const bannedSlursMatched = getMatchesByWordArray(message, bannedSlurs);
return bannedSlursMatched.length > 0;
}
/**
* @api {get} /api/v3/groups/:groupId/chat Get chat messages from a group
* @apiName GetChat
@@ -85,6 +93,10 @@ api.getChat = {
},
};
function getBannedWordsFromText (message) {
return getMatchesByWordArray(message, bannedWords);
}
/**
* @api {post} /api/v3/groups/:groupId/chat Post chat message to a group
* @apiName PostChat
@@ -128,6 +140,39 @@ api.postChat = {
throw new BadRequest(res.t('featureRetired'));
}
// Check message for banned slurs
if (group && group.privacy !== 'private' && textContainsBannedSlur(req.body.message)) {
const { message } = req.body;
user.flags.chatRevoked = true;
await user.save();
// Email the mods
const authorEmail = getUserInfo(user, ['email']).email;
// Slack the mods
slack.sendSlurNotification({
authorEmail,
author: user,
group,
message,
});
throw new BadRequest(res.t('bannedSlurUsed'));
}
if (group.privacy === 'public' && user.flags.chatRevoked) {
throw new NotAuthorized(res.t('chatPrivilegesRevoked'));
}
// prevent banned words being posted, except in private guilds/parties
// and in certain public guilds with specific topics
if (group.privacy === 'public' && !group.bannedWordsAllowed) {
const matchedBadWords = getBannedWordsFromText(req.body.message);
if (matchedBadWords.length > 0) {
throw new BadRequest(res.t('bannedWordUsed', { swearWordsUsed: matchedBadWords.join(', ') }));
}
}
const chatRes = await Group.toJSONCleanChat(group, user);
const lastClientMsg = req.query.previousMsg;
const chatUpdated = !!(
@@ -172,7 +217,7 @@ api.postChat = {
});
}
const newChatMessage = group.sendChat({
const newChatMessage = await group.sendChat({
message,
user,
flagCount,
@@ -256,7 +301,7 @@ api.likeChat = {
throw new BadRequest(res.t('featureRetired'));
}
const message = await Chat.findOne({ _id: req.params.chatId }).exec();
const message = await Chat.findOne({ _id: req.params.chatId, groupId: group._id }).exec();
if (!message) throw new NotFound(res.t('messageGroupChatNotFound'));
if (!message.likes) message.likes = {};

View File

@@ -1,11 +1,20 @@
import nconf from 'nconf';
import { langCodes } from '../../libs/i18n';
import { CONTENT_CACHE_PATH, getLocalizedContentResponse } from '../../libs/content';
import { serveContent } from '../../libs/content';
const IS_PROD = nconf.get('IS_PROD');
const api = {};
const MOBILE_FILTER = ['achievements', 'questSeriesAchievements', 'animalColorAchievements', 'animalSetAchievements',
'stableAchievements', 'mystery', 'bundles', 'loginIncentives', 'pets', 'premiumPets', 'specialPets', 'questPets',
'wackyPets', 'mounts', 'premiumMounts,specialMounts,questMounts', 'events', 'dropEggs', 'questEggs', 'dropHatchingPotions',
'premiumHatchingPotions', 'wackyHatchingPotions', 'backgroundsFlat', 'questsByLevel', 'gear.tree', 'tasksByCategory',
'userDefaults', 'timeTravelStable', 'gearTypes', 'cardTypes'];
const ANDROID_FILTER = [...MOBILE_FILTER, 'appearances.background'].join(',');
const IOS_FILTER = [...MOBILE_FILTER, 'backgrounds'].join(',');
/**
* @api {get} /api/v3/content Get all available content objects
* @apiDescription Does not require authentication.
@@ -65,16 +74,17 @@ api.getContent = {
language = proposedLang;
}
if (IS_PROD) {
res.sendFile(`${CONTENT_CACHE_PATH}${language}.json`);
} else {
res.set({
'Content-Type': 'application/json',
});
const jsonResString = getLocalizedContentResponse(language);
res.status(200).send(jsonResString);
let filter = req.query.filter || '';
// apply defaults for mobile clients
if (filter === '') {
if (req.headers['x-client'] === 'habitica-android') {
filter = ANDROID_FILTER;
} else if (req.headers['x-client'] === 'habitica-ios') {
filter = IOS_FILTER;
}
}
serveContent(res, language, filter, IS_PROD);
},
};

View File

@@ -7,7 +7,7 @@ import {
} from '../../middlewares/auth';
import { ensurePermission } from '../../middlewares/ensureAccessRight';
import * as couponsLib from '../../libs/coupons';
import apiError from '../../libs/apiError';
import { apiError } from '../../libs/apiError';
import { model as Coupon } from '../../models/coupon';
const api = {};

View File

@@ -1,6 +1,9 @@
import _ from 'lodash';
import sinon from 'sinon';
import moment from 'moment';
import { authWithHeaders } from '../../middlewares/auth';
import ensureDevelpmentMode from '../../middlewares/ensureDevelpmentMode';
import ensureDevelopmentMode from '../../middlewares/ensureDevelopmentMode';
import ensureTimeTravelMode from '../../middlewares/ensureTimeTravelMode';
import { BadRequest } from '../../libs/errors';
import common from '../../../common';
@@ -30,7 +33,7 @@ const api = {};
api.addTenGems = {
method: 'POST',
url: '/debug/add-ten-gems',
middlewares: [ensureDevelpmentMode, authWithHeaders()],
middlewares: [ensureDevelopmentMode, authWithHeaders()],
async handler (req, res) {
const { user } = res.locals;
@@ -53,7 +56,7 @@ api.addTenGems = {
api.addHourglass = {
method: 'POST',
url: '/debug/add-hourglass',
middlewares: [ensureDevelpmentMode, authWithHeaders()],
middlewares: [ensureDevelopmentMode, authWithHeaders()],
async handler (req, res) {
const { user } = res.locals;
@@ -76,7 +79,7 @@ api.addHourglass = {
api.setCron = {
method: 'POST',
url: '/debug/set-cron',
middlewares: [ensureDevelpmentMode, authWithHeaders()],
middlewares: [ensureDevelopmentMode, authWithHeaders()],
async handler (req, res) {
const { user } = res.locals;
const cron = req.body.lastCron;
@@ -100,7 +103,7 @@ api.setCron = {
api.makeAdmin = {
method: 'POST',
url: '/debug/make-admin',
middlewares: [ensureDevelpmentMode, authWithHeaders()],
middlewares: [ensureDevelopmentMode, authWithHeaders()],
async handler (req, res) {
const { user } = res.locals;
@@ -131,7 +134,7 @@ api.makeAdmin = {
api.modifyInventory = {
method: 'POST',
url: '/debug/modify-inventory',
middlewares: [ensureDevelpmentMode, authWithHeaders()],
middlewares: [ensureDevelopmentMode, authWithHeaders()],
async handler (req, res) {
const { user } = res.locals;
const { gear } = req.body;
@@ -173,7 +176,7 @@ api.modifyInventory = {
api.questProgress = {
method: 'POST',
url: '/debug/quest-progress',
middlewares: [ensureDevelpmentMode, authWithHeaders()],
middlewares: [ensureDevelopmentMode, authWithHeaders()],
async handler (req, res) {
const { user } = res.locals;
const key = _.get(user, 'party.quest.key');
@@ -201,4 +204,63 @@ api.questProgress = {
},
};
let clock;
function fakeClock () {
if (clock) clock.restore();
const time = new Date();
clock = sinon.useFakeTimers({
now: time,
shouldAdvanceTime: true,
});
}
api.timeTravelTime = {
method: 'GET',
url: '/debug/time-travel-time',
middlewares: [ensureTimeTravelMode, authWithHeaders()],
async handler (req, res) {
if (clock === undefined) {
fakeClock();
}
res.respond(200, {
time: new Date(),
});
},
};
api.timeTravelAdjust = {
method: 'POST',
url: '/debug/jump-time',
middlewares: [ensureTimeTravelMode, authWithHeaders()],
async handler (req, res) {
const { user } = res.locals;
if (!user.permissions.fullAccess) {
throw new BadRequest('You do not have permission to time travel.');
}
const { offsetDays, reset, disable } = req.body;
if (reset) {
fakeClock();
} else if (disable) {
clock.restore();
clock = undefined;
} else if (clock !== undefined) {
try {
clock.setSystemTime(moment().add(offsetDays, 'days').toDate());
} catch (e) {
throw new BadRequest('Error adjusting time');
}
} else {
throw new BadRequest('Invalid command');
}
res.respond(200, {
time: new Date(),
});
},
};
export default api;

View File

@@ -26,7 +26,7 @@ import common from '../../../common';
import payments from '../../libs/payments/payments';
import stripePayments from '../../libs/payments/stripe';
import amzLib from '../../libs/payments/amazon';
import apiError from '../../libs/apiError';
import { apiError } from '../../libs/apiError';
import { model as UserNotification } from '../../models/userNotification';
const { MAX_SUMMARY_SIZE_FOR_GUILDS } = common.constants;
@@ -136,6 +136,7 @@ api.createGroup = {
if (user.party._id) throw new NotAuthorized(res.t('messageGroupAlreadyInParty'));
user.party._id = group._id;
user.party.seeking = undefined;
}
let savedGroup;
@@ -590,7 +591,7 @@ api.joinGroup = {
// Clear all invitations of new user and reset looking for party state
user.invitations.parties = [];
user.invitations.party = {};
user.party.seeking = null;
user.party.seeking = undefined;
// invite new user to pending quest
if (group.quest.key && !group.quest.active) {

View File

@@ -8,7 +8,7 @@ import common from '../../../common';
import {
NotFound,
} from '../../libs/errors';
import apiError from '../../libs/apiError';
import { apiError } from '../../libs/apiError';
import {
validateItemPath,
castItemVal,
@@ -146,7 +146,7 @@ api.getHeroes = {
// Note, while the following routes are called getHero / updateHero
// they can be used by admins to get/update any user
const heroAdminFields = 'auth balance contributor flags items lastCron party preferences profile purchased secret permissions';
const heroAdminFields = 'auth balance contributor flags items lastCron party preferences profile purchased secret permissions achievements';
const heroAdminFieldsToFetch = heroAdminFields; // these variables will make more sense when...
const heroAdminFieldsToShow = heroAdminFields; // ... apiTokenObscured is added
@@ -285,7 +285,7 @@ api.updateHero = {
if (plan.dateCurrentTypeCreated) {
hero.purchased.plan.dateCurrentTypeCreated = plan.dateCurrentTypeCreated;
}
if (plan.dateTerminated) {
if (plan.dateTerminated !== hero.purchased.plan.dateTerminated) {
hero.purchased.plan.dateTerminated = plan.dateTerminated;
}
if (plan.perkMonthCount) {
@@ -342,12 +342,48 @@ api.updateHero = {
hero.purchased.ads = updateData.purchased.ads;
}
if (updateData.purchasedPath && updateData.purchasedVal !== undefined
&& validateItemPath(updateData.purchasedPath)) {
const parts = updateData.purchasedPath.split('.');
const key = _.last(parts);
const type = parts[parts.length - 2];
// using _.set causes weird issues
if (updateData.purchasedVal === true) {
if (updateData.purchasedPath.indexOf('hair.') === 10) {
if (hero.purchased.hair[type] === undefined) hero.purchased.hair[type] = {};
hero.purchased.hair[type][key] = true;
} else {
if (hero.purchased[type] === undefined) hero.purchased[type] = {};
hero.purchased[type][key] = true;
}
} else if (updateData.purchasedPath.indexOf('hair.') === 10) {
delete hero.purchased.hair[type][key];
} else {
delete hero.purchased[type][key];
}
hero.markModified('purchased');
}
if (updateData.achievementPath && updateData.achievementVal !== undefined) {
const parts = updateData.achievementPath.split('.');
const key = _.last(parts);
const type = parts[parts.length - 2];
// using _.set causes weird issues
if (type !== 'achievements') {
if (hero.achievements[type] === undefined) hero.achievements[type] = {};
hero.achievements[type][key] = updateData.achievementVal;
} else {
hero.achievements[key] = updateData.achievementVal;
}
hero.markModified('achievements');
}
// give them the Dragon Hydra pet if they're above level 6
if (hero.contributor.level >= 6) {
hero.items.pets['Dragon-Hydra'] = 5;
hero.markModified('items.pets');
}
if (updateData.itemPath && updateData.itemVal && validateItemPath(updateData.itemPath)) {
if (updateData.itemPath && (updateData.itemVal || updateData.itemVal === '') && validateItemPath(updateData.itemPath)) {
// Sanitization at 5c30944 (deemed unnecessary)
_.set(hero, updateData.itemPath, castItemVal(updateData.itemPath, updateData.itemVal));
hero.markModified('items');
@@ -367,6 +403,7 @@ api.updateHero = {
if (updateData.flags && _.isBoolean(updateData.flags.chatShadowMuted)) {
hero.flags.chatShadowMuted = updateData.flags.chatShadowMuted;
}
if (updateData.profile) _.assign(hero.profile, updateData.profile);
if (updateData.secret) {
if (typeof updateData.secret.text !== 'undefined') {

View File

@@ -756,7 +756,7 @@ api.transferGems = {
]);
}
if (receiver.preferences.pushNotifications.giftedGems !== false) {
sendPushNotification(
await sendPushNotification(
receiver,
{
title: res.t('giftedGems', receiverLang),

View File

@@ -17,7 +17,7 @@ import {
} from '../../libs/email';
import common from '../../../common';
import { sendNotification as sendPushNotification } from '../../libs/pushNotifications';
import apiError from '../../libs/apiError';
import { apiError } from '../../libs/apiError';
import { questActivityWebhook } from '../../libs/webhook';
const analytics = getAnalyticsServiceByEnvironment();
@@ -120,10 +120,10 @@ api.inviteToQuest = {
// send out invites
const inviterVars = getUserInfo(user, ['name', 'email']);
const membersToEmail = members.filter(member => {
const membersToEmail = members.filter(async member => {
// send push notifications while filtering members before sending emails
if (member.preferences.pushNotifications.invitedQuest !== false) {
sendPushNotification(
await sendPushNotification(
member,
{
title: quest.text(member.preferences.language),
@@ -394,7 +394,7 @@ api.cancelQuest = {
if (group.quest.active) throw new NotAuthorized(res.t('cantCancelActiveQuest'));
const questName = questScrolls[group.quest.key].text('en');
const newChatMessage = group.sendChat({
const newChatMessage = await group.sendChat({
message: `\`${user.profile.name} cancelled the party quest ${questName}.\``,
info: {
type: 'quest_cancel',
@@ -456,7 +456,7 @@ api.abortQuest = {
if (user._id !== group.leader && user._id !== group.quest.leader) throw new NotAuthorized(res.t('onlyLeaderAbortQuest'));
const questName = questScrolls[group.quest.key].text('en');
const newChatMessage = group.sendChat({
const newChatMessage = await group.sendChat({
message: `\`${common.i18n.t('chatQuestAborted', { username: user.profile.name, questName }, 'en')}\``,
info: {
type: 'quest_abort',

View File

@@ -144,4 +144,26 @@ api.getBackgroundShopItems = {
},
};
/**
* @apiIgnore
* @api {get} /api/v3/shops/customizations get the available items for the customizations shop
* @apiName GetCustomizationShopItems
* @apiGroup Shops
*
* @apiSuccess {Object} data List of available avatar customizations
* @apiSuccess {string} message Success message
*/
api.getCustomizationsShop = {
method: 'GET',
url: '/shops/customizations',
middlewares: [authWithHeaders()],
async handler (req, res) {
const { user } = res.locals;
const resObject = shops.getCustomizationsShop(user, req.language);
res.respond(200, resObject);
},
};
export default api;

View File

@@ -27,7 +27,7 @@ import {
requiredGroupFields,
} from '../../libs/tasks/utils';
import common from '../../../common';
import apiError from '../../libs/apiError';
import { apiError } from '../../libs/apiError';
/**
* @apiDefine TaskNotFound

View File

@@ -18,7 +18,7 @@ import {
import {
moveTask,
} from '../../../libs/tasks/utils';
import apiError from '../../../libs/apiError';
import { apiError } from '../../../libs/apiError';
const requiredGroupFields = '_id leader tasksOrder name';
// @TODO: abstract to task lib

View File

@@ -1,5 +1,4 @@
import {
getCurrentEvent,
getCurrentEventList,
getWorldBoss,
} from '../../libs/worldState';
@@ -27,13 +26,19 @@ api.getWorldState = {
method: 'GET',
url: '/world-state',
async handler (req, res) {
const worldState = {};
const worldState = { npcImageSuffix: '', currentEvent: null };
worldState.worldBoss = await getWorldBoss();
worldState.currentEvent = getCurrentEvent();
worldState.npcImageSuffix = worldState.currentEvent ? worldState.currentEvent.npcImageSuffix : '';
worldState.currentEventList = getCurrentEventList();
if (worldState.currentEventList.length > 0) {
[worldState.currentEvent] = worldState.currentEventList;
}
worldState.currentEventList.forEach(event => {
if (event.npcImageSuffix) {
worldState.npcImageSuffix = event.npcImageSuffix;
}
});
res.respond(200, worldState);
},