import mongoose from 'mongoose'; import get from 'lodash/get'; import sinon from 'sinon'; import moment from 'moment'; import { v4 as uuid } from 'uuid'; import { authWithHeaders } from '../../middlewares/auth'; import ensureDevelopmentMode from '../../middlewares/ensureDevelopmentMode'; import ensureTimeTravelMode from '../../middlewares/ensureTimeTravelMode'; import { BadRequest } from '../../libs/errors'; import common from '../../../common'; import { model as Group, // basicFields as basicGroupFields, } from '../../models/group'; import { chatModel as Chat, inboxModel as Inbox } from '../../models/message'; import connectToMongoDB from '../../libs/mongoose'; const { content } = common; /** * @apiDefine Development Development * These routes only exist while Habitica is in development mode. * (Such as running a local instance on your computer). */ /** * @apiDefine Developers Local Development * This route only exists when developing Habitica in non-production environment. */ const api = {}; /** * @api {post} /api/v3/debug/add-ten-gems Add ten gems to the current user * @apiName AddTenGems * @apiGroup Development * @apiPermission Developers * * @apiSuccess {Object} data An empty Object */ api.addTenGems = { method: 'POST', url: '/debug/add-ten-gems', middlewares: [ensureDevelopmentMode, authWithHeaders()], async handler (req, res) { const { user } = res.locals; await user.updateBalance(2.5, 'debug'); await user.save(); res.respond(200, {}); }, }; /** * @api {post} /api/v3/debug/add-hourglass Add Hourglass to the current user * @apiName AddHourglass * @apiGroup Development * @apiPermission Developers * * @apiSuccess {Object} data An empty Object */ api.addHourglass = { method: 'POST', url: '/debug/add-hourglass', middlewares: [ensureDevelopmentMode, authWithHeaders()], async handler (req, res) { const { user } = res.locals; await user.purchased.plan.updateHourglasses(user._id, 1, 'debug'); await user.save(); res.respond(200, {}); }, }; /** * @api {post} /api/v3/debug/set-cron Set lastCron for user * @apiName setCron * @apiGroup Development * @apiPermission Developers * * @apiSuccess {Object} data An empty Object */ api.setCron = { method: 'POST', url: '/debug/set-cron', middlewares: [ensureDevelopmentMode, authWithHeaders()], async handler (req, res) { const { user } = res.locals; const cron = req.body.lastCron; user.lastCron = cron; await user.save(); res.respond(200, {}); }, }; /** * @api {post} /api/v3/debug/make-admin Sets admin privileges for current user * @apiName setCron * @apiGroup Development * @apiPermission Developers * * @apiSuccess {Object} data An empty Object */ api.makeAdmin = { method: 'POST', url: '/debug/make-admin', middlewares: [ensureDevelopmentMode, authWithHeaders()], async handler (req, res) { const { user } = res.locals; user.permissions.fullAccess = true; await user.save(); res.respond(200, {}); }, }; /** * @api {post} /api/v3/debug/modify-inventory Manipulate user's inventory * @apiName modifyInventory * @apiGroup Development * @apiPermission Developers * * @apiParam (Body) {Object} gear Object to replace user's gear.owned object. * @apiParam (Body) {Object} special Object to replace user's special object. * @apiParam (Body) {Object} pets Object to replace user's pets object. * @apiParam (Body) {Object} mounts Object to replace user's mounts object. * @apiParam (Body) {Object} eggs Object to replace user's eggs object. * @apiParam (Body) {Object} hatchingPotions Object to replace user's hatchingPotions object. * @apiParam (Body) {Object} food Object to replace user's food object. * @apiParam (Body) {Object} quests Object to replace user's quests object. * @apiSuccess {Object} data An empty Object */ api.modifyInventory = { method: 'POST', url: '/debug/modify-inventory', middlewares: [ensureDevelopmentMode, authWithHeaders()], async handler (req, res) { const { user } = res.locals; const { gear } = req.body; if (gear) { user.items.gear.owned = gear; user.markModified('items.gear.owned'); } [ 'special', 'pets', 'mounts', 'eggs', 'hatchingPotions', 'food', 'quests', ].forEach(type => { if (req.body[type]) { user.items[type] = req.body[type]; user.markModified(`items.${type}`); } }); await user.save(); res.respond(200, {}); }, }; /** * @api {post} /api/v3/debug/quest-progress Artificially accelerate quest progress * @apiName questProgress * @apiGroup Development * @apiPermission Developers * * @apiSuccess {Object} data An empty Object */ api.questProgress = { method: 'POST', url: '/debug/quest-progress', middlewares: [ensureDevelopmentMode, authWithHeaders()], async handler (req, res) { const { user } = res.locals; const key = get(user, 'party.quest.key'); const quest = content.quests[key]; if (!quest) { throw new BadRequest('User is not on a valid quest.'); } if (quest.boss) { if (!user.party.quest.progress.up) user.party.quest.progress.up = 0; user.party.quest.progress.up += 1000; } if (quest.collect) { if (!user.party.quest.progress.collectedItems) user.party.quest.progress.collectedItems = 0; user.party.quest.progress.collectedItems += 300; } user.markModified('party.quest.progress'); await user.save(); res.respond(200, {}); }, }; /** * @api {post} /api/v3/debug/boss-rage Artificially trigger boss rage bar * @apiName bossRage * @apiGroup Development * @apiPermission Developers * * @apiSuccess {Object} data An empty Object */ api.bossRage = { method: 'POST', url: '/debug/boss-rage', middlewares: [ensureDevelopmentMode, authWithHeaders()], async handler (req, res) { const { user } = res.locals; const party = await Group.getGroup({ user, groupId: 'party', }); if (!party) { throw new BadRequest('User not in a party.'); } if (!party.quest.progress.rage) party.quest.progress.rage = 0; party.quest.progress.rage += 50; party.markModified('party.quest.progress.rage'); await party.save(); res.respond(200, {}); }, }; 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 (offsetDays) { if (clock === undefined) { fakeClock(); } try { clock.setSystemTime(moment().add(offsetDays, 'days').toDate()); } catch (e) { throw new BadRequest('Error adjusting time'); } } else { throw new BadRequest('Invalid command'); } if (mongoose.connection.readyState === 0) { await connectToMongoDB(); } res.respond(200, { time: new Date(), }); }, }; api.seedPartyChat = { method: 'POST', url: '/debug/seed-party-chat', middlewares: [ensureDevelopmentMode, authWithHeaders()], async handler (req, res) { const { user } = res.locals; const messageCount = Number(req.body.messageCount); if (!Number.isInteger(messageCount) || messageCount < 1) { throw new BadRequest('messageCount must be a positive integer.'); } if (!user.party._id) { throw new BadRequest('You are not in a party.'); } const party = await Group.findOne({ _id: user.party._id, type: 'party' }).exec(); if (!party) { throw new BadRequest('Party not found.'); } const messages = []; const baseTimestamp = Date.now(); for (let i = 1; i <= messageCount; i += 1) { const id = uuid(); messages.push({ _id: id, id, groupId: party._id, text: `#${i}`, unformattedText: `#${i}`, timestamp: new Date(baseTimestamp - (messageCount - i) * 1000), likes: {}, flags: {}, flagCount: 0, uuid: 'system', user: 'System', client: 'debug-seed', }); } await Chat.insertMany(messages); res.respond(200, { messageCount }); }, }; // Messaging ourselves for testing api.seedInbox = { method: 'POST', url: '/debug/seed-inbox', middlewares: [ensureDevelopmentMode, authWithHeaders()], async handler (req, res) { const { user } = res.locals; const messageCount = Number(req.body.messageCount); if (!Number.isInteger(messageCount) || messageCount < 1) { throw new BadRequest('messageCount must be a positive integer.'); } const messages = []; const baseTimestamp = Date.now(); for (let i = 1; i <= messageCount; i += 1) { const id = uuid(); messages.push({ _id: id, id, ownerId: user._id, uuid: user._id, user: user.profile.name, text: `#${i}`, unformattedText: `#${i}`, timestamp: new Date(baseTimestamp - (messageCount - i) * 1000), likes: {}, flags: {}, flagCount: 0, sent: true, client: 'debug-seed', }); } await Inbox.insertMany(messages); res.respond(200, { messageCount }); }, }; export default api;