diff --git a/common/script/count.js b/common/script/count.js index 4572e29dd8..3b4c303b3b 100644 --- a/common/script/count.js +++ b/common/script/count.js @@ -8,7 +8,7 @@ import content from './content/index'; const DROP_ANIMALS = keys(content.pets); -function beastMasterProgress (pets) { +function beastMasterProgress (pets = {}) { let count = 0; each(DROP_ANIMALS, (animal) => { @@ -19,7 +19,7 @@ function beastMasterProgress (pets) { return count; } -function dropPetsCurrentlyOwned (pets) { +function dropPetsCurrentlyOwned (pets = {}) { let count = 0; each(DROP_ANIMALS, (animal) => { @@ -30,7 +30,7 @@ function dropPetsCurrentlyOwned (pets) { return count; } -function mountMasterProgress (mounts) { +function mountMasterProgress (mounts = {}) { let count = 0; each(DROP_ANIMALS, (animal) => { @@ -41,7 +41,7 @@ function mountMasterProgress (mounts) { return count; } -function remainingGearInSet (userGear, set) { +function remainingGearInSet (userGear = {}, set) { let gear = filter(content.gear.flat, (item) => { let setMatches = item.klass === set; let hasItem = userGear[item.key]; @@ -54,7 +54,7 @@ function remainingGearInSet (userGear, set) { return count; } -function questsOfCategory (userQuests, category) { +function questsOfCategory (userQuests = {}, category) { let quests = filter(content.quests, (quest) => { let categoryMatches = quest.category === category; let hasQuest = userQuests[quest.key]; diff --git a/common/script/ops/sortTask.js b/common/script/ops/sortTask.js index ce002d8dc0..07db8289cf 100644 --- a/common/script/ops/sortTask.js +++ b/common/script/ops/sortTask.js @@ -21,7 +21,7 @@ module.exports = function sortTask (user, req = {}) { if (index === -1) { throw new NotFound(i18n.t('messageTaskNotFound', req.language)); } - if (!to && !fromParam) { + if (to == null && fromParam == null) { // eslint-disable-line eqeqeq throw new BadRequest('?to=__&from=__ are required'); } diff --git a/config.json.example b/config.json.example index d70dcac18c..9d37d70db7 100644 --- a/config.json.example +++ b/config.json.example @@ -10,6 +10,7 @@ "TEST_DB_URI":"mongodb://localhost/habitrpg_test", "NODE_ENV":"development", "CRON_SAFE_MODE":"false", + "CRON_SEMI_SAFE_MODE":"false", "MAINTENANCE_MODE": "false", "SESSION_SECRET":"YOUR SECRET HERE", "ADMIN_EMAIL": "you@example.com", diff --git a/test/api/v3/integration/debug/POST-debug_make-admin.test.js b/test/api/v3/integration/debug/POST-debug_make-admin.test.js index 69628aa8bc..98818fb07e 100644 --- a/test/api/v3/integration/debug/POST-debug_make-admin.test.js +++ b/test/api/v3/integration/debug/POST-debug_make-admin.test.js @@ -3,7 +3,7 @@ import { generateUser, } from '../../../../helpers/api-v3-integration.helper'; -xdescribe('POST /debug/make-admin (pended for v3 prod testing)', () => { +describe('POST /debug/make-admin (pended for v3 prod testing)', () => { let user; before(async () => { diff --git a/website/client/js/controllers/hallCtrl.js b/website/client/js/controllers/hallCtrl.js index 3aad20a782..8d9d385546 100644 --- a/website/client/js/controllers/hallCtrl.js +++ b/website/client/js/controllers/hallCtrl.js @@ -1,47 +1,53 @@ "use strict"; -habitrpg.controller("HallHeroesCtrl", ['$scope', '$rootScope', 'User', 'Notification', 'ApiUrl', '$resource', - function($scope, $rootScope, User, Notification, ApiUrl, $resource) { - var Hero = $resource(ApiUrl.get() + '/api/v3/hall/heroes/:uid', {uid:'@_id'}); +habitrpg.controller("HallHeroesCtrl", ['$scope', '$rootScope', 'User', 'Notification', 'ApiUrl', 'Hall', + function($scope, $rootScope, User, Notification, ApiUrl, Hall) { $scope.hero = undefined; - $scope.loadHero = function(uuid){ - Hero.query({uid:uuid}, function (heroData) { - $scope.hero = heroData.data; + $scope.currentHeroIndex = undefined; + $scope.heroes = []; + + Hall.getHeroes() + .then(function (response) { + $scope.heroes = response.data.data; }); + + $scope.loadHero = function(uuid, heroIndex) { + $scope.currentHeroIndex = heroIndex; + Hall.getHero(uuid) + .then(function (response) { + $scope.hero = response.data.data; + }); } + $scope.saveHero = function(hero) { $scope.hero.contributor.admin = ($scope.hero.contributor.level > 7) ? true : false; - hero.$save(function(){ - Notification.text("User updated"); - $scope.hero = undefined; - $scope._heroID = undefined; - Hero.query({}, function (heroesData) { - $scope.heroes = heroesData.data; + Hall.updateHero($scope.hero) + .then(function (response) { + Notification.text("User updated"); + $scope.hero = undefined; + $scope._heroID = undefined; + $scope.heroes[$scope.currentHeroIndex] = response.data.data; + $scope.currentHeroIndex = undefined; }); - }) } - Hero.query({}, function (heroesData) { - $scope.heroes = heroesData.data; - }); - $scope.populateContributorInput = function(id) { + $scope.populateContributorInput = function(id, index) { $scope._heroID = id; - window.scrollTo(0,200); - $scope.loadHero(id); + window.scrollTo(0, 200); + $scope.loadHero(id, index); }; }]); -habitrpg.controller("HallPatronsCtrl", ['$scope', '$rootScope', 'User', 'Notification', 'ApiUrl', '$resource', - function($scope, $rootScope, User, Notification, ApiUrl, $resource) { - var Patron = $resource(ApiUrl.get() + '/api/v3/hall/patrons/:uid', {uid:'@_id'}); - +habitrpg.controller("HallPatronsCtrl", ['$scope', '$rootScope', 'User', 'Notification', 'ApiUrl', 'Hall', + function($scope, $rootScope, User, Notification, ApiUrl, Hall) { var page = 0; $scope.patrons = []; - $scope.loadMore = function(){ - Patron.query({page: page++}, function(patronsData){ - $scope.patrons = $scope.patrons.concat(patronsData.data); - }) + $scope.loadMore = function() { + Hall.getPatrons(page++) + .then(function (response) { + $scope.patrons = $scope.patrons.concat(response.data.data); + }); } $scope.loadMore(); diff --git a/website/client/js/controllers/tasksCtrl.js b/website/client/js/controllers/tasksCtrl.js index fb44d19cb3..995b910122 100644 --- a/website/client/js/controllers/tasksCtrl.js +++ b/website/client/js/controllers/tasksCtrl.js @@ -193,24 +193,27 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N // Don't allow creation of an empty checklist item // TODO Provide UI feedback that this item is still blank } else if ($index == task.checklist.length - 1) { - Tasks.addChecklistItem(task._id, task.checklist[$index]); - task.checklist.push({completed:false,text:''}); - focusChecklist(task,task.checklist.length-1); + Tasks.addChecklistItem(task._id, task.checklist[$index]) + .then(function (response) { + task.checklist[$index] = response.data.data.checklist[$index]; + }); + task.checklist.push({completed:false, text:''}); + focusChecklist(task, task.checklist.length - 1); } else { $scope.saveTask(task, true); focusChecklist(task, $index + 1); } } - $scope.removeChecklistItem = function(task, $event, $index, force){ + $scope.removeChecklistItem = function(task, $event, $index, force) { // Remove item if clicked on trash icon if (force) { - Tasks.removeChecklistItem(task._id, task.checklist[$index].id); + if (task.checklist[$index].id) Tasks.removeChecklistItem(task._id, task.checklist[$index].id); task.checklist.splice($index, 1); } else if (!task.checklist[$index].text) { // User deleted all the text and is now wishing to delete the item // saveTask will prune the empty item - Tasks.removeChecklistItem(task._id, task.checklist[$index].id); + if (task.checklist[$index].id) Tasks.removeChecklistItem(task._id, task.checklist[$index].id); // Move focus if the list is still non-empty if ($index > 0) focusChecklist(task, $index-1); diff --git a/website/client/js/services/hallServices.js b/website/client/js/services/hallServices.js new file mode 100644 index 0000000000..a3b7e49751 --- /dev/null +++ b/website/client/js/services/hallServices.js @@ -0,0 +1,41 @@ +'use strict'; + +angular.module('habitrpg') +.factory('Hall', [ '$rootScope', 'ApiUrl', '$http', + function($rootScope, ApiUrl, $http) { + var apiV3Prefix = '/api/v3'; + var Hall = {}; + + Hall.getHeroes = function () { + return $http({ + method: 'GET', + url: apiV3Prefix + '/hall/heroes', + }); + } + + Hall.getHero = function (uuid) { + return $http({ + method: 'GET', + url: apiV3Prefix + '/hall/heroes/' + uuid, + }); + } + + Hall.updateHero = function (heroDetails) { + return $http({ + method: 'PUT', + url: apiV3Prefix + '/hall/heroes/' + heroDetails._id, + data: heroDetails, + }); + } + + Hall.getPatrons = function (page) { + if (!page) page = 0; + + return $http({ + method: 'GET', + url: apiV3Prefix + '/hall/patrons?page=' + page, + }); + } + + return Hall; + }]); diff --git a/website/client/js/services/taskServices.js b/website/client/js/services/taskServices.js index e7eeef1a93..7a99cd748e 100644 --- a/website/client/js/services/taskServices.js +++ b/website/client/js/services/taskServices.js @@ -1,6 +1,6 @@ 'use strict'; -var TASK_KEYS_TO_REMOVE = ['_id', 'completed', 'date', 'dateCompleted', 'history', 'id', 'streak', 'createdAt']; +var TASK_KEYS_TO_REMOVE = ['_id', 'completed', 'date', 'dateCompleted', 'history', 'id', 'streak', 'createdAt', 'challenge']; angular.module('habitrpg') .factory('Tasks', ['$rootScope', 'Shared', '$http', diff --git a/website/client/manifest.json b/website/client/manifest.json index 2c770893f1..fb59224a34 100644 --- a/website/client/manifest.json +++ b/website/client/manifest.json @@ -56,6 +56,7 @@ "js/services/socialServices.js", "js/services/statServices.js", "js/services/userServices.js", + "js/services/hallServices.js", "js/filters/money.js", "js/filters/roundLargeNumbers.js", diff --git a/website/server/controllers/api-v3/debug.js b/website/server/controllers/api-v3/debug.js index 9949421513..9315f9bf4e 100644 --- a/website/server/controllers/api-v3/debug.js +++ b/website/server/controllers/api-v3/debug.js @@ -88,21 +88,20 @@ api.setCron = { * * @apiSuccess {Object} data An empty Object */ -// TODO: Re-enable after v3 prod testing is done -// api.makeAdmin = { -// method: 'POST', -// url: '/debug/make-admin', -// middlewares: [ensureDevelpmentMode, authWithHeaders()], -// async handler (req, res) { -// let user = res.locals.user; -// -// user.contributor.admin = true; -// -// await user.save(); -// -// res.respond(200, {}); -// }, -// }; +api.makeAdmin = { + method: 'POST', + url: '/debug/make-admin', + middlewares: [ensureDevelpmentMode, authWithHeaders()], + async handler (req, res) { + let user = res.locals.user; + + user.contributor.admin = true; + + await user.save(); + + res.respond(200, {}); + }, +}; /** * @api {post} /api/v3/debug/modify-inventory Manipulate user's inventory diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index 919c2fb541..092aed83ba 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -357,7 +357,7 @@ function _generateWebhookTaskData (task, direction, delta, stats, user) { } /** - * @api {put} /api/v3/tasks/:taskId/score/:direction Score a task + * @api {post} /api/v3/tasks/:taskId/score/:direction Score a task * @apiVersion 3.0.0 * @apiName ScoreTask * @apiGroup Task diff --git a/website/server/controllers/top-level/pages.js b/website/server/controllers/top-level/pages.js index 614b1edc0a..0af5bb5f9c 100644 --- a/website/server/controllers/top-level/pages.js +++ b/website/server/controllers/top-level/pages.js @@ -48,6 +48,15 @@ _.each(staticPages, (name) => { }; }); +api.redirectApi = { + method: 'GET', + url: '/static/api', + runCron: false, + async handler (req, res) { + res.redirect(301, '/apidoc'); + }, +}; + let shareables = ['level-up', 'hatch-pet', 'raise-pet', 'unlock-quest', 'won-challenge', 'achievement']; _.each(shareables, (name) => { diff --git a/website/server/libs/api-v3/cron.js b/website/server/libs/api-v3/cron.js index da076b54ce..9f74df610b 100644 --- a/website/server/libs/api-v3/cron.js +++ b/website/server/libs/api-v3/cron.js @@ -5,6 +5,7 @@ import _ from 'lodash'; import nconf from 'nconf'; const CRON_SAFE_MODE = nconf.get('CRON_SAFE_MODE') === 'true'; +const CRON_SEMI_SAFE_MODE = nconf.get('CRON_SEMI_SAFE_MODE') === 'true'; const shouldDo = common.shouldDo; const scoreTask = common.ops.scoreTask; // const maxPMs = 200; @@ -175,13 +176,15 @@ export function cron (options = {}) { cron: true, }); - // 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 - // 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 - // setting between Trivial and Easy. + 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 + // 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 + // setting between Trivial and Easy. + } } } } diff --git a/website/server/models/group.js b/website/server/models/group.js index 53305d94b9..fc1fba287b 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -517,7 +517,8 @@ schema.statics.bossQuest = async function bossQuest (user, progress) { group.quest.progress.hp -= progress.up; // TODO Create a party preferred language option so emits like this can be localized. Suggestion: Always display the English version too. Or, if English is not displayed to the players, at least include it in a new field in the chat object that's visible in the database - essential for admins when troubleshooting quests! let playerAttack = `${user.profile.name} attacks ${quest.boss.name('en')} for ${progress.up.toFixed(1)} damage.`; - let bossAttack = nconf.get('CRON_SAFE_MODE') === 'true' ? `${quest.boss.name('en')} did not attack the party because it was asleep while maintenance was happening.` : `${quest.boss.name('en')} attacks party for ${Math.abs(down).toFixed(1)} damage.`; + let bossAttack = nconf.get('CRON_SAFE_MODE') === 'true' || nconf.get('CRON_SEMI_SAFE_MODE') === 'true' ? `${quest.boss.name('en')} does not attack, because it respects the fact that there are some bugs\` \`post-maintenance and it doesn't want to hurt anyone unfairly. It will continue its rampage soon!` : `${quest.boss.name('en')} attacks party for ${Math.abs(down).toFixed(1)} damage.`; + // TODO Consider putting the safe mode boss attack message in an ENV var group.sendChat(`\`${playerAttack}\` \`${bossAttack}\``); // If boss has Rage, increment Rage as well diff --git a/website/server/routes/pages.js b/website/server/routes/pages.js deleted file mode 100644 index 17818285ed..0000000000 --- a/website/server/routes/pages.js +++ /dev/null @@ -1,58 +0,0 @@ -var nconf = require('nconf'); -var express = require('express'); -var router = express.Router(); -var _ = require('lodash'); -var locals = require('../middlewares/api-v2/locals'); -var i18n = require('../libs/api-v2/i18n'); -var md = require('markdown-it')({ - html: true, -}); - -const TOTAL_USER_COUNT = '1,100,000'; - -// -------- App -------- -router.get('/', i18n.getUserLanguage, locals, function(req, res) { - if (!req.headers['x-api-user'] && !req.headers['x-api-key'] && !(req.session && req.session.userId)) - return res.redirect('/static/front'); - - return res.render('index', { - title: 'Habitica | Your Life The Role Playing Game', - env: res.locals.habitrpg - }); -}); - -// -------- Static Pages -------- - -var pages = ['front', 'privacy', 'terms', 'api', 'features', 'videos', 'contact', 'plans', 'new-stuff', 'community-guidelines', 'old-news', 'press-kit', 'faq', 'overview', 'apps', 'clear-browser-data', 'merch', 'maintenance-info']; - -_.each(pages, function(name){ - router.get('/static/' + name, i18n.getUserLanguage, locals, function(req, res) { - res.render( 'static/' + name, { - env: res.locals.habitrpg, - md: md, - userCount: TOTAL_USER_COUNT - }); - }); -}); - -// -------- Social Media Sharing -------- - -var shareables = ['level-up','hatch-pet','raise-pet','unlock-quest','won-challenge','achievement']; - -_.each(shareables, function(name){ - router.get('/social/' + name, i18n.getUserLanguage, locals, function(req, res) { - res.render( 'social/' + name, { - env: res.locals.habitrpg, - md: md, - userCount: TOTAL_USER_COUNT - }); - }); -}); - -// --------- Redirects -------- - -router.get('/static/extensions', function(req, res) { - res.redirect('http://habitica.wikia.com/wiki/App_and_Extension_Integrations'); -}); - -module.exports = router; diff --git a/website/views/options/social/hall.jade b/website/views/options/social/hall.jade index 381e5ec8fd..c5d0201666 100644 --- a/website/views/options/social/hall.jade +++ b/website/views/options/social/hall.jade @@ -88,7 +88,7 @@ script(type='text/ng-template', id='partials/options.social.hall.heroes.html') span(ng-class='userAdminGlyphiconStyle(hero)') span(ng-if='!hero.contributor.admin') a.label.label-default(ng-class='userLevelStyle(hero)', ng-click='clickMember(hero._id, true)') {{hero.profile.name}} - td(ng-if='user.contributor.admin', ng-click='populateContributorInput(hero._id)').btn-link {{hero._id}} + td(ng-if='user.contributor.admin', ng-click='populateContributorInput(hero._id, $index)').btn-link {{hero._id}} td {{hero.contributor.level}} td {{hero.contributor.text}} td diff --git a/website/views/shared/footer.jade b/website/views/shared/footer.jade index 0164b16783..26e017696c 100644 --- a/website/views/shared/footer.jade +++ b/website/views/shared/footer.jade @@ -98,8 +98,7 @@ footer.footer(ng-controller='FooterCtrl') a.btn.btn-default(ng-click='addLevelsAndGold()') +Exp +GP +MP a.btn.btn-default(ng-click='addOneLevel()') +1 Level a.btn.btn-default(ng-click='addQuestProgress()' tooltip="+1000 to boss quests. 300 items to collection quests") Quest Progress Up - // TODO Re-enable after v3 prod testing - // a.btn.btn-default(ng-click='makeAdmin()') Make Admin + a.btn.btn-default(ng-click='makeAdmin()') Make Admin a.btn.btn-default(ng-click='openModifyInventoryModal()') Modify Inventory div(ng-init='deferredScripts()')