mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
Merge branch 'develop' into party-chat-translations
This commit is contained in:
@@ -329,7 +329,7 @@ api.leaveChallenge = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /api/v3/challenges/user Get challenges for a user.
|
||||
* @api {get} /api/v3/challenges/user Get challenges for a user
|
||||
* @apiName GetUserChallenges
|
||||
* @apiGroup Challenge
|
||||
* @apiDescription Get challenges the user has access to. Includes public challenges, challenges belonging to the user's group, and challenges the user has already joined.
|
||||
@@ -640,7 +640,7 @@ api.exportChallengeCsv = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {put} /api/v3/challenges/:challengeId Update the name, description, or leader of a challenge.
|
||||
* @api {put} /api/v3/challenges/:challengeId Update the name, description, or leader of a challenge
|
||||
*
|
||||
* @apiName UpdateChallenge
|
||||
* @apiGroup Challenge
|
||||
|
||||
@@ -159,6 +159,7 @@ api.postChat = {
|
||||
}
|
||||
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (group.privacy !== 'private' && user.flags.chatRevoked) {
|
||||
throw new NotAuthorized(res.t('chatPrivilegesRevoked'));
|
||||
}
|
||||
|
||||
@@ -590,11 +590,6 @@ api.joinGroup = {
|
||||
// @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()) {
|
||||
await payments.addSubToGroupUser(user, group);
|
||||
await group.updateGroupPlan();
|
||||
}
|
||||
|
||||
group.memberCount += 1;
|
||||
|
||||
let promises = [group.save(), user.save()];
|
||||
@@ -638,6 +633,11 @@ api.joinGroup = {
|
||||
|
||||
promises = await Promise.all(promises);
|
||||
|
||||
if (group.hasNotCancelled()) {
|
||||
await payments.addSubToGroupUser(user, group);
|
||||
await group.updateGroupPlan();
|
||||
}
|
||||
|
||||
let response = await Group.toJSONCleanChat(promises[0], user);
|
||||
let leader = await User.findById(response.leader).select(nameFields).exec();
|
||||
if (leader) {
|
||||
@@ -790,7 +790,6 @@ api.leaveGroup = {
|
||||
}
|
||||
|
||||
await group.leave(user, req.query.keep, req.body.keepChallenges);
|
||||
if (group.hasNotCancelled()) await group.updateGroupPlan(true);
|
||||
_removeMessagesFromMember(user, group._id);
|
||||
await user.save();
|
||||
|
||||
@@ -804,6 +803,7 @@ api.leaveGroup = {
|
||||
await payments.cancelGroupSubscriptionForUser(user, group);
|
||||
}
|
||||
|
||||
if (group.hasNotCancelled()) await group.updateGroupPlan(true);
|
||||
res.respond(200, {});
|
||||
},
|
||||
};
|
||||
@@ -892,10 +892,6 @@ api.removeGroupMember = {
|
||||
|
||||
if (isInGroup) {
|
||||
group.memberCount -= 1;
|
||||
if (group.hasNotCancelled()) {
|
||||
await group.updateGroupPlan(true);
|
||||
await payments.cancelGroupSubscriptionForUser(member, group, true);
|
||||
}
|
||||
|
||||
if (group.quest && group.quest.leader === member._id) {
|
||||
group.quest.key = undefined;
|
||||
@@ -944,6 +940,12 @@ api.removeGroupMember = {
|
||||
member.save(),
|
||||
group.save(),
|
||||
]);
|
||||
|
||||
if (isInGroup && group.hasNotCancelled()) {
|
||||
await group.updateGroupPlan(true);
|
||||
await payments.cancelGroupSubscriptionForUser(member, group, true);
|
||||
}
|
||||
|
||||
res.respond(200, {});
|
||||
},
|
||||
};
|
||||
@@ -1213,6 +1215,16 @@ api.inviteToGroup = {
|
||||
results.push(...emailResults);
|
||||
}
|
||||
|
||||
let analyticsObject = {
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
groupType: group.type,
|
||||
headers: req.headers,
|
||||
};
|
||||
|
||||
res.analytics.track('group invite', analyticsObject);
|
||||
|
||||
res.respond(200, results);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -273,6 +273,7 @@ api.updateHero = {
|
||||
if (updateData.auth && updateData.auth.blocked === false) {
|
||||
hero.auth.blocked = false;
|
||||
}
|
||||
|
||||
if (updateData.flags && _.isBoolean(updateData.flags.chatRevoked)) hero.flags.chatRevoked = updateData.flags.chatRevoked;
|
||||
|
||||
let savedHero = await hero.save();
|
||||
|
||||
@@ -16,7 +16,7 @@ function geti18nBrowserScript (language) {
|
||||
availableLanguages,
|
||||
language,
|
||||
strings: translations[langCode],
|
||||
momentLang: momentLangs[language.momentLangCode],
|
||||
momentLang: momentLangs[langCode],
|
||||
})};
|
||||
})()`;
|
||||
}
|
||||
|
||||
@@ -478,25 +478,25 @@ api.sendPrivateMessage = {
|
||||
req.checkBody('message', res.t('messageRequired')).notEmpty();
|
||||
req.checkBody('toUserId', res.t('toUserIDRequired')).notEmpty().isUUID();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let sender = res.locals.user;
|
||||
let message = req.body.message;
|
||||
let receiver = await User.findById(req.body.toUserId).exec();
|
||||
const sender = res.locals.user;
|
||||
const message = req.body.message;
|
||||
const receiver = await User.findById(req.body.toUserId).exec();
|
||||
if (!receiver) throw new NotFound(res.t('userNotFound'));
|
||||
|
||||
let objections = sender.getObjectionsToInteraction('send-private-message', receiver);
|
||||
|
||||
const objections = sender.getObjectionsToInteraction('send-private-message', receiver);
|
||||
if (objections.length > 0 && !sender.isAdmin()) throw new NotAuthorized(res.t(objections[0]));
|
||||
|
||||
await sender.sendMessage(receiver, { receiverMsg: message });
|
||||
const newMessage = await sender.sendMessage(receiver, { receiverMsg: message });
|
||||
|
||||
if (receiver.preferences.emailNotifications.newPM !== false) {
|
||||
sendTxnEmail(receiver, 'new-pm', [
|
||||
{name: 'SENDER', content: getUserInfo(sender, ['name']).name},
|
||||
]);
|
||||
}
|
||||
|
||||
if (receiver.preferences.pushNotifications.newPM !== false) {
|
||||
sendPushNotification(
|
||||
receiver,
|
||||
@@ -510,7 +510,7 @@ api.sendPrivateMessage = {
|
||||
);
|
||||
}
|
||||
|
||||
res.respond(200, {});
|
||||
res.respond(200, { message: newMessage });
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { authWithHeaders } from '../../middlewares/auth';
|
||||
let api = {};
|
||||
|
||||
// @TODO export this const, cannot export it from here because only routes are exported from controllers
|
||||
const LAST_ANNOUNCEMENT_TITLE = 'HABITICA COMIC-CON MEETUP AND WIKI SPOTLIGHT ON THE POMODORO TECHNIQUE';
|
||||
const LAST_ANNOUNCEMENT_TITLE = 'AUGUST SUBSCRIBER ITEMS AND WIKI SPOTLIGHT ON CUSTOMIZING THE HABITICA EXPERIENCE';
|
||||
const worldDmg = { // @TODO
|
||||
bailey: false,
|
||||
};
|
||||
@@ -30,23 +30,24 @@ api.getNews = {
|
||||
<div class="mr-3 ${baileyClass}"></div>
|
||||
<div class="media-body">
|
||||
<h1 class="align-self-center">${res.t('newStuff')}</h1>
|
||||
<h2>7/19/2018 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
|
||||
<h2>8/23/2018 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="media align-items-center">
|
||||
<div class="media-body">
|
||||
<h3>Habitica at San Diego Comic Con!</h3>
|
||||
<p>Beffymaroo will be representing Habitica at San Diego Comic Con this year. If you’d like to meet her, along with other fellow Habiticans, join us at the Habitica SDCC Meetup! Beffymaroo will be handing out Habitica stickers, promo codes for the Unconventional Armor set, and other exciting special swag (quantities limited!).</p>
|
||||
<p>You can find the meetup on Saturday, July 21, at the San Diego Bayfront Hilton lobby from 12:00-1:00 PM! Look for the purple Gryphon banner. Can’t wait to meet you :)</p>
|
||||
<h3>August Subscriber Set Revealed!</h3>
|
||||
<p>Subscriber Items for August have been revealed: the Lava Dragon Item Set! You only have until August 31 to receive the item set when you <a href='/user/settings/subscription' target='_blank'>subscribe</a>. If you're already an active subscriber, reload the site and then head to Inventory > Items to claim your gear!</p>
|
||||
</div>
|
||||
<div class="promo_unconventional_armor ml-3 mb-3"></div>
|
||||
<div class="promo_mystery_201808 ml-3"></div>
|
||||
</div>
|
||||
<p>Subscribers also receive the ability to buy Gems for Gold -- the longer you subscribe, the more Gems you can buy per month! There are other perks as well, such as longer access to uncompressed data and a cute Jackalope pet. Best of all, subscriptions let us keep Habitica running. Thank you very much for your support -- it means a lot to us.</p>
|
||||
<div class="small mb-3">by Beffymaroo</div>
|
||||
<div class="media align-items-center">
|
||||
<div class="scene_pomodoro mr-3"></div>
|
||||
<div class="scene_casting_spells mr-3 mb-3"></div>
|
||||
<div class="media-body">
|
||||
<h3>Wiki Spotlight: The Pomodoro Technique</h3>
|
||||
<p>This month's <a href='https://habitica.wordpress.com/2018/07/18/pomodoro/' target='_blank'>featured Wiki article</a> is about the Pomodoro Technique! We hope that it will help you as you look for new productivity strategies. Be sure to check it out, and let us know what you think by reaching out on <a href='https://twitter.com/habitica' target='_blank'>Twitter</a>, <a href='http://blog.habitrpg.com' target='_blank'>Tumblr</a>, and <a href='https://facebook.com/habitica' target='_blank'>Facebook</a>.</p>
|
||||
<h3>Blog Post: Creating a Unique Experience</h3>
|
||||
<p>This month's <a href='https://habitica.wordpress.com/2018/08/22/creating-a-unique-experience/' target='_blank'>featured Wiki article</a> is about using Habitica's features to create a unique experience! We hope that it will help you as you customize Habitica to make the app even more motivating and fun. Be sure to check it out, and let us know what you think by reaching out on <a href='https://twitter.com/habitica' target='_blank'>Twitter</a>, <a href='http://blog.habitrpg.com' target='_blank'>Tumblr</a>, and <a href='https://facebook.com/habitica' target='_blank'>Facebook</a>.</p>
|
||||
<div class="small mb-3">by shanaqui and the Wiki Wizards</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
} from '../../libs/errors';
|
||||
import { model as PushDevice } from '../../models/pushDevice';
|
||||
|
||||
let api = {};
|
||||
|
||||
@@ -25,17 +26,17 @@ api.addPushDevice = {
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
const user = res.locals.user;
|
||||
|
||||
req.checkBody('regId', res.t('regIdRequired')).notEmpty();
|
||||
req.checkBody('type', res.t('typeRequired')).notEmpty().isIn(['ios', 'android']);
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let pushDevices = user.pushDevices;
|
||||
const pushDevices = user.pushDevices;
|
||||
|
||||
let item = {
|
||||
const item = {
|
||||
regId: req.body.regId,
|
||||
type: req.body.type,
|
||||
};
|
||||
@@ -44,9 +45,14 @@ api.addPushDevice = {
|
||||
throw new NotAuthorized(res.t('pushDeviceAlreadyAdded'));
|
||||
}
|
||||
|
||||
pushDevices.push(item);
|
||||
// Concurrency safe update
|
||||
const pushDevice = (new PushDevice(item)).toJSON(); // Create a mongo doc
|
||||
await user.update({
|
||||
$push: { pushDevices: pushDevice },
|
||||
}).exec();
|
||||
|
||||
await user.save();
|
||||
// Update the response
|
||||
user.pushDevices.push(pushDevice);
|
||||
|
||||
res.respond(200, user.pushDevices, res.t('pushDeviceAdded'));
|
||||
},
|
||||
@@ -70,16 +76,18 @@ api.removePushDevice = {
|
||||
userFieldsToExclude: ['inbox'],
|
||||
})],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
const user = res.locals.user;
|
||||
|
||||
req.checkParams('regId', res.t('regIdRequired')).notEmpty();
|
||||
let validationErrors = req.validationErrors();
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
let regId = req.params.regId;
|
||||
|
||||
let pushDevices = user.pushDevices;
|
||||
const regId = req.params.regId;
|
||||
|
||||
let indexOfPushDevice = pushDevices.findIndex((element) => {
|
||||
const pushDevices = user.pushDevices;
|
||||
|
||||
const indexOfPushDevice = pushDevices.findIndex((element) => {
|
||||
return element.regId === regId;
|
||||
});
|
||||
|
||||
@@ -87,8 +95,12 @@ api.removePushDevice = {
|
||||
throw new NotFound(res.t('pushDeviceNotFound'));
|
||||
}
|
||||
|
||||
// Concurrency safe update
|
||||
const pullQuery = { $pull: { pushDevices: { $elemMatch: { regId } } } };
|
||||
await user.update(pullQuery).exec();
|
||||
|
||||
// Update the response
|
||||
pushDevices.splice(indexOfPushDevice, 1);
|
||||
await user.save();
|
||||
|
||||
res.respond(200, user.pushDevices, res.t('pushDeviceRemoved'));
|
||||
},
|
||||
|
||||
@@ -175,6 +175,7 @@ api.createUserTasks = {
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
taskType: task.type,
|
||||
headers: req.headers,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -702,6 +703,7 @@ api.scoreTask = {
|
||||
category: 'behavior',
|
||||
taskType: task.type,
|
||||
direction,
|
||||
headers: req.headers,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -147,7 +147,7 @@ api.getBuyList = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /api/v3/user/in-app-rewards Get the in app items appaearing in the user's reward column
|
||||
* @api {get} /api/v3/user/in-app-rewards Get the in app items appearing in the user's reward column
|
||||
* @apiName UserGetInAppRewards
|
||||
* @apiGroup User
|
||||
*
|
||||
@@ -1445,8 +1445,8 @@ api.userSell = {
|
||||
* @apiParam (Query) {String} path Full path to unlock. See "content" API call for list of items.
|
||||
*
|
||||
* @apiParamExample {curl}
|
||||
* curl -x POST http://habitica.com/api/v3/user/unlock?path=background.midnight_clouds
|
||||
* curl -x POST http://habitica.com/api/v3/user/unlock?path=hair.color.midnight
|
||||
* curl -X POST http://habitica.com/api/v3/user/unlock?path=background.midnight_clouds
|
||||
* curl -X POST http://habitica.com/api/v3/user/unlock?path=hair.color.midnight
|
||||
*
|
||||
* @apiSuccess {Object} data.purchased
|
||||
* @apiSuccess {Object} data.items
|
||||
|
||||
@@ -5,24 +5,24 @@ import { authWithHeaders } from '../../../middlewares/auth';
|
||||
let api = {};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/allocate Allocate a single attribute point
|
||||
* @api {post} /api/v3/user/allocate Allocate a single Stat Point (previously called Attribute Point)
|
||||
* @apiName UserAllocate
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiParam (Body) {String="str","con","int","per"} stat Query parameter - Default ='str'
|
||||
* @apiParam (Query) {String="str","con","int","per"} stat The Stat to increase. Default is 'str'
|
||||
*
|
||||
* @apiParamExample {json} Example request
|
||||
* {"stat":"int"}
|
||||
* @apiParamExample {curl}
|
||||
* curl -X POST -d "" https://habitica.com/api/v3/user/allocate?stat=int
|
||||
*
|
||||
* @apiSuccess {Object} data Returns stats from the user profile
|
||||
* @apiSuccess {Object} data Returns stats and notifications from the user profile
|
||||
*
|
||||
* @apiError {NotAuthorized} NoPoints Not enough attribute points to increment a stat.
|
||||
* @apiError {NotAuthorized} NoPoints You don't have enough Stat Points.
|
||||
*
|
||||
* @apiErrorExample {json}
|
||||
* {
|
||||
* "success": false,
|
||||
* "error": "NotAuthorized",
|
||||
* "message": "You don't have enough attribute points."
|
||||
* "message": "You don't have enough Stat Points."
|
||||
* }
|
||||
*/
|
||||
api.allocate = {
|
||||
@@ -40,7 +40,7 @@ api.allocate = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/allocate-bulk Allocate multiple attribute points
|
||||
* @api {post} /api/v3/user/allocate-bulk Allocate multiple Stat Points
|
||||
* @apiName UserAllocateBulk
|
||||
* @apiGroup User
|
||||
*
|
||||
@@ -49,22 +49,22 @@ api.allocate = {
|
||||
* @apiParamExample {json} Example request
|
||||
* {
|
||||
* stats: {
|
||||
* 'int': int,
|
||||
* 'str': int,
|
||||
* 'con': int,
|
||||
* 'per': int,
|
||||
* },
|
||||
* "int": int,
|
||||
* "str": str,
|
||||
* "con": con,
|
||||
* "per": per
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Object} data Returns stats from the user profile
|
||||
* @apiSuccess {Object} data Returns stats and notifications from the user profile
|
||||
*
|
||||
* @apiError {NotAuthorized} NoPoints Not enough attribute points to increment a stat.
|
||||
* @apiError {NotAuthorized} NoPoints You don't have enough Stat Points.
|
||||
*
|
||||
* @apiErrorExample {json}
|
||||
* {
|
||||
* "success": false,
|
||||
* "error": "NotAuthorized",
|
||||
* "message": "You don't have enough attribute points."
|
||||
* "message": "You don't have enough Stat Points."
|
||||
* }
|
||||
*/
|
||||
api.allocateBulk = {
|
||||
@@ -82,7 +82,7 @@ api.allocateBulk = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/allocate-now Allocate all attribute points
|
||||
* @api {post} /api/v3/user/allocate-now Allocate all Stat Points
|
||||
* @apiDescription Uses the user's chosen automatic allocation method, or if none, assigns all to STR. Note: will return success, even if there are 0 points to allocate.
|
||||
* @apiName UserAllocateNow
|
||||
* @apiGroup User
|
||||
@@ -119,7 +119,8 @@ api.allocateBulk = {
|
||||
* "per": 0,
|
||||
* "str": 0,
|
||||
* "con": 0
|
||||
* }
|
||||
* },
|
||||
* "notifications": [ .... ],
|
||||
* }
|
||||
* }
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user