Compare commits

..

10 Commits

Author SHA1 Message Date
Phillip Thelen ad8d0a3ed5 Revert "Use lean user for default getUser route"
This reverts commit 55cdd19215.
2026-02-03 15:46:25 +01:00
Phillip Thelen ac322b1925 fix issues with task routes 2026-02-03 15:46:01 +01:00
Phillip Thelen ff46459159 fix unit test 2026-02-03 15:20:41 +01:00
Phillip Thelen e952a1dbaf lint fix 2026-02-03 12:48:58 +01:00
Phillip Thelen e64428c89e optimize party seeking call 2026-02-03 11:04:24 +01:00
Phillip Thelen b97dfdfa83 Use lean in more places 2026-02-03 11:04:24 +01:00
Phillip Thelen e1a68cd02a optimize getting news posts 2026-02-03 11:04:24 +01:00
Phillip Thelen 5d7a3bedf7 don’t load list of mods on startup 2026-02-03 11:04:24 +01:00
Phillip Thelen 55cdd19215 Use lean user for default getUser route 2026-02-03 11:04:24 +01:00
Phillip Thelen ce24ce3079 Optimize tag api calls 2026-02-03 11:04:24 +01:00
35 changed files with 254 additions and 155 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "habitica",
"version": "5.45.0",
"version": "5.44.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "habitica",
"version": "5.45.0",
"version": "5.44.2",
"hasInstallScript": true,
"dependencies": {
"@babel/core": "^7.22.10",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.45.0",
"version": "5.44.2",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
@@ -0,0 +1,35 @@
import { v4 as generateUUID } from 'uuid';
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
xdescribe('GET /export/avatar-:memberId.html', () => {
let user;
before(async () => {
user = await generateUser();
});
it('validates req.params.memberId', async () => {
await expect(user.get('/export/avatar-:memberId.html')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('handles non-existing members', async () => {
const dummyId = generateUUID();
await expect(user.get(`/export/avatar-${dummyId}.html`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', { userId: dummyId }),
});
});
it('returns an html page', async () => {
const res = await user.get(`/export/avatar-${user._id}.html`);
expect(res.substring(0, 100).indexOf('<!DOCTYPE html>')).to.equal(0);
});
});
@@ -0,0 +1,3 @@
// TODO how to test this route since it points to a file on AWS s3?
describe('GET /export/avatar-:memberId.png', () => {});
@@ -498,13 +498,8 @@ export default {
await this.triggerGetWorldState();
this.currentEvent = _find(this.currentEventList, event => Boolean(event.season));
if (this.currentEvent.season === 'valentines') {
this.imageURLs.background = 'url(/static/npc/spring/seasonal_shop_opened_background.png)';
this.imageURLs.npc = 'url(/static/npc/spring/seasonal_shop_opened_npc.png)';
} else {
this.imageURLs.background = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_background.png)`;
this.imageURLs.npc = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_npc.png)`;
}
this.imageURLs.background = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_background.png)`;
this.imageURLs.npc = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_npc.png)`;
},
beforeDestroy () {
this.$root.$off('buyModal::boughtItem');
+19 -3
View File
@@ -348,6 +348,7 @@
import throttle from 'lodash/throttle';
import isEmpty from 'lodash/isEmpty';
import draggable from 'vuedraggable';
import { shouldDo } from '@/../../common/script/cron';
import inAppRewards from '@/../../common/script/libs/inAppRewards';
import taskDefaults from '@/../../common/script/libs/taskDefaults';
import Task from './task';
@@ -481,10 +482,25 @@ export default {
return this.$t('addATask', { type });
},
badgeCount () {
if (this.type === 'reward') {
return 0;
// 0 means the badge will not be shown
// It is shown for the all and due views of dailies
// and for the active and scheduled views of todos.
if (this.type === 'todo' && this.activeFilter.label !== 'complete2') {
return this.taskList.length;
} if (this.type === 'daily') {
if (this.activeFilter.label === 'due') {
return this.taskList.length;
} if (this.activeFilter.label === 'all') {
return this.taskList
.reduce(
(count, t) => (!t.completed
&& shouldDo(new Date(), t, this.getUserPreferences) ? count + 1 : count),
0,
);
}
}
return this.taskList.length;
return 0;
},
},
watch: {
@@ -1340,7 +1340,7 @@ export default {
},
openAdminPanel () {
this.$router.push(`/admin/panel/${this.hero._id}`);
this.$router.push(`/admin-panel/${this.hero._id}`);
},
},
};
@@ -43,7 +43,7 @@
<strong>{{ $t('equipment') }}:</strong>
<span :class="{ 'positive-stat': statsComputed.gearBonus[stat] !== 0 }">
{{ statsComputed.gearBonus[stat] !== 0 ? '+' : '' }}{{
statsComputed.gearBonus[stat] + statsComputed.classBonus[stat]
statsComputed.gearBonus[stat]
}}
</span>
</li>
@@ -246,9 +246,7 @@
:class="{white: user.preferences.background}"
style="overflow:hidden"
>
<Sprite
v-if="user.preferences.background && user.preferences.background !== ''"
:image-name="'icon_background_' + user.preferences.background" />
<Sprite :image-name="'icon_background_' + user.preferences.background" />
</div>
<b-popover
v-if="label !== 'skip'
+1 -1
View File
@@ -122,7 +122,7 @@ export default defineConfig({
},
rollupOptions: {
output: {
experimentalMinChunkSize: 20000
experimentalMinChunkSize: 1000
}
}
},
+5 -2
View File
@@ -1353,6 +1353,8 @@ api.getLookingForParty = {
const seekers = await User
.find({
'auth.blocked': { $ne: true },
'flags.chatRevoked': { $ne: true },
'party.seeking': { $exists: true },
'invitations.party.id': { $exists: false },
'auth.timestamps.loggedin': {
@@ -1360,12 +1362,13 @@ api.getLookingForParty = {
},
})
// eslint-disable-next-line no-multi-str
.select('_id auth.blocked auth.local.username auth.timestamps backer contributor.level \
flags.chatRevoked flags.classSelected inbox.blocks invitations.party items.gear.costume \
.select('_id auth.local.username auth.timestamps backer contributor.level \
flags.classSelected inbox.blocks invitations.party items.gear.costume \
items.gear.equipped loginIncentives party._id preferences.background preferences.chair \
preferences.costume preferences.hair preferences.shirt preferences.size preferences.skin \
preferences.language profile.name stats.buffs stats.class stats.lvl')
.sort('-auth.timestamps.loggedin')
.lean()
.exec();
const filteredSeekers = seekers.filter(seeker => {
+1 -1
View File
@@ -24,7 +24,7 @@ const api = {};
api.getInboxMessages = {
method: 'GET',
url: '/inbox/messages',
middlewares: [authWithHeaders({ userFieldsToInclude: ['profile', 'contributor', 'backer', 'inbox'] })],
middlewares: [authWithHeaders({ leanUser: true, userFieldsToInclude: ['profile', 'contributor', 'backer', 'inbox'] })],
async handler (req, res) {
const { user } = res.locals;
const { page } = req.query;
+6 -6
View File
@@ -40,7 +40,7 @@ const api = {};
api.createTag = {
method: 'POST',
url: '/tags',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({ userFieldsToInclude: ['tags'] })],
async handler (req, res) {
const { user } = res.locals;
@@ -69,7 +69,7 @@ api.createTag = {
api.getTags = {
method: 'GET',
url: '/tags',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({ leanUser: true, userFieldsToInclude: ['tags'] })],
async handler (req, res) {
const { user } = res.locals;
res.respond(200, user.tags);
@@ -95,7 +95,7 @@ api.getTags = {
api.getTag = {
method: 'GET',
url: '/tags/:tagId',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({ leanUser: true, userFieldsToInclude: ['tags'] })],
async handler (req, res) {
const { user } = res.locals;
@@ -133,7 +133,7 @@ api.getTag = {
api.updateTag = {
method: 'PUT',
url: '/tags/:tagId',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({ userFieldsToInclude: ['tags'] })],
async handler (req, res) {
const { user } = res.locals;
@@ -175,7 +175,7 @@ api.updateTag = {
api.reorderTags = {
method: 'POST',
url: '/reorder-tags',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({ userFieldsToInclude: ['tags'] })],
async handler (req, res) {
const { user } = res.locals;
@@ -215,7 +215,7 @@ api.reorderTags = {
api.deleteTag = {
method: 'DELETE',
url: '/tags/:tagId',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({ userFieldsToInclude: ['tags'] })],
async handler (req, res) {
const { user } = res.locals;
+2 -2
View File
@@ -388,7 +388,7 @@ api.getUserTasks = {
method: 'GET',
url: '/tasks/user',
middlewares: [authWithHeaders({
// Some fields (including _id, preferences) are always loaded (see middlewares/auth)
leanUser: true,
userFieldsToInclude: ['tasksOrder'],
})],
async handler (req, res) {
@@ -953,7 +953,7 @@ api.addChecklistItem = {
api.scoreCheckListItem = {
method: 'POST',
url: '/tasks/:taskId/checklist/:itemId/score',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({ leanUser: true, userFieldsToInclude: ['_id'] })],
async handler (req, res) {
const { user } = res.locals;
+1 -1
View File
@@ -406,7 +406,7 @@ api.getUserAnonymized = {
{ type: { $in: ['habit', 'daily', 'reward'] } },
],
};
const tasks = await Tasks.Task.find(query).exec();
const tasks = await Tasks.Task.find(query).lean().exec();
forEach(tasks, task => {
task.text = 'task text';
@@ -22,6 +22,7 @@ api.purchaseHistory = {
let transactions = await Transaction
.find({ userId: req.params.memberId })
.sort({ createdAt: -1 })
.lean()
.exec();
if (!res.locals.user.hasPermission('userSupport')) {
@@ -1,39 +0,0 @@
import {
disableCache,
} from '../../middlewares/cache';
import SERVER_STATUS from '../../libs/serverStatus';
const api = {};
/**
* @api {get} /api/v3/ready Get Habitica's Server readiness status
* @apiName GetReady
* @apiGroup Status
*
* @apiSuccess {String} data.status 'ready' if everything is ok
*
* @apiSuccessExample {JSON} Server is Ready
* {
* 'status': 'ready',
* }
*/
api.getReady = {
method: 'GET',
url: '/ready',
// explicitly disable caching so that the server is always checked
middlewares: [disableCache],
async handler (req, res) {
// This allows kubernetes to determine if the server is ready to receive traffic
if (!SERVER_STATUS.MONGODB || !SERVER_STATUS.REDIS || !SERVER_STATUS.EXPRESS) {
res.respond(503, {
status: 'not ready',
});
} else {
res.respond(200, {
status: 'ready',
});
}
},
};
export default api;
+3 -1
View File
@@ -313,7 +313,9 @@ api.purchaseHistory = {
url: '/user/purchase-history',
async handler (req, res) {
const { user } = res.locals;
const transactions = await Transaction.find({ userId: user._id }).sort({ createdAt: -1 });
const transactions = await Transaction.find({ userId: user._id })
.sort({ createdAt: -1 })
.lean();
res.respond(200, transactions);
},
};
@@ -6,10 +6,18 @@ import moment from 'moment';
import md from 'habitica-markdown';
import csvStringify from '../../libs/csvStringify';
import { marshallUserData } from '../../libs/xmlMarshaller';
import { NotFound } from '../../libs/errors';
import * as Tasks from '../../models/task';
import * as inboxLib from '../../libs/inbox';
// import { model as User } from '../../models/user';
import { authWithSession } from '../../middlewares/auth';
/* import {
S3,
} from '../../libs/aws'; */
// const S3_BUCKET = nconf.get('S3_BUCKET');
// const BASE_URL = nconf.get('BASE_URL');
const api = {};
@@ -45,7 +53,7 @@ api.exportUserHistory = {
const tasks = await Tasks.Task.find({
userId: user._id,
type: { $in: ['habit', 'daily'] },
}).exec();
}).lean().exec();
const output = [
['Task Name', 'Task ID', 'Task Type', 'Date', 'Value'],
@@ -84,7 +92,7 @@ async function _getUserDataForExport (user) {
const [tasks, messages] = await Promise.all([
Tasks.Task.find({
userId: user._id,
}).exec(),
}).lean().exec(),
inboxLib.getUserInbox(user, { asArray: false }),
]);
@@ -92,7 +100,6 @@ async function _getUserDataForExport (user) {
userData.inbox.messages = messages;
_(tasks)
.map(task => task.toJSON())
.groupBy(task => task.type)
.forEach((tasksPerType, taskType) => {
userData.tasks[`${taskType}s`] = tasksPerType;
@@ -149,6 +156,122 @@ api.exportUserDataXml = {
},
};
/**
* @api {get} /export/avatar-:uuid.html Render a user avatar as an HTML page
* @apiName ExportUserAvatarHtml
* @apiDescription This HTML export feature is not currently working (https://github.com/HabitRPG/habitica/issues/9489).
* @apiGroup DataExport
*
* @apiParam (Path) {String} uuid The User ID of the user
*
* @apiSuccess {HTML} File An html page rendering the user's avatar.
*
* @apiUse UserNotFound
*/
// @TODO fix
api.exportUserAvatarHtml = {
method: 'GET',
url: '/export/avatar-:memberId.html',
// middlewares: [locals],
async handler (/* req, res */) {
throw new NotFound('This API route is currently not available. See https://github.com/HabitRPG/habitica/issues/9489.');
/* req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
const { memberId } = req.params;
throw new NotFound('This API route is currently not available. See https://github.com/HabitRPG/habitica/issues/9489.');
const member = await User
.findById(memberId)
.select('stats profile items achievements preferences backer contributor')
.exec();
if (!member) throw new NotFound(res.t('userWithIDNotFound', { userId: memberId }));
res.render('avatar-static', {
title: member.profile.name,
env: _.defaults({ user: member }, res.locals.habitrpg),
}); */
},
};
/**
* @api {get} /export/avatar-:uuid.png Render a user avatar as a PNG file
* @apiName ExportUserAvatarPng
* @apiDescription This PNG export feature is not currently working (https://github.com/HabitRPG/habitica/issues/9489).
* @apiGroup DataExport
*
* @apiParam (Path) {String} uuid The User ID of the user
*
* @apiSuccess {PNG} File A png file of the user's avatar.
*/
api.exportUserAvatarPng = {
method: 'GET',
url: '/export/avatar-:memberId.png',
async handler (/* req, res */) {
throw new NotFound('This API route is currently not available. See https://github.com/HabitRPG/habitica/issues/9489.');
/* req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
const { memberId } = req.params;
const filename = `avatars/${memberId}.png`;
const s3url = `https://${S3_BUCKET}.s3.amazonaws.com/${filename}`;
let response;
try {
response = await got.head(s3url); // TODO add timeout and retries
} catch (gotError) {
// If the file does not exist AWS S3 can return a 403 error
if (gotError.code !== 'ENOTFOUND' && gotError.statusCode
!== 404 && gotError.statusCode !== 403) {
throw gotError;
}
}
// cache images for 30 minutes on aws, else upload a new one
if (response && response.statusCode === 200 && moment()
.diff(response.headers['last-modified'], 'minutes') < 30) {
return res.redirect(s3url);
}
const pageBuffer = await new Pageres()
.src(`${BASE_URL}/export/avatar-${memberId}.html`, ['140x147'], {
crop: true,
filename: filename.replace('.png', ''),
})
.run();
const s3upload = S3.upload({
Bucket: S3_BUCKET,
Key: filename,
ACL: 'public-read',
StorageClass: 'REDUCED_REDUNDANCY',
ContentType: 'image/png',
Expires: moment().add({ minutes: 5 }).toDate(),
Body: pageBuffer,
});
const s3res = await new Promise((resolve, reject) => {
s3upload.send((err, s3uploadRes) => {
if (err) {
reject(err);
} else {
resolve(s3uploadRes);
}
});
});
return res.redirect(s3res.Location); */
},
};
/**
* @api {get} /export/inbox.html Export user private messages as HTML document
* @apiName ExportUserPrivateMessages
+1
View File
@@ -22,6 +22,7 @@ export async function sendChatPushNotifications (user, group, message, mentions,
'party._id': group._id,
_id: { $ne: user._id },
})
.lean()
.select('preferences.pushNotifications preferences.language profile.name pushDevices auth.local.username')
.exec();
+2 -2
View File
@@ -25,13 +25,13 @@ export async function getGroupChat (group, options = {}) {
.sort('-timestamp');
if (before) {
const beforeMessage = await Chat.findOne({ _id: before }).exec();
const beforeMessage = await Chat.findOne({ _id: before }, { timestamp: 1 }).lean().exec();
if (beforeMessage) {
query = query.where('timestamp').lt(beforeMessage.timestamp);
}
}
const groupChat = await query.limit(effectiveLimit).exec();
const groupChat = await query.limit(effectiveLimit).lean().exec();
// @TODO: Concat old chat to keep continuity of chat stored on group object
const currentGroupChat = group.chat || [];
@@ -22,7 +22,7 @@ async function usersMapByConversations (users) {
stats: 1,
flags: 1,
inbox: 1,
}).exec();
}).lean().exec();
for (const usr of loadedUsers) {
const loadedUserConversation = {
+2 -2
View File
@@ -3,7 +3,6 @@ import winston from 'winston';
import { Loggly } from 'winston-loggly-bulk';
import nconf from 'nconf';
import _ from 'lodash';
import os from 'os';
import {
CustomError,
} from './errors';
@@ -66,8 +65,9 @@ if (IS_PROD) {
),
}));
}
if (LOGGLY_TOKEN && LOGGLY_SUBDOMAIN) {
const tags = ['Winston-NodeJS', os.hostname()];
const tags = ['Winston-NodeJS'];
if (nconf.get('SERVER_EMOJI')) {
tags.push(nconf.get('SERVER_EMOJI'));
}
-8
View File
@@ -5,7 +5,6 @@ import {
getDevelopmentConnectionUrl,
getDefaultConnectionOptions,
} from './mongodb';
import SERVER_STATUS from './serverStatus';
const IS_PROD = nconf.get('IS_PROD');
const MAINTENANCE_MODE = nconf.get('MAINTENANCE_MODE');
@@ -25,13 +24,6 @@ const connectionUrl = IS_PROD ? DB_URI : getDevelopmentConnectionUrl(DB_URI);
export default async function connectToMongoDB () {
// Do not connect to MongoDB when in maintenance mode
if (MAINTENANCE_MODE !== 'true') {
mongoose.connection.on('open', () => {
SERVER_STATUS.MONGODB = true;
});
mongoose.connection.on('disconnected', () => {
SERVER_STATUS.MONGODB = false;
});
return mongoose.connect(connectionUrl, mongooseOptions).then(() => {
logger.info('Connected with Mongoose.');
});
+4 -1
View File
@@ -169,7 +169,10 @@ api.subscribe = async function subscribe (user, receipt, headers, nextPaymentPro
{ 'purchased.plan.customerId': purchase.originalTransactionId },
{ 'purchased.plan.customerId': purchase.transactionId },
],
}).exec();
}, {
_id: 1,
'purchased.plan': 1,
}).lean().exec();
if (existingUsers.length > 0) {
if (purchase.originalTransactionId === purchase.transactionId) {
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
-7
View File
@@ -1,7 +0,0 @@
const SERVER_STATUS = {
MONGODB: false,
REDIS: false,
EXPRESS: false,
};
export default SERVER_STATUS;
+12 -3
View File
@@ -178,7 +178,7 @@ async function getTasks (req, res, options = {}) {
],
},
{ _id: 1 },
).exec();
).lean().exec();
}
if (upgradedGroups.length > 0) {
for (const upgradedGroup of upgradedGroups) {
@@ -270,7 +270,6 @@ async function getTasks (req, res, options = {}) {
remove(taskOrder, taskId => tasks.findIndex(task => task._id === taskId) === -1);
if (preLength !== taskOrder.length) {
owner.tasksOrder[key] = taskOrder;
owner.markModified('tasksOrder');
ownerDirty = true;
}
});
@@ -303,7 +302,17 @@ async function getTasks (req, res, options = {}) {
}
});
if (ownerDirty) await owner.save();
if (ownerDirty) {
let model;
if (challenge) {
model = Challenge;
} else if (group) {
model = Group;
} else {
model = User;
}
await model.updateOne({ _id: owner._id }, { tasksOrder: owner.tasksOrder }).exec();
}
// Remove empty values from the array and add any unordered task
orderedTasks = compact(orderedTasks).concat(unorderedTasks);
+1 -1
View File
@@ -82,7 +82,7 @@ export function setNextDue (task, user, dueDateOption) {
now = dateTaskIsDue;
}
const optionsForShouldDo = user.preferences.toObject();
const optionsForShouldDo = user.preferences;
optionsForShouldDo.now = now;
task.isDue = shared.shouldDo(dateTaskIsDue, task, optionsForShouldDo);
+1 -1
View File
@@ -186,7 +186,7 @@ export async function update (req, res, { isV3 = false }) {
],
}, {
_id: 1,
}).exec();
}).lean().exec();
matchingGroupsArray = _.map(matchingGroups, groupRecord => groupRecord._id);
}
-11
View File
@@ -10,7 +10,6 @@ import {
} from '../libs/errors';
import logger from '../libs/logger';
import { apiError } from '../libs/apiError';
import SERVER_STATUS from '../libs/serverStatus';
// Middleware to rate limit requests to the API
@@ -48,14 +47,6 @@ if (RATE_LIMITER_ENABLED) {
enable_offline_queue: false,
});
redisClient.on('ready', () => {
SERVER_STATUS.REDIS = true;
});
redisClient.on('reconnecting', () => {
SERVER_STATUS.REDIS = false;
});
redisClient.on('error', error => {
logger.error(error, 'Redis Error');
});
@@ -65,8 +56,6 @@ if (RATE_LIMITER_ENABLED) {
storeClient: redisClient,
});
}
} else {
SERVER_STATUS.REDIS = true;
}
function setResponseHeaders (res, rateLimiterRes) {
@@ -41,7 +41,7 @@ export const logRequestData = (req, res, next) => {
export const logSlowRequests = (req, res, next) => {
req.requestStartTime = Date.now();
req.once('close', () => {
req.on('close', () => {
const requestTime = Date.now() - req.requestStartTime;
if (requestTime > SLOW_REQUEST_THRESHOLD) {
const data = buildBaseLogData(req);
+1
View File
@@ -43,6 +43,7 @@ schema.statics.getNews = async function getNews (isAdmin, options = { page: 0 })
.sort({ publishDate: -1 })
.limit(POSTS_PER_PAGE)
.skip(POSTS_PER_PAGE * Number(page))
.lean()
.exec();
};
-17
View File
@@ -1,6 +1,5 @@
import mongoose from 'mongoose';
import logger from '../../libs/logger';
import schema from './schema'; // eslint-disable-line import/no-cycle
import './hooks'; // eslint-disable-line import/no-cycle
@@ -19,19 +18,3 @@ export const nameFields = 'profile.name auth.local.username flags.verifiedUserna
export { schema };
export const model = mongoose.model('User', schema);
// Initially export an empty object so external requires will get
// the right object by reference when it's defined later
// Otherwise it would remain undefined if requested before the query executes
export const mods = [];
mongoose.model('User')
.find({ 'contributor.moderator': true })
.sort('-contributor.level -backer.npc profile.name')
.select('profile contributor backer')
.exec()
.then(foundMods => {
// Using push to maintain the reference to mods
mods.push(...foundMods);
})
.catch(err => logger.error(err));
+16 -12
View File
@@ -367,14 +367,14 @@ schema.methods.getUtcOffset = function getUtcOffset () {
return common.fns.getUtcOffset(this);
};
schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
schema.statics.daysUserHasMissed = function daysUserHasMissed (user, now, req = {}) {
// If the user's timezone has changed (due to travel or daylight savings),
// cron can be triggered twice in one day, so we check for that and use
// both timezones to work out if cron should run.
// CDS = Custom Day Start time.
let timezoneUtcOffsetFromUserPrefs = this.getUtcOffset();
const timezoneUtcOffsetAtLastCron = Number.isFinite(this.preferences.timezoneOffsetAtLastCron)
? -this.preferences.timezoneOffsetAtLastCron
let timezoneUtcOffsetFromUserPrefs = common.fns.getUtcOffset(user);
const timezoneUtcOffsetAtLastCron = Number.isFinite(user.preferences.timezoneOffsetAtLastCron)
? -user.preferences.timezoneOffsetAtLastCron
: timezoneUtcOffsetFromUserPrefs;
let timezoneUtcOffsetFromBrowser = typeof req.header === 'function' && -Number(req.header('x-user-timezoneoffset'));
@@ -386,16 +386,16 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
if (timezoneUtcOffsetFromBrowser !== timezoneUtcOffsetFromUserPrefs) {
// The user's browser has just told Habitica that the user's timezone has
// changed so store and use the new zone.
this.preferences.timezoneOffset = -timezoneUtcOffsetFromBrowser;
user.preferences.timezoneOffset = -timezoneUtcOffsetFromBrowser;
timezoneUtcOffsetFromUserPrefs = timezoneUtcOffsetFromBrowser;
}
let lastCronTime = this.lastCron;
if (this.auth.timestamps.loggedIn < lastCronTime) {
lastCronTime = this.auth.timestamps.loggedIn;
let lastCronTime = user.lastCron;
if (user.auth.timestamps.loggedIn < lastCronTime) {
lastCronTime = user.auth.timestamps.loggedIn;
}
// How many days have we missed using the user's current timezone:
let daysMissed = daysSince(lastCronTime, defaults({ now }, this.preferences));
let daysMissed = daysSince(lastCronTime, defaults({ now }, user.preferences));
if (timezoneUtcOffsetAtLastCron !== timezoneUtcOffsetFromUserPrefs) {
// Give the user extra time based on the difference in timezones
@@ -410,7 +410,7 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
const daysMissedOldZone = daysSince(lastCronTime, defaults({
now,
timezoneUtcOffsetOverride: timezoneUtcOffsetAtLastCron,
}, this.preferences));
}, user.preferences));
if (timezoneUtcOffsetAtLastCron > timezoneUtcOffsetFromUserPrefs) {
// The timezone change was in the unsafe direction.
@@ -447,12 +447,12 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
const timezoneOffsetDiff = timezoneUtcOffsetFromUserPrefs - timezoneUtcOffsetAtLastCron;
// e.g., for dangerous zone change: -300 - -240 = -60 or 600 - 660= -60
this.lastCron = moment(lastCronTime).subtract(timezoneOffsetDiff, 'minutes');
user.lastCron = moment(lastCronTime).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.
// From now on we can ignore the old timezone:
// This is still timezoneOffset for backwards compatibility reasons.
this.preferences.timezoneOffsetAtLastCron = -timezoneUtcOffsetAtLastCron;
user.preferences.timezoneOffsetAtLastCron = -timezoneUtcOffsetAtLastCron;
} else {
// Both old and new timezones indicate that cron should
// NOT run.
@@ -474,6 +474,10 @@ schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
return { daysMissed, timezoneUtcOffsetFromUserPrefs };
};
schema.methods.daysUserHasMissed = function daysUserHasMissed (now, req = {}) {
return schema.statics.daysUserHasMissed(this, now, req);
};
async function getUserGroupData (user) {
const userGroups = user.getGroups();
-13
View File
@@ -1,8 +1,6 @@
import nconf from 'nconf';
import express from 'express';
import http from 'http';
import mongoose from 'mongoose';
import redis from 'redis';
import logger from './libs/logger';
// Setup translations
@@ -20,22 +18,12 @@ import './libs/setupFirebase';
import './models/challenge';
import './models/group';
import './models/user';
import SERVER_STATUS from './libs/serverStatus';
connectToMongoDB();
const server = http.createServer();
const app = express();
process.on('SIGTERM', async () => {
console.log('SIGTERM signal received: closing HTTP server');
server.close(async () => {
await mongoose.disconnect();
await redis.quit();
process.exit(0);
});
});
app.set('port', nconf.get('PORT'));
attachMiddlewares(app, server);
@@ -43,7 +31,6 @@ attachMiddlewares(app, server);
server.on('request', app);
server.listen(app.get('port'), () => {
logger.info(`Express server listening on port ${app.get('port')}`);
SERVER_STATUS.EXPRESS = true;
});
export default server;