Merge branch 'develop' of https://github.com/HabitRPG/habitrpg into merchant-button-fix
@@ -34,6 +34,18 @@ function($scope, $rootScope, User, $http, Notification, ApiUrl) {
|
||||
// Twitter
|
||||
$.getScript('https://platform.twitter.com/widgets.js');
|
||||
|
||||
// Facebook
|
||||
(function(d, s, id) {
|
||||
var js, fjs = d.getElementsByTagName(s)[0];
|
||||
if (d.getElementById(id)) return;
|
||||
js = d.createElement(s); js.id = id;
|
||||
js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.5";
|
||||
fjs.parentNode.insertBefore(js, fjs);
|
||||
}(document, 'script', 'facebook-jssdk'));
|
||||
|
||||
// Tumblr
|
||||
$.getScript('https://assets.tumblr.com/share-button.js');
|
||||
|
||||
/* Google Content Experiments
|
||||
if (window.env.NODE_ENV === 'production') {
|
||||
$.getScript('//www.google-analytics.com/cx/api.js?experiment=boVO4eEyRfysNE5D53nCMQ', function(){
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
// Make user and settings available for everyone through root scope.
|
||||
habitrpg.controller('SettingsCtrl',
|
||||
['$scope', 'User', '$rootScope', '$http', 'ApiUrl', 'Guide', '$location', '$timeout', 'Notification', 'Shared',
|
||||
function($scope, User, $rootScope, $http, ApiUrl, Guide, $location, $timeout, Notification, Shared) {
|
||||
['$scope', 'User', '$rootScope', '$http', 'ApiUrl', 'Guide', '$location', '$timeout', 'Content', 'Notification', 'Shared',
|
||||
function($scope, User, $rootScope, $http, ApiUrl, Guide, $location, $timeout, Content, Notification, Shared) {
|
||||
|
||||
// FIXME we have this re-declared everywhere, figure which is the canonical version and delete the rest
|
||||
// $scope.auth = function (id, token) {
|
||||
@@ -150,6 +150,7 @@ habitrpg.controller('SettingsCtrl',
|
||||
Notification.text(env.t('promoCodeApplied'));
|
||||
});
|
||||
}
|
||||
|
||||
$scope.generateCodes = function(codes){
|
||||
$http.post(ApiUrl.get() + '/api/v2/coupons/generate/'+codes.event+'?count='+(codes.count || 1))
|
||||
.success(function(res,code){
|
||||
@@ -158,6 +159,7 @@ habitrpg.controller('SettingsCtrl',
|
||||
window.location.href = '/api/v2/coupons?limit='+codes.count+'&_id='+User.user._id+'&apiToken='+User.user.apiToken;
|
||||
})
|
||||
}
|
||||
|
||||
$scope.releasePets = function() {
|
||||
User.user.ops.releasePets({});
|
||||
$rootScope.$state.go('tasks');
|
||||
@@ -194,12 +196,30 @@ habitrpg.controller('SettingsCtrl',
|
||||
$http.get(ApiUrl.get() + '/api/v2/coupons/valid-discount/'+coupon)
|
||||
.success(function(){
|
||||
Notification.text("Coupon applied!");
|
||||
var subs = $scope.Content.subscriptionBlocks;
|
||||
var subs = Content.subscriptionBlocks;
|
||||
subs["basic_6mo"].discount = true;
|
||||
subs["google_6mo"].discount = false;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.gemGoldCap = function(subscription) {
|
||||
var baseCap = 25;
|
||||
var gemCapExtra = User.user.purchased.plan.consecutive.gemCapExtra;
|
||||
// @TODO: What are these magic numbers? 3? 5?
|
||||
var blocks = Content.subscriptionBlocks[subscription.key].months / 3 * 5;
|
||||
var flooredBlocks = Math.floor(blocks);
|
||||
|
||||
var userTotalDropCap = baseCap + gemCapExtra + flooredBlocks;
|
||||
var maxDropCap = 50;
|
||||
|
||||
return [userTotalDropCap, maxDropCap];
|
||||
};
|
||||
|
||||
$scope.numberOfMysticHourglasses = function(subscription) {
|
||||
var numberOfHourglasses = Content.subscriptionBlocks[subscription.key].months / 3;
|
||||
return Math.floor(numberOfHourglasses);
|
||||
};
|
||||
|
||||
function _calculateNextCron() {
|
||||
$scope.dayStart;
|
||||
|
||||
|
||||
@@ -108,13 +108,14 @@ angular.module("habitrpg").factory("Notification",
|
||||
var stack_topright = {"dir1": "down", "dir2": "left", "spacing1": 15, "spacing2": 15, "firstpos1": 60};
|
||||
|
||||
function _notify(html, type, icon) {
|
||||
console.log(type)
|
||||
var notice = $.pnotify({
|
||||
type: type || 'warning', //('info', 'text', 'warning', 'success', 'gp', 'xp', 'hp', 'lvl', 'death', 'mp', 'crit')
|
||||
text: html,
|
||||
opacity: 1,
|
||||
addclass: 'alert-' + type,
|
||||
delay: 7000,
|
||||
hide: (type == 'error') ? false : true,
|
||||
hide: (type == 'error' || type == 'danger') ? false : true,
|
||||
mouse_reset: false,
|
||||
width: "250px",
|
||||
stack: stack_topright,
|
||||
|
||||
|
After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 495 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 56 KiB |
@@ -31,7 +31,9 @@ function track(eventType, data) {
|
||||
function _sendDataToAmplitude(eventType, data) {
|
||||
var amplitudeData = _formatDataForAmplitude(data);
|
||||
amplitudeData.event_type = eventType;
|
||||
amplitude.track(amplitudeData);
|
||||
amplitude.track(amplitudeData).catch(function(error) {
|
||||
// @TODO log error with new relic
|
||||
});
|
||||
}
|
||||
|
||||
function _sendDataToGoogle(eventType, data) {
|
||||
@@ -87,7 +89,9 @@ function _sendPurchaseDataToAmplitude(data) {
|
||||
amplitudeData.event_type = 'purchase';
|
||||
amplitudeData.revenue = data.purchaseValue;
|
||||
|
||||
amplitude.track(amplitudeData)
|
||||
amplitude.track(amplitudeData).catch(function(error) {
|
||||
// @TODO log error with new relic
|
||||
});
|
||||
}
|
||||
|
||||
function _formatDataForAmplitude(data) {
|
||||
@@ -95,7 +99,7 @@ function _formatDataForAmplitude(data) {
|
||||
var event_properties = _.omit(data, PROPERTIES_TO_SCRUB);
|
||||
|
||||
var ampData = {
|
||||
user_id: data.uuid,
|
||||
user_id: data.uuid || 'no-user-id-was-provided',
|
||||
platform: 'server',
|
||||
event_properties: event_properties
|
||||
}
|
||||
|
||||
@@ -25,10 +25,6 @@ var accountSuspended = function(uuid){
|
||||
code: 'ACCOUNT_SUSPENDED'
|
||||
};
|
||||
}
|
||||
// Allow case-insensitive regex searching for Mongo queries. See http://stackoverflow.com/a/3561711/362790
|
||||
var RegexEscape = function(s){
|
||||
return new RegExp('^' + s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$', 'i');
|
||||
}
|
||||
|
||||
api.auth = function(req, res, next) {
|
||||
var uid = req.headers['x-api-user'];
|
||||
@@ -67,35 +63,42 @@ api.authWithUrl = function(req, res, next) {
|
||||
}
|
||||
|
||||
api.registerUser = function(req, res, next) {
|
||||
var regEmail = RegexEscape(req.body.email),
|
||||
regUname = RegexEscape(req.body.username);
|
||||
var email = req.body.email && req.body.email.toLowerCase();
|
||||
var username = req.body.username;
|
||||
// Get the lowercase version of username to check that we do not have duplicates
|
||||
// So we can search for it in the database and then reject the choosen username if 1 or more results are found
|
||||
var lowerCaseUsername = username && username.toLowerCase();
|
||||
|
||||
async.auto({
|
||||
validate: function(cb) {
|
||||
if (!(req.body.username && req.body.password && req.body.email))
|
||||
if (!(username && req.body.password && email))
|
||||
return cb({code:401, err: ":username, :email, :password, :confirmPassword required"});
|
||||
if (req.body.password !== req.body.confirmPassword)
|
||||
return cb({code:401, err: ":password and :confirmPassword don't match"});
|
||||
if (!validator.isEmail(req.body.email))
|
||||
if (!validator.isEmail(email))
|
||||
return cb({code:401, err: ":email invalid"});
|
||||
cb();
|
||||
},
|
||||
findReg: function(cb) {
|
||||
User.findOne({$or:[{'auth.local.email': regEmail}, {'auth.local.username': regUname}]}, {'auth.local':1}, cb);
|
||||
// Search for duplicates using lowercase version of username
|
||||
User.findOne({$or:[{'auth.local.email': email}, {'auth.local.lowerCaseUsername': lowerCaseUsername}]}, {'auth.local':1}, cb);
|
||||
},
|
||||
findFacebook: function(cb){
|
||||
User.findOne({_id: req.headers['x-api-user'], apiToken: req.headers['x-api-key']}, {auth:1}, cb);
|
||||
},
|
||||
register: ['validate', 'findReg', 'findFacebook', function(cb, data) {
|
||||
if (data.findReg) {
|
||||
if (regEmail.test(data.findReg.auth.local.email)) return cb({code:401, err:"Email already taken"});
|
||||
if (regUname.test(data.findReg.auth.local.username)) return cb({code:401, err:"Username already taken"});
|
||||
if (email === data.findReg.auth.local.email) return cb({code:401, err:"Email already taken"});
|
||||
// Check that the lowercase username isn't already used
|
||||
if (lowerCaseUsername === data.findReg.auth.local.lowerCaseUsername) return cb({code:401, err:"Username already taken"});
|
||||
}
|
||||
var salt = utils.makeSalt();
|
||||
var newUser = {
|
||||
auth: {
|
||||
local: {
|
||||
username: req.body.username,
|
||||
email: req.body.email,
|
||||
username: username,
|
||||
lowerCaseUsername: lowerCaseUsername, // Store the lowercase version of the username
|
||||
email: email, // Store email as lowercase
|
||||
salt: salt,
|
||||
hashed_password: utils.encryptPassword(req.body.password, salt)
|
||||
},
|
||||
@@ -115,12 +118,14 @@ api.registerUser = function(req, res, next) {
|
||||
var analyticsData = {
|
||||
category: 'acquisition',
|
||||
type: 'local',
|
||||
gaLabel: 'local'
|
||||
gaLabel: 'local',
|
||||
uuid: user._id,
|
||||
};
|
||||
analytics.track('register', analyticsData)
|
||||
|
||||
user.save(function(err, savedUser){
|
||||
// Clean previous email preferences
|
||||
// TODO when emails added to EmailUnsubcription they should use lowercase version
|
||||
EmailUnsubscription.remove({email: savedUser.auth.local.email}, function(){
|
||||
utils.txnEmail(savedUser, 'welcome');
|
||||
});
|
||||
@@ -143,7 +148,10 @@ api.loginLocal = function(req, res, next) {
|
||||
var username = req.body.username;
|
||||
var password = req.body.password;
|
||||
if (!(username && password)) return res.json(401, {err:'Missing :username or :password in request body, please provide both'});
|
||||
var login = validator.isEmail(username) ? {'auth.local.email':username} : {'auth.local.username':username};
|
||||
var login = validator.isEmail(username) ?
|
||||
{'auth.local.email':username.toLowerCase()} : // Emails are all lowercase
|
||||
{'auth.local.username':username}; // Use the username as the user typed it
|
||||
|
||||
User.findOne(login, {auth:1}, function(err, user){
|
||||
if (err) return next(err);
|
||||
if (!user) return res.json(401, {err:"Uh-oh - your username or password is incorrect.\n- Make sure your username or email is typed correctly.\n- You may have signed up with Facebook, not email. Double-check by trying Facebook login.\n- If you forgot your password, click \"Forgot Password\"."});
|
||||
@@ -205,7 +213,8 @@ api.loginSocial = function(req, res, next) {
|
||||
var analyticsData = {
|
||||
category: 'acquisition',
|
||||
type: network,
|
||||
gaLabel: network
|
||||
gaLabel: network,
|
||||
uuid: user._id,
|
||||
};
|
||||
analytics.track('register', analyticsData)
|
||||
}]
|
||||
@@ -233,12 +242,14 @@ api.deleteSocial = function(req,res,next){
|
||||
}
|
||||
|
||||
api.resetPassword = function(req, res, next){
|
||||
var email = req.body.email,
|
||||
var email = req.body.email && req.body.email.toLowerCase(), // Emails are all lowercase
|
||||
salt = utils.makeSalt(),
|
||||
newPassword = utils.makeSalt(), // use a salt as the new password too (they'll change it later)
|
||||
hashed_password = utils.encryptPassword(newPassword, salt);
|
||||
|
||||
User.findOne({'auth.local.email': RegexEscape(email)}, function(err, user){
|
||||
if(!email) return res.json(400, {err: "Email not provided"});
|
||||
|
||||
User.findOne({'auth.local.email': email}, function(err, user){
|
||||
if (err) return next(err);
|
||||
if (!user) return res.send(401, {err:"Sorry, we can't find a user registered with email " + email + "\n- Make sure your email address is typed correctly.\n- You may have signed up with Facebook, not email. Double-check by trying Facebook login."});
|
||||
user.auth.local.salt = salt;
|
||||
@@ -266,15 +277,22 @@ var invalidPassword = function(user, password){
|
||||
}
|
||||
|
||||
api.changeUsername = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
var username = req.body.username;
|
||||
var lowerCaseUsername = username && username.toLowerCase(); // we search for the lowercased version to intercept duplicates
|
||||
|
||||
if(!username) return res.json(400, {err: "Username not provided"});
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
User.findOne({'auth.local.username': RegexEscape(req.body.username)}, {auth:1}, cb);
|
||||
User.findOne({'auth.local.lowerCaseUsername': lowerCaseUsername}, {auth:1}, cb);
|
||||
},
|
||||
function(found, cb){
|
||||
if (found) return cb({code:401, err: "Username already taken"});
|
||||
if (invalidPassword(res.locals.user, req.body.password)) return cb(invalidPassword(res.locals.user, req.body.password));
|
||||
res.locals.user.auth.local.username = req.body.username;
|
||||
res.locals.user.save(cb);
|
||||
if (invalidPassword(user, req.body.password)) return cb(invalidPassword(user, req.body.password));
|
||||
user.auth.local.username = username;
|
||||
user.auth.local.lowerCaseUsername = lowerCaseUsername;
|
||||
|
||||
user.save(cb);
|
||||
}
|
||||
], function(err){
|
||||
if (err) return err.code ? res.json(err.code, err) : next(err);
|
||||
@@ -283,14 +301,17 @@ api.changeUsername = function(req, res, next) {
|
||||
}
|
||||
|
||||
api.changeEmail = function(req, res, next){
|
||||
var email = req.body.email && req.body.email.toLowerCase(); // emails are all lowercase
|
||||
if(!email) return res.json(400, {err: "Email not provided"});
|
||||
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
User.findOne({'auth.local.email': RegexEscape(req.body.email)}, {auth:1}, cb);
|
||||
User.findOne({'auth.local.email': email}, {auth:1}, cb);
|
||||
},
|
||||
function(found, cb){
|
||||
if(found) return cb({code:401, err: "Email already taken"});
|
||||
if (invalidPassword(res.locals.user, req.body.password)) return cb(invalidPassword(res.locals.user, req.body.password));
|
||||
res.locals.user.auth.local.email = req.body.email;
|
||||
res.locals.user.auth.local.email = email;
|
||||
res.locals.user.save(cb);
|
||||
}
|
||||
], function(err){
|
||||
@@ -333,7 +354,7 @@ api.getFirebaseToken = function(req, res, next) {
|
||||
.createToken({
|
||||
uid: user._id,
|
||||
isHabiticaUser: true
|
||||
}, {
|
||||
}, {
|
||||
expires: expires
|
||||
});
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ api.update = function(req, res, next){
|
||||
// before-save / after-save comparison to determine if we need to sync to users
|
||||
before = _before;
|
||||
var attrs = _.pick(req.body, 'name shortName description habits dailys todos rewards date'.split(' '));
|
||||
Challenge.findByIdAndUpdate(cid, {$set:attrs}, cb);
|
||||
Challenge.findByIdAndUpdate(cid, {$set:attrs}, {new: true}, cb);
|
||||
},
|
||||
function(saved, cb) {
|
||||
|
||||
@@ -271,7 +271,7 @@ function closeChal(cid, broken, cb) {
|
||||
function(_removed, cb2) {
|
||||
removed = _removed;
|
||||
var pull = {'$pull':{}}; pull['$pull'][_removed._id] = 1;
|
||||
Group.findByIdAndUpdate(_removed.group, pull);
|
||||
Group.findByIdAndUpdate(_removed.group, {new: true}, pull);
|
||||
User.find({_id:{$in: removed.members}}, cb2);
|
||||
},
|
||||
function(users, cb2) {
|
||||
@@ -297,7 +297,7 @@ function closeChal(cid, broken, cb) {
|
||||
/**
|
||||
* Delete & close
|
||||
*/
|
||||
api['delete'] = function(req, res, next){
|
||||
api.delete = function(req, res, next){
|
||||
var user = res.locals.user;
|
||||
var cid = req.params.cid;
|
||||
|
||||
@@ -370,7 +370,7 @@ api.join = function(req, res, next){
|
||||
|
||||
async.waterfall([
|
||||
function(cb) {
|
||||
Challenge.findByIdAndUpdate(cid, {$addToSet:{members:user._id}}, cb);
|
||||
Challenge.findByIdAndUpdate(cid, {$addToSet:{members:user._id}}, {new: true}, cb);
|
||||
},
|
||||
function(chal, cb) {
|
||||
|
||||
@@ -403,7 +403,7 @@ api.leave = function(req, res, next){
|
||||
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
Challenge.findByIdAndUpdate(cid, {$pull:{members:user._id}}, cb);
|
||||
Challenge.findByIdAndUpdate(cid, {$pull:{members:user._id}}, {new: true}, cb);
|
||||
},
|
||||
function(chal, cb){
|
||||
|
||||
|
||||
@@ -521,6 +521,7 @@ api.leave = function(req, res, next) {
|
||||
group.leave(user, keep, function(err){
|
||||
if (err) return next(err);
|
||||
user = group = keep = null;
|
||||
|
||||
return res.send(204);
|
||||
});
|
||||
};
|
||||
@@ -683,7 +684,7 @@ api.invite = function(req, res, next){
|
||||
} else if (req.body.emails) {
|
||||
inviteByEmails(req.body.emails, group, req, res, next)
|
||||
} else {
|
||||
return res.json(400,{err: "Can invite only by email or uuid"});
|
||||
return res.json(400, {err: "Can only invite by email or uuid"});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -912,7 +913,8 @@ api.questAccept = function(req, res, next) {
|
||||
owner: true,
|
||||
response: 'accept',
|
||||
gaLabel: 'accept',
|
||||
questName: key
|
||||
questName: key,
|
||||
uuid: user._id,
|
||||
};
|
||||
analytics.track('quest',analyticsData);
|
||||
group.quest.members[m] = true;
|
||||
@@ -958,7 +960,8 @@ api.questAccept = function(req, res, next) {
|
||||
owner: false,
|
||||
response: 'accept',
|
||||
gaLabel: 'accept',
|
||||
questName: group.quest.key
|
||||
questName: group.quest.key,
|
||||
uuid: user._id,
|
||||
};
|
||||
analytics.track('quest',analyticsData);
|
||||
group.quest.members[user._id] = true;
|
||||
@@ -977,7 +980,8 @@ api.questReject = function(req, res, next) {
|
||||
owner: false,
|
||||
response: 'reject',
|
||||
gaLabel: 'reject',
|
||||
questName: group.quest.key
|
||||
questName: group.quest.key,
|
||||
uuid: user._id,
|
||||
};
|
||||
analytics.track('quest',analyticsData);
|
||||
group.quest.members[user._id] = false;
|
||||
|
||||
@@ -101,9 +101,27 @@ exports.iosVerify = function(req, res, next) {
|
||||
if (iap.isValidated(appleRes)) {
|
||||
var purchaseDataList = iap.getPurchaseData(appleRes);
|
||||
if (purchaseDataList.length > 0) {
|
||||
if (purchaseDataList[0].productId === 'com.habitrpg.ios.Habitica.20gems') {
|
||||
//Correct receipt
|
||||
payments.buyGems({user:user, paymentMethod:'IAP AppleStore'});
|
||||
var correctReceipt = true;
|
||||
for (var index in purchaseDataList) {
|
||||
switch (purchaseDataList[index].productId) {
|
||||
case 'com.habitrpg.ios.Habitica.4gems':
|
||||
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 1});
|
||||
break;
|
||||
case 'com.habitrpg.ios.Habitica.8gems':
|
||||
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 2});
|
||||
break;
|
||||
case 'com.habitrpg.ios.Habitica.20gems':
|
||||
case 'com.habitrpg.ios.Habitica.21gems':
|
||||
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 5.25});
|
||||
break;
|
||||
case 'com.habitrpg.ios.Habitica.42gems':
|
||||
payments.buyGems({user:user, paymentMethod:'IAP AppleStore', amount: 10.5});
|
||||
break;
|
||||
default:
|
||||
correctReceipt = false;
|
||||
}
|
||||
}
|
||||
if (correctReceipt) {
|
||||
var resObj = {
|
||||
ok: true,
|
||||
data: appleRes
|
||||
|
||||
@@ -137,7 +137,8 @@ exports.cancelSubscription = function(data, cb) {
|
||||
}
|
||||
|
||||
exports.buyGems = function(data, cb) {
|
||||
var amt = data.gift ? data.gift.gems.amount/4 : 5;
|
||||
var amt = data.amount || 5;
|
||||
amt = data.gift ? data.gift.gems.amount/4 : amt;
|
||||
(data.gift ? data.gift.member : data.user).balance += amt;
|
||||
data.user.purchased.txnCount++;
|
||||
if(isProduction) {
|
||||
|
||||
@@ -13,14 +13,14 @@ var gcm = gcmApiKey ? pushNotify.gcm({
|
||||
|
||||
if(gcm){
|
||||
gcm.on('transmitted', function (result, message, registrationId) {
|
||||
console.info("transmitted", result, message, registrationId);
|
||||
//console.info("transmitted", result, message, registrationId);
|
||||
});
|
||||
|
||||
gcm.on('transmissionError', function (error, message, registrationId) {
|
||||
console.info("transmissionError", error, message, registrationId);
|
||||
//console.info("transmissionError", error, message, registrationId);
|
||||
});
|
||||
gcm.on('updated', function (result, registrationId) {
|
||||
console.info("updated", result, registrationId);
|
||||
//console.info("updated", result, registrationId);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ api.unsubscribe = function(req, res, next){
|
||||
if(data._id){
|
||||
User.update({_id: data._id}, {
|
||||
$set: {'preferences.emailNotifications.unsubscribeFromAll': true}
|
||||
}, {multi: false}, function(err, nAffected){
|
||||
}, {multi: false}, function(err, updateRes){
|
||||
if(err) return next(err);
|
||||
if(nAffected !== 1) return res.json(404, {err: 'User not found'});
|
||||
if(updateRes.n !== 1) return res.json(404, {err: 'User not found'});
|
||||
|
||||
res.send('<h1>' + i18n.t('unsubscribedSuccessfully', null, req.language) + '</h1>' + i18n.t('unsubscribedTextUsers', null, req.language));
|
||||
});
|
||||
|
||||
@@ -298,10 +298,9 @@ acceptablePUTPaths = _.reduce(require('./../models/user').schema.paths, function
|
||||
return m;
|
||||
}, {})
|
||||
|
||||
//// Uncomment this if we we want to disable GP-restoring (eg, holiday events)
|
||||
//_.each('stats.gp'.split(' '), function(removePath){
|
||||
// delete acceptablePUTPaths[removePath];
|
||||
//})
|
||||
_.each('stats.class'.split(' '), function(removePath){
|
||||
delete acceptablePUTPaths[removePath];
|
||||
})
|
||||
|
||||
/**
|
||||
* Update user
|
||||
@@ -318,7 +317,7 @@ api.update = function(req, res, next) {
|
||||
if (acceptablePUTPaths[k])
|
||||
user.fns.dotSet(k, v);
|
||||
else
|
||||
errors.push("path `" + k + "` was not saved, as it's a protected path. See https://github.com/HabitRPG/habitrpg/blob/develop/API.md for PUT /api/v2/user.");
|
||||
errors.push("path `" + k + "` was not saved, as it's a protected path.");
|
||||
return true;
|
||||
});
|
||||
user.save(function(err) {
|
||||
@@ -365,7 +364,7 @@ api.cron = function(req, res, next) {
|
||||
// api.reroll // Shared.ops
|
||||
// api.reset // Shared.ops
|
||||
|
||||
api['delete'] = function(req, res, next) {
|
||||
api.delete = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
var plan = user.purchased.plan;
|
||||
|
||||
@@ -397,38 +396,32 @@ api['delete'] = function(req, res, next) {
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Gems
|
||||
Development Only Operations
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
if (nconf.get('NODE_ENV') === 'development') {
|
||||
|
||||
// api.unlock // see Shared.ops
|
||||
api.addTenGems = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
|
||||
api.addTenGems = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
user.balance += 2.5;
|
||||
user.save(function(err){
|
||||
if (err) return next(err);
|
||||
res.send(204);
|
||||
})
|
||||
}
|
||||
user.balance += 2.5;
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Hourglass
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
user.save(function(err){
|
||||
if (err) return next(err);
|
||||
res.send(204);
|
||||
});
|
||||
};
|
||||
|
||||
// api.unlock // see Shared.ops
|
||||
api.addHourglass = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
|
||||
api.addHourglass = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
user.purchased.plan.consecutive.trinkets += 1;
|
||||
|
||||
user.purchased.plan.consecutive.trinkets += 1;
|
||||
|
||||
user.save(function(err){
|
||||
if (err) return next(err);
|
||||
res.send(204);
|
||||
})
|
||||
user.save(function(err){
|
||||
if (err) return next(err);
|
||||
res.send(204);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -607,6 +600,7 @@ api.batchUpdate = function(req, res, next) {
|
||||
return cb(code+": "+ (data.message ? data.message : data.err ? data.err : JSON.stringify(data)));
|
||||
return cb();
|
||||
};
|
||||
if(!api[_req.op]) { return cb(_req.op + ' operation not found'); }
|
||||
api[_req.op](_req, res, cb);
|
||||
});
|
||||
})
|
||||
@@ -635,9 +629,7 @@ api.batchUpdate = function(req, res, next) {
|
||||
// Fetch full user object
|
||||
} else if (response.wasModified){
|
||||
// Preen 3-day past-completed To-Dos from Angular & mobile app
|
||||
response.todos = _.where(response.todos, function(t) {
|
||||
return !t.completed || (t.challenge && t.challenge.id) || moment(t.dateCompleted).isAfter(moment().subtract({days:3}));
|
||||
});
|
||||
response.todos = shared.preenTodos(response.todos);
|
||||
res.json(200, response);
|
||||
|
||||
// return only the version number
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
var fs = require('fs'),
|
||||
path = require('path'),
|
||||
_ = require('lodash'),
|
||||
User = require('./models/user').model,
|
||||
shared = require('../../common'),
|
||||
translations = {};
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var _ = require('lodash');
|
||||
var accepts = require('accepts');
|
||||
var User = require('./models/user').model;
|
||||
var shared = require('../../common');
|
||||
var translations = {};
|
||||
|
||||
var localePath = path.join(__dirname, "/../../common/locales/")
|
||||
|
||||
@@ -74,7 +75,9 @@ var chineseVersions = ['zh-tw'];
|
||||
|
||||
var getUserLanguage = function(req, res, next){
|
||||
var getFromBrowser = function(){
|
||||
var acceptable = _(req.acceptedLanguages).map(function(lang){
|
||||
var acceptedLanguages = accepts(req).languages();
|
||||
|
||||
var acceptable = _(acceptedLanguages).map(function(lang){
|
||||
return lang.slice(0, 2);
|
||||
}).uniq().value();
|
||||
|
||||
@@ -83,7 +86,7 @@ var getUserLanguage = function(req, res, next){
|
||||
var iAcceptedCompleteLang = (matches.length > 0) ? multipleVersionsLanguages.indexOf(matches[0].toLowerCase()) : -1;
|
||||
|
||||
if(iAcceptedCompleteLang !== -1){
|
||||
var acceptedCompleteLang = _.find(req.acceptedLanguages, function(accepted){
|
||||
var acceptedCompleteLang = _.find(acceptedLanguages, function(accepted){
|
||||
return accepted.slice(0, 2) == multipleVersionsLanguages[iAcceptedCompleteLang];
|
||||
});
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@ module.exports = function(server,mongoose) {
|
||||
useAvg = false, // use average over 3 minutes, or simply the last minute's report
|
||||
url = 'https://api.newrelic.com/v2/applications/'+nconf.get('NEW_RELIC_APPLICATION_ID')+'/metrics/data.json?names[]=Apdex&values[]=score';
|
||||
setInterval(function(){
|
||||
// TODO, DISABLED UNTIL NEW RELIC IS ADDED AGAIN
|
||||
// see https://docs.newrelic.com/docs/apm/apis/api-v2-examples/average-response-time-examples-api-v2, https://rpm.newrelic.com/api/explore/applications/data
|
||||
request({
|
||||
/*request({
|
||||
url: useAvg ? url+'&from='+moment().subtract({minutes:mins}).utc().format()+'&to='+moment().utc().format()+'&summarize=true' : url,
|
||||
headers: {'X-Api-Key': nconf.get('NEW_RELIC_API_KEY')}
|
||||
}, function(err, response, body){
|
||||
@@ -22,8 +23,12 @@ module.exports = function(server,mongoose) {
|
||||
apdexBad = score < .75 || score == 1,
|
||||
memory = os.freemem() / os.totalmem(),
|
||||
memoryHigh = memory < 0.1;
|
||||
if (/*apdexBad || */memoryHigh) throw '[Memory Leak] Apdex='+score+' Memory='+parseFloat(memory).toFixed(3)+' Time='+moment().format();
|
||||
});
|
||||
if (apdexBad || memoryHigh) throw '[Memory Leak] Apdex='+score+' Memory='+parseFloat(memory).toFixed(3)+' Time='+moment().format();
|
||||
|
||||
});*/
|
||||
var memory = os.freemem() / os.totalmem(),
|
||||
memoryHigh = memory < 0.1;
|
||||
if (memoryHigh) throw '[Memory Leak] Memory='+parseFloat(memory).toFixed(3)+' Time='+moment().format();
|
||||
}, mins*60*1000);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ var i18n = require('../i18n.js');
|
||||
var buildManifest = require('../libs/buildManifest');
|
||||
var shared = require('../../../common');
|
||||
var forceRefresh = require('./forceRefresh');
|
||||
var tavern = require('../models/group').tavern;
|
||||
var tavernQuest = require('../models/group').tavernQuest;
|
||||
var mods = require('../models/user').mods;
|
||||
|
||||
// To avoid stringifying more data then we need,
|
||||
// items from `env` used on the client will have to be specified in this array
|
||||
var clientVars = ['language', 'isStaticPage', 'avalaibleLanguages', 'translations',
|
||||
'FACEBOOK_KEY', 'NODE_ENV', 'BASE_URL', 'GA_ID',
|
||||
'AMAZON_PAYMENTS', 'STRIPE_PUB_KEY', 'AMPLITUDE_KEY',
|
||||
'AMAZON_PAYMENTS', 'STRIPE_PUB_KEY', 'AMPLITUDE_KEY',
|
||||
'worldDmg', 'mods', 'IS_MOBILE'];
|
||||
|
||||
var env = {
|
||||
@@ -21,7 +21,6 @@ var env = {
|
||||
getBuildUrl: buildManifest.getBuildUrl,
|
||||
_: _,
|
||||
clientVars: clientVars,
|
||||
tavern: tavern, // for world boss
|
||||
mods: mods,
|
||||
Content: shared.content,
|
||||
siteVersion: forceRefresh.siteVersion,
|
||||
@@ -53,9 +52,9 @@ module.exports = function(req, res, next) {
|
||||
args.push(language.code);
|
||||
return shared.i18n.t.apply(null, args);
|
||||
},
|
||||
// Defined here and not outside of the middleware because tavern might be an
|
||||
// Defined here and not outside of the middleware because tavernQuest might be an
|
||||
// empty object until the query to fetch it finishes
|
||||
worldDmg: (tavern && tavern.quest && tavern.quest.extra && tavern.quest.extra.worldDmg) || {},
|
||||
worldDmg: (tavernQuest && tavernQuest.extra && tavernQuest.extra.worldDmg) || {},
|
||||
});
|
||||
|
||||
// Put query-string party (& guild but use partyInvite for backward compatibility)
|
||||
@@ -68,4 +67,4 @@ module.exports = function(req, res, next) {
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -269,12 +269,15 @@ GroupSchema.statics.collectQuest = function(user, progress, cb) {
|
||||
}
|
||||
|
||||
// to set a boss: `db.groups.update({_id:'habitrpg'},{$set:{quest:{key:'dilatory',active:true,progress:{hp:1000,rage:1500}}}})`
|
||||
module.exports.tavern = {};
|
||||
module.exports.tavernQuest = {};
|
||||
var tavernQ = {_id:'habitrpg','quest.key':{$ne:null}};
|
||||
process.nextTick(function(){
|
||||
mongoose.model('Group').findOne(tavernQ,function(err,tavern){
|
||||
// Using _assign so we don't lose the reference to the exported tavern
|
||||
_.assign(module.exports.tavern, tavern);
|
||||
mongoose.model('Group').findOne(tavernQ, function(err,tavern){
|
||||
if (!tavern) return; // No tavern quest
|
||||
|
||||
var quest = tavern.quest.toObject();
|
||||
// Using _assign so we don't lose the reference to the exported tavernQuest
|
||||
_.assign(module.exports.tavernQuest, quest);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -291,14 +294,13 @@ GroupSchema.statics.tavernBoss = function(user,progress) {
|
||||
},
|
||||
function(tavern,cb){
|
||||
if (!(tavern && tavern.quest && tavern.quest.key)) return cb(true);
|
||||
module.exports.tavern = tavern;
|
||||
|
||||
var quest = shared.content.quests[tavern.quest.key];
|
||||
if (tavern.quest.progress.hp <= 0) {
|
||||
tavern.sendChat(quest.completionChat('en'));
|
||||
tavern.finishQuest(quest, function(){});
|
||||
tavern.save(cb);
|
||||
module.exports.tavern = undefined;
|
||||
_.assign(module.exports.tavernQuest, {extra: null});
|
||||
} else {
|
||||
// Deal damage. Note a couple things here, str & def are calculated. If str/def are defined in the database,
|
||||
// use those first - which allows us to update the boss on the go if things are too easy/hard.
|
||||
@@ -308,8 +310,7 @@ GroupSchema.statics.tavernBoss = function(user,progress) {
|
||||
if (tavern.quest.progress.rage >= quest.boss.rage.value) {
|
||||
if (!tavern.quest.extra.worldDmg) tavern.quest.extra.worldDmg = {};
|
||||
var wd = tavern.quest.extra.worldDmg;
|
||||
// var scene = wd.tavern ? wd.stables ? wd.market ? false : 'market' : 'stables' : 'tavern'; // Dilatory attacks tavern, stables, market
|
||||
var scene = wd.stables ? wd.bailey ? wd.guide ? false : 'guide' : 'bailey' : 'stables'; // Stressbeast attacks stables, Bailey, Justin
|
||||
var scene = wd.quests ? wd.seasonalShop ? wd.tavern ? false : 'tavern' : 'seasonalShop' : 'quests'; // Burnout attacks Ian, Seasonal Sorceress, tavern
|
||||
if (!scene) {
|
||||
tavern.sendChat('`'+quest.boss.name('en')+' tries to unleash '+quest.boss.rage.title('en')+', but is too tired.`');
|
||||
tavern.quest.progress.rage = 0 //quest.boss.rage.value;
|
||||
@@ -319,16 +320,20 @@ GroupSchema.statics.tavernBoss = function(user,progress) {
|
||||
tavern.quest.extra.worldDmg.recent = scene;
|
||||
tavern.markModified('quest.extra.worldDmg');
|
||||
tavern.quest.progress.rage = 0;
|
||||
tavern.quest.progress.hp += (quest.boss.rage.healing * tavern.quest.progress.hp);
|
||||
if (quest.boss.rage.healing) {
|
||||
tavern.quest.progress.hp += (quest.boss.rage.healing * tavern.quest.progress.hp);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((tavern.quest.progress.hp < quest.boss.desperation.threshold) && !tavern.quest.extra.desperate) {
|
||||
if (quest.boss.desperation && (tavern.quest.progress.hp < quest.boss.desperation.threshold) && !tavern.quest.extra.desperate) {
|
||||
tavern.sendChat(quest.boss.desperation.text('en'));
|
||||
tavern.quest.extra.desperate = true;
|
||||
tavern.quest.extra.def = quest.boss.desperation.def;
|
||||
tavern.quest.extra.str = quest.boss.desperation.str;
|
||||
tavern.markModified('quest.extra');
|
||||
}
|
||||
|
||||
_.assign(module.exports.tavernQuest, tavern.quest.toObject());
|
||||
tavern.save(cb);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,8 @@ var UserSchema = new Schema({
|
||||
email: String,
|
||||
hashed_password: String,
|
||||
salt: String,
|
||||
username: String
|
||||
username: String,
|
||||
lowerCaseUsername: String // Store a lowercase version of username to check for duplicates
|
||||
},
|
||||
timestamps: {
|
||||
created: {type: Date,'default': Date.now},
|
||||
@@ -138,6 +139,29 @@ var UserSchema = new Schema({
|
||||
hall: {type: Number, 'default': -1},
|
||||
equipment: {type: Number, 'default': -1}
|
||||
},
|
||||
tutorial: {
|
||||
common: {
|
||||
habits: {type: Boolean, 'default': false},
|
||||
dailies: {type: Boolean, 'default': false},
|
||||
todos: {type: Boolean, 'default': false},
|
||||
rewards: {type: Boolean, 'default': false},
|
||||
party: {type: Boolean, 'default': false},
|
||||
pets: {type: Boolean, 'default': false},
|
||||
gems: {type: Boolean, 'default': false},
|
||||
skills: {type: Boolean, 'default': false},
|
||||
classes: {type: Boolean, 'default': false},
|
||||
tavern: {type: Boolean, 'default': false},
|
||||
equipment: {type: Boolean, 'default': false},
|
||||
items: {type: Boolean, 'default': false},
|
||||
},
|
||||
ios: {
|
||||
addTask: {type: Boolean, 'default': false},
|
||||
editTask: {type: Boolean, 'default': false},
|
||||
deleteTask: {type: Boolean, 'default': false},
|
||||
filterTask: {type: Boolean, 'default': false},
|
||||
groupPets: {type: Boolean, 'default': false},
|
||||
}
|
||||
},
|
||||
dropsEnabled: {type: Boolean, 'default': false},
|
||||
itemsEnabled: {type: Boolean, 'default': false},
|
||||
newStuff: {type: Boolean, 'default': false},
|
||||
@@ -171,7 +195,6 @@ var UserSchema = new Schema({
|
||||
todos: Array //[{data: Date, value: Number}] // big peformance issues if these are defined
|
||||
},
|
||||
|
||||
// FIXME remove?
|
||||
invitations: {
|
||||
guilds: {type: Array, 'default': []},
|
||||
party: Schema.Types.Mixed
|
||||
@@ -324,6 +347,7 @@ var UserSchema = new Schema({
|
||||
language: String,
|
||||
automaticAllocation: Boolean,
|
||||
allocationMode: {type:String, enum: ['flat','classbased','taskbased'], 'default': 'flat'},
|
||||
autoEquip: {type: Boolean, 'default': true},
|
||||
costume: Boolean,
|
||||
dateFormat: {type: String, enum:['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'], 'default': 'MM/dd/yyyy'},
|
||||
sleep: {type: Boolean, 'default': false},
|
||||
@@ -579,7 +603,7 @@ UserSchema.methods.unlink = function(options, cb) {
|
||||
|
||||
module.exports.schema = UserSchema;
|
||||
module.exports.model = mongoose.model("User", UserSchema);
|
||||
// Initially export an empty object so external requires will get
|
||||
// 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
|
||||
module.exports.mods = [];
|
||||
|
||||
@@ -155,7 +155,7 @@ router.post('/user/tasks/:id/:direction', auth.auth, i18n.getUserLanguage, cron,
|
||||
// Tasks
|
||||
router.get('/user/tasks', auth.auth, i18n.getUserLanguage, cron, api.getTasks);
|
||||
router.get('/user/task/:id', auth.auth, i18n.getUserLanguage, cron, api.getTask);
|
||||
router["delete"]('/user/task/:id', auth.auth, i18n.getUserLanguage, cron, api.deleteTask);
|
||||
router.delete('/user/task/:id', auth.auth, i18n.getUserLanguage, cron, api.deleteTask);
|
||||
router.post('/user/task', auth.auth, i18n.getUserLanguage, cron, api.addTask);
|
||||
|
||||
// User
|
||||
|
||||
@@ -265,7 +265,7 @@ module.exports = (swagger, v2) ->
|
||||
method: 'DELETE'
|
||||
description: "Delete a user object entirely, USE WITH CAUTION!"
|
||||
middleware: [auth.auth, i18n.getUserLanguage]
|
||||
action: user["delete"]
|
||||
action: user.delete
|
||||
|
||||
"/user/revive":
|
||||
spec:
|
||||
@@ -347,8 +347,19 @@ module.exports = (swagger, v2) ->
|
||||
action: user.batchUpdate
|
||||
|
||||
# Tags
|
||||
"/user/tags":
|
||||
"/user/tags/{id}:GET":
|
||||
spec:
|
||||
path: '/user/tags/{id}'
|
||||
method: 'GET'
|
||||
description: "Get a tag"
|
||||
parameters: [
|
||||
path 'id','The id of the tag to get','string'
|
||||
]
|
||||
action: user.getTag
|
||||
|
||||
"/user/tags:POST":
|
||||
spec:
|
||||
path: "/user/tags"
|
||||
method: 'POST'
|
||||
description: 'Create a new tag'
|
||||
parameters: [
|
||||
@@ -356,6 +367,13 @@ module.exports = (swagger, v2) ->
|
||||
]
|
||||
action: user.addTag
|
||||
|
||||
"/user/tags:GET":
|
||||
spec:
|
||||
path: "/user/tags"
|
||||
method: 'GET'
|
||||
description: 'List all of a user\'s tags'
|
||||
action: user.getTags
|
||||
|
||||
"/user/tags/sort":
|
||||
spec:
|
||||
method: 'POST'
|
||||
@@ -756,7 +774,7 @@ module.exports = (swagger, v2) ->
|
||||
description: "Delete a challenge"
|
||||
parameters: [path('cid','Challenge id','string')]
|
||||
middleware: [auth.auth, i18n.getUserLanguage]
|
||||
action: challenges["delete"]
|
||||
action: challenges.delete
|
||||
|
||||
"/challenges/{cid}/close":
|
||||
spec:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Only do the minimal amount of work before forking just in case of a dyno restart
|
||||
var cluster = require("cluster");
|
||||
var cluster = require('cluster');
|
||||
var _ = require('lodash');
|
||||
var nconf = require('nconf');
|
||||
var utils = require('./utils');
|
||||
@@ -7,11 +7,14 @@ utils.setupConfig();
|
||||
var logging = require('./logging');
|
||||
var isProd = nconf.get('NODE_ENV') === 'production';
|
||||
var isDev = nconf.get('NODE_ENV') === 'development';
|
||||
var cores = +nconf.get("WEB_CONCURRENCY") || 0;
|
||||
var DISABLE_LOGGING = nconf.get('DISABLE_REQUEST_LOGGING');
|
||||
var cores = +nconf.get('WEB_CONCURRENCY') || 0;
|
||||
|
||||
if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
|
||||
// Fork workers. If config.json has CORES=x, use that - otherwise, use all cpus-1 (production)
|
||||
_.times(cores, cluster.fork);
|
||||
for (var i = 0, n = cores; i < n; i += 1) {
|
||||
cluster.fork();
|
||||
}
|
||||
|
||||
cluster.on('disconnect', function(worker, code, signal) {
|
||||
var w = cluster.fork(); // replace the dead worker
|
||||
@@ -20,10 +23,10 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
|
||||
|
||||
} else {
|
||||
require('coffee-script'); // remove this once we've fully converted over
|
||||
var express = require("express");
|
||||
var http = require("http");
|
||||
var path = require("path");
|
||||
var swagger = require("swagger-node-express");
|
||||
var express = require('express');
|
||||
var http = require('http');
|
||||
var path = require('path');
|
||||
var swagger = require('swagger-node-express');
|
||||
var autoinc = require('mongoose-id-autoinc');
|
||||
var shared = require('../../common');
|
||||
|
||||
@@ -76,8 +79,8 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
|
||||
// This auth strategy is no longer used. It's just kept around for auth.js#loginFacebook() (passport._strategies.facebook.userProfile)
|
||||
// The proper fix would be to move to a general OAuth module simply to verify accessTokens
|
||||
passport.use(new FacebookStrategy({
|
||||
clientID: nconf.get("FACEBOOK_KEY"),
|
||||
clientSecret: nconf.get("FACEBOOK_SECRET"),
|
||||
clientID: nconf.get('FACEBOOK_KEY'),
|
||||
clientSecret: nconf.get('FACEBOOK_SECRET'),
|
||||
//callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback"
|
||||
},
|
||||
function(accessToken, refreshToken, profile, done) {
|
||||
@@ -86,61 +89,75 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
|
||||
));
|
||||
|
||||
// ------------ Server Configuration ------------
|
||||
var publicDir = path.join(__dirname, "/../public");
|
||||
var publicDir = path.join(__dirname, '/../public');
|
||||
|
||||
app.set("port", nconf.get('PORT'));
|
||||
app.set('port', nconf.get('PORT'));
|
||||
require('./middlewares/apiThrottle')(app);
|
||||
app.use(require('./middlewares/domain')(server,mongoose));
|
||||
if (!isProd) app.use(express.logger("dev"));
|
||||
app.use(express.compress());
|
||||
app.set("views", __dirname + "/../views");
|
||||
app.set("view engine", "jade");
|
||||
app.use(express.favicon(publicDir + '/favicon.ico'));
|
||||
if (!isProd && !DISABLE_LOGGING) app.use(require('morgan')('dev'));
|
||||
app.use(require('compression')());
|
||||
app.set('views', __dirname + '/../views');
|
||||
app.set('view engine', 'jade');
|
||||
app.use(require('serve-favicon')(publicDir + '/favicon.ico'));
|
||||
app.use(require('./middlewares/cors'));
|
||||
|
||||
var redirects = require('./middlewares/redirects');
|
||||
app.use(redirects.forceHabitica);
|
||||
app.use(redirects.forceSSL);
|
||||
app.use(express.urlencoded());
|
||||
app.use(express.json());
|
||||
|
||||
var bodyParser = require('body-parser');
|
||||
// Default limit is 100kb, need that because we actually send whole groups to the server
|
||||
// FIXME as soon as possible (need to move on the client from $resource -> $http)
|
||||
app.use(bodyParser.urlencoded({
|
||||
limit: '1mb',
|
||||
parameterLimit: 10000, // Upped for safety from 1k, FIXME as above
|
||||
extended: true // Uses 'qs' library as old connect middleware
|
||||
}));
|
||||
app.use(bodyParser.json({
|
||||
limit: '1mb'
|
||||
}));
|
||||
|
||||
app.use(require('method-override')());
|
||||
//app.use(express.cookieParser(nconf.get('SESSION_SECRET')));
|
||||
app.use(express.cookieParser());
|
||||
app.use(express.cookieSession({ secret: nconf.get('SESSION_SECRET'), httpOnly: false, cookie: { maxAge: TWO_WEEKS }}));
|
||||
//app.use(express.session());
|
||||
app.use(require('cookie-parser')());
|
||||
app.use(require('cookie-session')({
|
||||
name: 'connect:sess', // Used to keep backward compatibility with Express 3 cookies
|
||||
secret: nconf.get('SESSION_SECRET'),
|
||||
httpOnly: false,
|
||||
maxAge: TWO_WEEKS
|
||||
}));
|
||||
|
||||
// Initialize Passport! Also use passport.session() middleware, to support
|
||||
// persistent login sessions (recommended).
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
app.use(app.router);
|
||||
// Custom Directives
|
||||
app.use(require('./routes/pages'));
|
||||
app.use(require('./routes/payments'));
|
||||
app.use(require('./routes/auth'));
|
||||
app.use(require('./routes/coupon'));
|
||||
app.use(require('./routes/unsubscription'));
|
||||
var v2 = express();
|
||||
app.use('/api/v2', v2);
|
||||
app.use('/api/v1', require('./routes/apiv1'));
|
||||
app.use('/export', require('./routes/dataexport'));
|
||||
require('./routes/apiv2.coffee')(swagger, v2);
|
||||
|
||||
var maxAge = isProd ? 31536000000 : 0;
|
||||
// Cache emojis without copying them to build, they are too many
|
||||
app.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge }));
|
||||
app.use('/common/dist', express['static'](publicDir + "/../../common/dist", { maxAge: maxAge }));
|
||||
app.use('/common/audio', express['static'](publicDir + "/../../common/audio", { maxAge: maxAge }));
|
||||
app.use('/common/script/public', express['static'](publicDir + "/../../common/script/public", { maxAge: maxAge }));
|
||||
app.use('/common/img', express['static'](publicDir + "/../../common/img", { maxAge: maxAge }));
|
||||
app.use(express['static'](publicDir));
|
||||
app.use(express.static(path.join(__dirname, '/../build'), { maxAge: maxAge }));
|
||||
app.use('/common/dist', express.static(publicDir + '/../../common/dist', { maxAge: maxAge }));
|
||||
app.use('/common/audio', express.static(publicDir + '/../../common/audio', { maxAge: maxAge }));
|
||||
app.use('/common/script/public', express.static(publicDir + '/../../common/script/public', { maxAge: maxAge }));
|
||||
app.use('/common/img', express.static(publicDir + '/../../common/img', { maxAge: maxAge }));
|
||||
app.use(express.static(publicDir));
|
||||
|
||||
// Custom Directives
|
||||
app.use(require('./routes/pages').middleware);
|
||||
app.use(require('./routes/payments').middleware);
|
||||
app.use(require('./routes/auth').middleware);
|
||||
app.use(require('./routes/coupon').middleware);
|
||||
app.use(require('./routes/unsubscription').middleware);
|
||||
var v2 = express();
|
||||
app.use('/api/v2', v2);
|
||||
app.use('/api/v1', require('./routes/apiv1').middleware);
|
||||
app.use('/export', require('./routes/dataexport').middleware);
|
||||
require('./routes/apiv2.coffee')(swagger, v2);
|
||||
app.use(require('./middlewares/errorHandler'));
|
||||
|
||||
server.on('request', app);
|
||||
server.listen(app.get("port"), function() {
|
||||
return logging.info("Express server listening on port " + app.get("port"));
|
||||
server.listen(app.get('port'), function() {
|
||||
return logging.info('Express server listening on port ' + app.get('port'));
|
||||
});
|
||||
|
||||
module.exports = server;
|
||||
|
||||
@@ -159,6 +159,10 @@ module.exports.makeSalt = function() {
|
||||
return crypto.randomBytes(Math.ceil(len / 2)).toString('hex').substring(0, len);
|
||||
}
|
||||
|
||||
// Prepare to export analytics object
|
||||
// Export emoty methods until the right ones are ready
|
||||
module.exports.analytics = { track: function() { }, trackPurchase: function() { } };
|
||||
|
||||
/**
|
||||
* Load nconf and define default configuration values if config.json or ENV vars are not found
|
||||
*/
|
||||
@@ -170,8 +174,8 @@ module.exports.setupConfig = function(){
|
||||
|
||||
if (nconf.get('NODE_ENV') === "development")
|
||||
Error.stackTraceLimit = Infinity;
|
||||
//if (nconf.get('NODE_ENV') === 'production')
|
||||
// require('newrelic');
|
||||
if (nconf.get('NODE_ENV') === 'production')
|
||||
require('newrelic');
|
||||
|
||||
isProd = nconf.get('NODE_ENV') === 'production';
|
||||
baseUrl = nconf.get('BASE_URL');
|
||||
@@ -182,9 +186,13 @@ module.exports.setupConfig = function(){
|
||||
googleAnalytics: nconf.get('GA_ID')
|
||||
}
|
||||
|
||||
module.exports.analytics = analytics
|
||||
? analytics(analyticsTokens)
|
||||
: { track: function() { }, trackPurchase: function() { } };
|
||||
if(analytics){
|
||||
analytics = analytics(analyticsTokens);
|
||||
// Use the right analytics methods, don't substitute the entire object
|
||||
// or all the require() across the code will keep the empty methods
|
||||
module.exports.analytics.track = analytics.track;
|
||||
module.exports.analytics.trackPurchase = analytics.trackPurchase;
|
||||
}
|
||||
};
|
||||
|
||||
var algorithm = 'aes-256-ctr';
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
h3.equipment-title.hint(popover-trigger='mouseenter',
|
||||
popover-placement='right', popover-append-to-body='true',
|
||||
popover=env.t('battleGearText'))=env.t('battleGear')
|
||||
|
||||
.checkbox.equipment-title
|
||||
label
|
||||
input(type="checkbox", ng-model="user.preferences.autoEquip",
|
||||
ng-change='set({"preferences.autoEquip":user.preferences.autoEquip ? true : false})')
|
||||
|
|
||||
=env.t('autoEquipBattleGear')
|
||||
|
||||
div
|
||||
button.btn.btn-default(type="button", ng-click='dequip("battleGear");') {{env.t("unequipBattleGear")}}
|
||||
|
||||
@@ -4,12 +4,13 @@ include ../../shared/mixins
|
||||
.row
|
||||
.col-md-6
|
||||
.clearfix
|
||||
.npc_ian.pull-left-sm.col-centered
|
||||
div(class="#{env.worldDmg.quests ? 'npc_ian_broken' : 'npc_ian'}").pull-left-sm.col-centered
|
||||
.popover.static-popover.fade.right.in.pull-left-sm.col-centered
|
||||
.arrow.hidden-xs
|
||||
h3.popover-title=env.t('ian')
|
||||
.popover-content
|
||||
p=env.t('ianText')
|
||||
p(ng-if='env.worldDmg.quests')=env.t('ianBrokenText')
|
||||
p(ng-if='!env.worldDmg.quests')=env.t('ianText')
|
||||
|
||||
h3.equipment-title=env.t('yourQuests')
|
||||
+ownedQuests(true,'right')
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
.container-fluid
|
||||
.stable.row: .col-xs-12
|
||||
.seasonalshop_open.pull-left-sm.col-centered
|
||||
div(class="#{env.worldDmg.seasonalShop ? 'seasonalshop_broken' : 'seasonalshop_open'}").pull-left-sm.col-centered
|
||||
.popover.static-popover.fade.right.in.pull-left-sm.col-centered
|
||||
.arrow.hidden-xs
|
||||
h3.popover-title!=env.t('seasonalShopTitle', {linkStart:"<a href='http://blog.habitrpg.com/who' target='_blank'>", linkEnd: "</a>"})
|
||||
.popover-content
|
||||
p!=env.t('seasonalShopFallText')
|
||||
p(ng-if='!env.worldDmg.seasonalShop')!=env.t('seasonalShopFallText')
|
||||
p(ng-if='env.worldDmg.seasonalShop')!=env.t('seasonalShopFallTextBroken')
|
||||
|
||||
.well(ng-if='User.user.achievements.rebirths > 0')=env.t('seasonalShopRebirth')
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
.pull-right
|
||||
span.inventory_special_trinket.inline-gems
|
||||
b x{{user.purchased.plan.consecutive.trinkets}}
|
||||
p!=env.t('timeTravelersPopover', {linkStart: "<a href='http://habitica.wikia.com/wiki/Mystery_Item' target='_blank'>", linkEnd: "</a>"})
|
||||
p!=env.t('timeTravelersPopover')
|
||||
|
||||
.row: .col-md-7
|
||||
.alert.alert-info(ng-if='hasAllTimeTravelerItems()')=env.t('timeTravelersAlreadyOwned')
|
||||
|
||||
@@ -103,19 +103,19 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
|
||||
br.visible-xs
|
||||
button.btn.btn-block.btn-primary(ng-click='openDayStartModal(dayStart)',
|
||||
ng-disabled='dayStart == user.preferences.dayStart')
|
||||
| Save Custom Day Start
|
||||
=env.t('saveCustomDayStart')
|
||||
|
||||
.personal-options.col-md-6
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
span Registration
|
||||
span=env.t('registration')
|
||||
.panel-body
|
||||
div(ng-if='user.auth.facebook.id')
|
||||
button.btn.btn-primary(disabled='disabled', ng-if='!user.auth.local.username')=env.t('registeredWithFb')
|
||||
button.btn.btn-danger(ng-click='http("delete","/api/v2/user/auth/social",null,"detachedFacebook")', ng-if='user.auth.local.username')=env.t('detachFacebook')
|
||||
hr
|
||||
div(ng-if='!user.auth.local.username')
|
||||
p Add local authentication:
|
||||
p=env.t('addLocalAuth')
|
||||
form(ng-submit='http("post","/api/v2/register",localAuth,"addedLocalAuth")', ng-init='localAuth={}', name='localAuth', novalidate)
|
||||
//-.alert.alert-danger(ng-messages='changeUsername.$error && changeUsername.submitted')=env.t('fillAll')
|
||||
.form-group
|
||||
@@ -189,15 +189,15 @@ script(type='text/ng-template', id='partials/options.settings.promo.html')
|
||||
small= env.t('couponText')
|
||||
div(ng-if='user.contributor.sudo')
|
||||
hr
|
||||
h4 Generate Codes
|
||||
h4=env.t('generateCodes')
|
||||
form.form(role='form',ng-submit='generateCodes(_codes)',ng-init='_codes={}')
|
||||
.form-group
|
||||
input.form-control(type='text',ng-model='_codes.event',placeholder="Event code (eg, 'wondercon')")
|
||||
.form-group
|
||||
input.form-control(type='number',ng-model='_codes.count',placeholder="Number of codes to generate (eg, 250)")
|
||||
.form-group
|
||||
button.btn.btn-primary(type='submit') Generate
|
||||
a.btn.btn-default(href='/api/v2/coupons?_id={{user._id}}&apiToken={{user.apiToken}}') Get Codes
|
||||
button.btn.btn-primary(type='submit')=env.t('generate')
|
||||
a.btn.btn-default(href='/api/v2/coupons?_id={{user._id}}&apiToken={{user.apiToken}}')=env.t('getCodes')
|
||||
|
||||
script(type='text/ng-template', id='partials/options.settings.api.html')
|
||||
.container-fluid
|
||||
@@ -214,12 +214,12 @@ script(type='text/ng-template', id='partials/options.settings.api.html')
|
||||
|
||||
hr
|
||||
|
||||
h2 Webhooks
|
||||
h2=env.t('webhooks')
|
||||
table.table.table-striped
|
||||
thead(ng-if='hasWebhooks')
|
||||
tr
|
||||
th Enabled
|
||||
th Webhook URL
|
||||
th=env.t('enabled')
|
||||
th=env.t('webhookURL')
|
||||
th
|
||||
tbody
|
||||
tr(ng-repeat="webhook in user.preferences.webhooks | toArray:true | orderBy:'sort'")
|
||||
@@ -235,9 +235,9 @@ script(type='text/ng-template', id='partials/options.settings.api.html')
|
||||
td(colspan=2)
|
||||
form.form-horizontal(ng-submit='addWebhook(_newWebhook.url)')
|
||||
.form-group.col-sm-10
|
||||
input.form-control(type='url', ng-model='_newWebhook.url', placeholder='Webhook URL')
|
||||
input.form-control(type='url', ng-model='_newWebhook.url', placeholder=env.t('webhookURL'))
|
||||
.col-sm-2
|
||||
button.btn.btn-sm.btn-primary(type='submit') Add
|
||||
button.btn.btn-sm.btn-primary(type='submit')=env.t('add')
|
||||
|
||||
script(id='partials/options.settings.export.html', type="text/ng-template")
|
||||
.container-fluid
|
||||
@@ -258,7 +258,7 @@ mixin subPerks()
|
||||
tr
|
||||
td
|
||||
span.hint(popover=env.t('buyGemsGoldText', {gemCost: "{{Shared.planGemLimits.convRate}}", gemLimit: "{{Shared.planGemLimits.convCap}}"}),popover-trigger='mouseenter',popover-placement='right') #{env.t('buyGemsGold')}
|
||||
span.badge.badge-success(ng-show='_subscription.key!="basic_earned"') Cap raised to {{ [25 + user.purchased.plan.consecutive.gemCapExtra + Math.floor(Content.subscriptionBlocks[_subscription.key].months/3*5), 50] | min }}
|
||||
span.badge.badge-success(ng-show='_subscription.key!="basic_earned"')=env.t('buyGemsGoldCap', {amount: '{{:: gemGoldCap(_subscription) | min }}'})
|
||||
tr
|
||||
td
|
||||
span.hint(popover=env.t('retainHistoryText'),popover-trigger='mouseenter',popover-placement='right')=env.t('retainHistory')
|
||||
@@ -269,8 +269,8 @@ mixin subPerks()
|
||||
td
|
||||
span.hint(popover=env.t('mysteryItemText'),popover-trigger='mouseenter',popover-placement='right') #{env.t('mysteryItem')}
|
||||
div(ng-show='_subscription.key!="basic_earned"')
|
||||
.badge.badge-success +{{Math.floor(Content.subscriptionBlocks[_subscription.key].months/3)}} Mystic Hourglass
|
||||
.small.muted Mystic Hourglasses allow purchasing a previous month's Mystery Item set.
|
||||
.badge.badge-success=env.t('mysticHourglass', {amount: '+{{:: numberOfMysticHourglasses(_subscription)}}'})
|
||||
.small.muted=env.t('mysticHourglassText')
|
||||
tr
|
||||
td
|
||||
span.hint(popover=env.t('supportDevsText'),popover-trigger='mouseenter',popover-placement='right')=env.t('supportDevs')
|
||||
@@ -324,17 +324,17 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template')
|
||||
| #{env.t('subCanceled')} <strong>{{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
|
||||
tr(ng-if='!user.purchased.plan.dateTerminated'): td
|
||||
h4=env.t('subscribed')
|
||||
p(ng-if='user.purchased.plan.planId') Recurring ${{Content.subscriptionBlocks[user.purchased.plan.planId].price}} each {{Content.subscriptionBlocks[user.purchased.plan.planId].months}} Month(s) ({{user.purchased.plan.paymentMethod}})
|
||||
p(ng-if='user.purchased.plan.planId')=env.t('purchasedPlanId', {price: '{{Content.subscriptionBlocks[user.purchased.plan.planId].price}}', months: '{{Content.subscriptionBlocks[user.purchased.plan.planId].months}}', plan: '{{user.purchased.plan.paymentMethod}}'})
|
||||
tr(ng-if='user.purchased.plan.extraMonths'): td
|
||||
span.glyphicon.glyphicon-credit-card
|
||||
| You have {{user.purchased.plan.extraMonths | number:2}} months of subscription credit.
|
||||
| #{env.t('purchasedPlanExtraMonths', {months: '{{user.purchased.plan.extraMonths | number:2}}'})}
|
||||
tr(ng-if='user.purchased.plan.consecutive.count || user.purchased.plan.consecutive.offset'): td
|
||||
span.glyphicon.glyphicon-forward
|
||||
| Consecutive Subscription
|
||||
| #{env.t('consecutiveSubscription')}
|
||||
ul.list-unstyled
|
||||
li Consecutive Months: {{user.purchased.plan.consecutive.count + user.purchased.plan.consecutive.offset}}
|
||||
li Gem Cap Extra: {{user.purchased.plan.consecutive.gemCapExtra}}
|
||||
li Mystic Hourglasses: {{user.purchased.plan.consecutive.trinkets}}
|
||||
li #{env.t('consecutiveMonths')} {{user.purchased.plan.consecutive.count + user.purchased.plan.consecutive.offset}}
|
||||
li #{env.t('gemCapExtra')} {{user.purchased.plan.consecutive.gemCapExtra}}
|
||||
li #{env.t('mysticHourglasses')} {{user.purchased.plan.consecutive.trinkets}}
|
||||
div(ng-if='!user.purchased.plan.customerId || (user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')
|
||||
.form-group
|
||||
.radio(ng-repeat='block in Content.subscriptionBlocks | toArray | omit: "discount==true" | orderBy:"months"')
|
||||
@@ -355,8 +355,8 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template')
|
||||
|
||||
h3(ng-if='(user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')= env.t("resubscribe")
|
||||
a.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon})', ng-disabled='!_subscription.key')= env.t('card')
|
||||
a.btn.btn-success(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}', ng-disabled='!_subscription.key') PayPal
|
||||
a.btn.btn-warning(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon})") Amazon Payments
|
||||
a.btn.btn-success(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}', ng-disabled='!_subscription.key')=env.t('paypal')
|
||||
a.btn.btn-warning(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon})")=env.t('amazonPayments')
|
||||
div(ng-if='user.purchased.plan.customerId')
|
||||
.btn.btn-primary(ng-if='!user.purchased.plan.dateTerminated && user.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit()')=env.t('subUpdateCard')
|
||||
.btn.btn-sm.btn-danger(ng-if='!user.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription()')=env.t('cancelSub')
|
||||
|
||||
@@ -186,11 +186,13 @@ script(type='text/ng-template', id='partials/options.social.challenges.html')
|
||||
| {{challenge.group.name}}
|
||||
a(ui-sref="options.social.party" ng-if="challenge.group.type=='party'")
|
||||
| {{challenge.group.name}}
|
||||
li
|
||||
li(ng-if='challenge.leader.profile.name')
|
||||
=env.t('by') + ' '
|
||||
a(ng-click='clickMember(challenge.leader._id, true)') {{challenge.leader.profile.name}}
|
||||
li
|
||||
=env.t('participants', {membercount: "{{challenge.memberCount}}"})
|
||||
a(ng-click='clickMember(challenge.leader._id, true)') {{::challenge.leader.profile.name}}
|
||||
li(ng-if='!challenge.leader.profile.name',
|
||||
popover=env.t('noChallengeOwnerPopover'),
|
||||
popover-trigger='mouseenter')=env.t('noChallengeOwner')
|
||||
li=env.t('participants', {membercount: "{{challenge.memberCount}}"})
|
||||
li(ng-show='challenge.prize')
|
||||
p!=env.t('prizeValue', {gemcount: "{{challenge.prize}}", gemicon: "<span class='inline-block Pet_Currency_Gem1x'></span>"})
|
||||
li.bg-transparent
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
div.chat-form.guidelines-not-accepted(ng-if='!user.flags.communityGuidelinesAccepted')
|
||||
p If you would like to post messages in the Tavern or any party or guild chat, please first read our
|
||||
|
|
||||
a(target='_blank', href='/static/community-guidelines')=env.t('communityGuidelines')
|
||||
| and then click the button below to indicate that you accept them.
|
||||
p!=env.t('requestAcceptGuidelines', {linkStart: '<a target="_blank", href="/static/community-guidelines">', linkEnd: '</a>'})
|
||||
.chat-controls
|
||||
div
|
||||
button.btn.btn-warning(ng-click='acceptCommunityGuidelines()')=env.t('iAcceptCommunityGuidelines')
|
||||
|
||||
@@ -8,17 +8,17 @@ else
|
||||
.meter-label(tooltip=env.t('bossHP'))
|
||||
span.glyphicon.glyphicon-heart
|
||||
.meter.health
|
||||
.bar(ng-style='{width: Shared.percent(progress.hp, boss.hp) + "%"}')
|
||||
.bar(ng-style='{width: Shared.percent(group.quest.progress.hp, Content.quests[group.quest.key].boss.hp) + "%"}')
|
||||
span.meter-text.value
|
||||
| {{Math.ceil(progress.hp) | roundLargeNumbers}}
|
||||
| / {{::boss.hp | roundLargeNumbers}}
|
||||
div(ng-if='boss.rage')
|
||||
.meter-label(tooltip='Rage')
|
||||
| {{Math.ceil(group.quest.progress.hp) | roundLargeNumbers}}
|
||||
| / {{::Content.quests[group.quest.key].boss.hp | roundLargeNumbers}}
|
||||
div(ng-if='Content.quests[group.quest.key].boss.rage')
|
||||
.meter-label(tooltip=env.t('rage'))
|
||||
span.glyphicon.glyphicon-fire
|
||||
.meter.mana(popover="{{::boss.rage.description()}}",
|
||||
popover-title="{{::boss.rage.title()}}",
|
||||
.meter.mana(popover="{{::Content.quests[group.quest.key].boss.rage.description()}}",
|
||||
popover-title="{{::Content.quests[group.quest.key].boss.rage.title()}}",
|
||||
popover-trigger='mouseenter', popover-placement='bottom')
|
||||
.bar(ng-style='{width: Shared.percent(progress.rage, boss.rage.value) + "%"}')
|
||||
.bar(ng-style='{width: Shared.percent(group.quest.progress.rage, Content.quests[group.quest.key].boss.rage.value) + "%"}')
|
||||
span.meter-text.value
|
||||
| {{Math.ceil(progress.rage) | roundLargeNumbers}}
|
||||
| / {{::boss.rage.value | roundLargeNumbers}}
|
||||
| {{Math.ceil(group.quest.progress.rage) | roundLargeNumbers}}
|
||||
| / {{::Content.quests[group.quest.key].boss.rage.value | roundLargeNumbers}}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
.npc_ian.pull-left
|
||||
div(class="#{env.worldDmg.quests ? 'npc_ian_broken' : 'npc_ian'}").pull-left
|
||||
|
||||
div(ng-if='::Content.quests[group.quest.key].boss')
|
||||
if tavern
|
||||
p!=env.t('tavernBossInfo')
|
||||
p(ng-if='!env.worldDmg.quests')!=env.t('tavernBossInfo')
|
||||
p(ng-if='env.worldDmg.quests')!=env.t('tavernBossInfoBroken')
|
||||
else
|
||||
p!=env.t('bossDmg1')
|
||||
p(ng-if='!env.worldDmg.quests')!=env.t('bossDmg1')
|
||||
p(ng-if='env.worldDmg.quests')!=env.t('bossDmg1Broken')
|
||||
br
|
||||
p=env.t('bossDmg2')
|
||||
p(ng-if='!env.worldDmg.quests')=env.t('bossDmg2')
|
||||
p(ng-if='env.worldDmg.quests')=env.t('bossDmg2Broken')
|
||||
|
||||
div(ng-if='::Content.quests[group.quest.key].collect')
|
||||
p=env.t('bossColl1')
|
||||
p(ng-if='!env.worldDmg.quests')=env.t('bossColl1')
|
||||
p(ng-if='env.worldDmg.quests')=env.t('bossColl1Broken')
|
||||
br
|
||||
p=env.t('bossColl2')
|
||||
p(ng-if='!env.worldDmg.quests')=env.t('bossColl2')
|
||||
p(ng-if='env.worldDmg.quests')=env.t('bossColl2Broken')
|
||||
|
||||
@@ -9,12 +9,15 @@ div(ng-if='group.quest.active===true')
|
||||
include ./collectionStats
|
||||
|
||||
div(ng-bind-html='::Content.quests[group.quest.key].notes()')
|
||||
unless tavern
|
||||
quest-rewards(key='{{::group.quest.key}}')
|
||||
|
||||
tab(heading=env.t('questParticipants'))
|
||||
+participants(true)
|
||||
|
||||
if tavern
|
||||
include ./bossStats
|
||||
div(ng-bind-html='::Content.quests[group.quest.key].notes()')
|
||||
|
||||
hr
|
||||
|
||||
include ./ianQuestInfo
|
||||
|
||||
@@ -18,8 +18,9 @@ div(ng-if='group.quest.active===false')
|
||||
|
||||
hr
|
||||
|
||||
.npc_ian.pull-left
|
||||
p=env.t('questStart')
|
||||
div(class="#{env.worldDmg.quests ? 'npc_ian_broken' : 'npc_ian'}").pull-left
|
||||
p(ng-if='!env.worldDmg.quests')=env.t('questStart')
|
||||
p(ng-if='env.worldDmg.quests')=env.t('questStartBroken')
|
||||
|
||||
span(ng-if='user.party.quest.RSVPNeeded')
|
||||
button.btn.btn-sm.btn-success(ng-click='questAccept()')=env.t('accept')
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
h3.popover-title
|
||||
a(target='_blank', href='http://www.kickstarter.com/profile/2014640723')=env.t('daniel')
|
||||
.popover-content
|
||||
| {{user.preferences.sleep ? env.t('innText',{name: user.profile.name}) : env.t('danielText')}}
|
||||
span(ng-if='!env.worldDmg.tavern') {{user.preferences.sleep ? env.t('innText',{name: user.profile.name}) : env.t('danielText')}}
|
||||
span(ng-if='env.worldDmg.tavern') {{user.preferences.sleep ? env.t('innTextBroken',{name: user.profile.name}) : env.t('danielTextBroken')}}
|
||||
button.btn-block.btn.btn-lg.btn-success(ng-click='User.user.ops.sleep({})')
|
||||
| {{user.preferences.sleep ? env.t('innCheckOut') : env.t('innCheckIn')}}
|
||||
span(ng-if='!user.preferences.sleep')=env.t('danielText2')
|
||||
span(ng-if='!user.preferences.sleep && !env.worldDmg.tavern')=env.t('danielText2')
|
||||
span(ng-if='!user.preferences.sleep && env.worldDmg.tavern')=env.t('danielText2Broken')
|
||||
|
||||
|
||||
// Resources
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
footer.footer(ng-controller='FooterCtrl')
|
||||
div(id='fb-root')
|
||||
.container
|
||||
.row
|
||||
.col-sm-3
|
||||
@@ -63,16 +64,19 @@ footer.footer(ng-controller='FooterCtrl')
|
||||
table
|
||||
tr
|
||||
td
|
||||
a.addthis_button_facebook_like(fb:like:layout='button_count')
|
||||
.fb-like(data-href='https://habitica.com/static/front', data-layout='button', data-action='like', data-share='true')
|
||||
tr
|
||||
td
|
||||
a.twitter-share-button(href='https://twitter.com/intent/tweet?text=Improve+yourself+in+the+land+of+Habitica!&via=habitica&url=https://habitica.com/&count=none')=env.t('tweet')
|
||||
tr
|
||||
td
|
||||
iframe(src='/bower_components/github-buttons/github-btn.html?user=habitrpg&repo=habitrpg&type=watch&count=true', allowtransparency='true', frameborder='0', scrolling='0', width='85px', height='20px')
|
||||
a.tumblr-share-button(data-href='https://habitica.com/static/front', data-notes='none')
|
||||
tr
|
||||
td
|
||||
a.addthis_button_google_plusone(g:plusone:size='medium')
|
||||
tr
|
||||
td
|
||||
iframe(src='/bower_components/github-buttons/github-btn.html?user=habitrpg&repo=habitrpg&type=watch&count=true', allowtransparency='true', frameborder='0', scrolling='0', width='85px', height='20px')
|
||||
else if (env.NODE_ENV==='development' || env.NODE_ENV==='test') && !env.isStaticPage
|
||||
h4 Debug
|
||||
.btn-group-vertical
|
||||
|
||||
@@ -38,9 +38,11 @@ script(type='text/ng-template', id='modals/showQuest.html')
|
||||
.modal-body
|
||||
+questInfo
|
||||
hr
|
||||
.npc_ian.pull-left
|
||||
p=env.t('questSend')
|
||||
p=env.t('questWarning')
|
||||
div(class="#{env.worldDmg.quests ? 'npc_ian_broken' : 'npc_ian'}").pull-left
|
||||
p(ng-if='!env.worldDmg.quests')=env.t('questSend')
|
||||
p(ng-if='!env.worldDmg.quests')=env.t('questWarning')
|
||||
p(ng-if='env.worldDmg.quests')=env.t('questSendBroken')
|
||||
p(ng-if='env.worldDmg.quests')=env.t('questWarningBroken')
|
||||
.modal-footer
|
||||
button.btn.btn-default(ng-click='closeQuest(); $close()')=env.t('cancel')
|
||||
button.btn.btn-primary(ng-click='questInit()')
|
||||
|
||||
@@ -9,7 +9,7 @@ script(type='text/ng-template', id='modals/reroll.html')
|
||||
.modal-footer
|
||||
button.btn.btn-default(ng-click='$close()')=env.t('neverMind')
|
||||
span(ng-if='user.balance < 1')
|
||||
a.btn.btn-success(ng-click='openModal("buyGems",{track:{"Gems > Reroll"}})')=env.t('buyMoreGems')
|
||||
a.btn.btn-success(ng-click='openModal("buyGems",{track:"Gems > Reroll"})')=env.t('buyMoreGems')
|
||||
span.gem-cost=env.t('notEnoughGems')
|
||||
span(ng-if='user.balance >= 1', ng-controller='SettingsCtrl')
|
||||
a.btn.btn-danger(ng-click='$close(); reroll()')=env.t('fortify')
|
||||
|
||||
@@ -1,32 +1,126 @@
|
||||
h2 10/5/2015 - OCTOBER BACKGROUNDS REVEALED, COSTUME CHALLENGE BEGINS, AND FALL PLOT-LINE CONTINUES
|
||||
h2 10/27/2015 - BURNOUT STRIKES AGAIN! PLUS, SPOOKY POTIONS VANISHING SOON
|
||||
hr
|
||||
tr
|
||||
td
|
||||
.background_harvest_moon.pull-right
|
||||
h3 October Backgrounds Revealed
|
||||
p There are three new avatar backgrounds in the <a href='/#/options/profile/backgrounds'>Background Shop!</a> Now your avatar can slog through a Slimy Swamp, cackle under the Harvest Moon, or shiver in the Swarming Darkness!
|
||||
p.small.muted by Leephon, midori88, and Draayder
|
||||
.npc_daniel_broken.pull-right
|
||||
h3 World Boss: Burnout Uses Exhaust Strike!
|
||||
p Many Habiticans have been hiding from Burnout in the Tavern, but no longer! With a screeching howl, Burnout rakes the Tavern with its white-hot hands. As the Tavern patrons flee, Daniel is caught in Burnout's grip, and transforms into an Exhaust Spirit right in front of you!
|
||||
br
|
||||
p This hot-headed horror has gone on for too long. Don't give up... we're so close to vanquishing Burnout for once and for all!
|
||||
br
|
||||
p Late to the fight? Learn about Burnout and how to defeat World Bosses <a href='http://habitica.wikia.com/wiki/Boss#World_Bosses' target='_blank'>here</a>.
|
||||
tr
|
||||
td
|
||||
h3 Costume Challenge Begins
|
||||
p The Community Costume Challenge has begun! Between now and October 31st, dress up as your avatar in real life and post a photo on social media to get the coveted Costume Challenge badge! Read the full rules on the Challenge page <a href='/#/options/groups/challenges/0b3d1116-89d3-4025-8a9e-593bc28e9cff'>here</a>.
|
||||
p.small.muted by Lemoness
|
||||
tr
|
||||
td
|
||||
h3 Fall Plot-Line Continues
|
||||
p Although we are now well into autumn, the Flourishing Fields are locked in a sweltering heat wave. The candies grow sticky in the sun, and the Spooky Pets lie panting in the shade. But this is minor news compared to the calamity that has just occurred.
|
||||
.Pet-Wolf-Spooky.pull-right
|
||||
h3 Spooky Potions Vanishing Soon
|
||||
p If you're hoping to collect all the Spooky Pets, be sure to do it soon. The Spooky Hatching Potions will disappear from the Market on November 1st!
|
||||
br
|
||||
p The Joyful Reaper is gone.
|
||||
br
|
||||
p The citizens of the Flourishing Fields are working frantically to find their lost ruler, but many of their search parties have not returned. Those few that do make it back are empty-handed. Worse yet, they report frightening hordes of spirits drifting at the edges of the Fields.
|
||||
br
|
||||
p Redphoenix grimly gathers her scouts. "If these spirits are a threat, we need to face them directly," she says. "There's no point in procrastination." Without fuss or fanfare, they set off into the night.
|
||||
p If you purchase Spooky Hatching Potions before November 1st but don't have a chance to use them, they will remain in your inventory. Be sure to stock up now, or you'll have to wait a whole year for them to return!
|
||||
|
||||
if menuItem !== 'oldNews'
|
||||
hr
|
||||
a(href='/static/old-news', target='_blank') Read older news
|
||||
|
||||
mixin oldNews
|
||||
h2 10/26/2015 - OCTOBER SUBSCRIBER ITEM REVEALED
|
||||
tr
|
||||
td
|
||||
.promo_mystery_201510.pull-right
|
||||
h3 October Subscriber Item: Horned Goblin!
|
||||
p The October Subscriber Item has been revealed: the Horned Goblin Set! All October subscribers will receive the Goblin Horns and the Goblin Tail. You still have four days to <a href='/#/options/settings/subscription'>subscribe</a> and receive the item set! Thank you so much for your support - we really do rely on you to keep Habitica free to use and running smoothly.
|
||||
p.small.muted by Lemoness
|
||||
|
||||
h2 10/21/2015 - FROG PET QUEST, TO-DO SORTING FIX, AND SECOND WORLD BOSS EXHAUST STRIKE
|
||||
tr
|
||||
td
|
||||
span.Mount_Body_Frog-Base.pull-right
|
||||
span.Mount_Head_Frog-Base.pull-right(style='margin:0')
|
||||
h3 Frog Pet Quest
|
||||
p Deep in the Swamps of Stagnation, you find your path obstructed by debris... and an angry amphibian. Yuck! If you complete the <a href='/#/options/inventory/quests'>Clutter Frog quest</a>, you'll be rewarded with some princely Frog pets!
|
||||
p.small.muted Art by starsystemic, RosemonkeyCT, Jon Arjinborn, and Breadstrings
|
||||
p.small.muted Writing by Fluitare
|
||||
tr
|
||||
td
|
||||
h3 To-Do Sorting Fix
|
||||
p To-dos will now stay sorted when you drag and drop them! Often, To-Dos used to change their order unless you cleared your completed To-Dos on a regular basis. That should no longer be a problem.
|
||||
p.small.muted by ikadzuchi and Alys
|
||||
tr
|
||||
td
|
||||
.seasonalshop_broken.pull-right
|
||||
h3 World Boss: Burnout Uses Exhaust Strike!
|
||||
p Ahh!!! Our incomplete Dailies have fed the flames of Burnout, and now it has enough energy to strike again! It lets loose a gout of spectral flame that sears the Seasonal Shop. You're horrified to see that the cheery Seasonal Sorceress has been transformed into a sorrowful Exhaust Spirit!
|
||||
br
|
||||
p We have to rescue our NPCs! Hurry, Habiticans, complete your tasks and defeat Burnout before it strikes for a third time!
|
||||
br
|
||||
p Late to the fight? Learn about Burnout and how to defeat World Bosses <a href='http://habitica.wikia.com/wiki/Boss#World_Bosses' target='_blank'>here</a>.
|
||||
h2 10/14/2015 - JACK O'LANTERN PETS AND MOUNTS! WORLD BOSS EXHAUST STRIKE!
|
||||
hr
|
||||
tr
|
||||
td
|
||||
.Pet-JackOLantern-Base.pull-right
|
||||
h3 Jack O'Lantern Pets and Mounts
|
||||
p The Flourishing Fields are full of cute carved pumpkins - and it looks like one has followed you home! It must be frightened of Burnout and looking for an adventurer to protect it.
|
||||
br
|
||||
p Those of you who weren't around last Fall Festival have received a pet Jack-O-Lantern, and those of you who got one last year have received a Jack-O-Lantern Mount! Happy Fall Festival.
|
||||
p.small.muted by Lemoness
|
||||
tr
|
||||
td
|
||||
.npc_ian_broken.pull-right
|
||||
h3 World Boss: Burnout Uses Exhaust Strike!
|
||||
p Oh no! Despite our best efforts, we've let some Dailies get away from us, and now Burnout is inflamed with energy! With a crackling snarl, it engulfs Ian the Quest Master in a surge of spectral fire. As fallen quest scrolls smolder, the smoke clears, and you see that Ian has been drained of energy and turned into a drifting Exhaust Spirit!
|
||||
br
|
||||
p Only defeating Burnout can break the spell and save our beloved Quest Master. Let's keep our Dailies in check and defeat this monster before it attacks again!
|
||||
br
|
||||
p Late to the fight? Learn about Burnout and how to defeat World Bosses <a href='http://habitica.wikia.com/wiki/Boss#World_Bosses' target='_blank'>here</a>.
|
||||
|
||||
h2 10/8/2015 - WORLD BOSS REVEALED: BURNOUT!
|
||||
tr
|
||||
td
|
||||
.promo_burnout.pull-right
|
||||
h3 Burnout, Scourge of the Flourishing Fields
|
||||
p It is well past midnight, still and stiflingly hot, when Redphoenix and scout captain Kiwibot abruptly burst through the city gates. "We need to evacuate all the wooden buildings!" Redphoenix shouts. "Hurry!"
|
||||
br
|
||||
p Kiwibot grips the wall as she catches her breath."It's draining people and turning them into Exhaust Spirits! That's why everything was delayed. That's where the missing people have gone. It's been stealing their energy!"
|
||||
br
|
||||
p "'It'?'" asks Lemoness.
|
||||
br
|
||||
p And then the heat takes form.
|
||||
br
|
||||
p It rises from the earth in a billowing, twisting mass, and the air chokes with the scent of smoke and sulphur. Flames lick across the molten ground and contort into limbs, writhing to horrific heights. Smoldering eyes snap open, and the creature lets out a deep and crackling cackle.
|
||||
br
|
||||
p Kiwibot whispers a single word.
|
||||
br
|
||||
p "Burnout."
|
||||
hr
|
||||
p Quickly, Habiticans, we have to save the Flourishing Fields from incineration! All of us will battle this World Boss together by completing tasks, but it won't attack us individually. However, the more Dailies we skip, the closer we get to triggering its fearsome Exhaust Strike, which will target our NPCs!
|
||||
br
|
||||
p Knowing your enemy is the first step to defeating it. <a href='http://habitica.wikia.com/wiki/Boss#World_Bosses' target='_blank'>Read more about World Bosses here</a>.
|
||||
br
|
||||
p.small.muted by Lemoness, SabreCat, and Kiwibot
|
||||
|
||||
h2 10/5/2015 - OCTOBER BACKGROUNDS REVEALED, COSTUME CHALLENGE BEGINS, AND FALL PLOT-LINE CONTINUES
|
||||
tr
|
||||
td
|
||||
.background_harvest_moon.pull-right
|
||||
h3 October Backgrounds Revealed
|
||||
p There are three new avatar backgrounds in the <a href='/#/options/profile/backgrounds'>Background Shop!</a> Now your avatar can slog through a Slimy Swamp, cackle under the Harvest Moon, or shiver in the Swarming Darkness!
|
||||
p.small.muted by Leephon, midori88, and Draayder
|
||||
tr
|
||||
td
|
||||
h3 Costume Challenge Begins
|
||||
p The Community Costume Challenge has begun! Between now and October 31st, dress up as your avatar in real life and post a photo on social media to get the coveted Costume Challenge badge! Read the full rules on the Challenge page <a href='/#/options/groups/challenges/0b3d1116-89d3-4025-8a9e-593bc28e9cff'>here</a>.
|
||||
p.small.muted by Lemoness
|
||||
tr
|
||||
td
|
||||
h3 Fall Plot-Line Continues
|
||||
p Although we are now well into autumn, the Flourishing Fields are locked in a sweltering heat wave. The candies grow sticky in the sun, and the Spooky Pets lie panting in the shade. But this is minor news compared to the calamity that has just occurred.
|
||||
br
|
||||
p The Joyful Reaper is gone.
|
||||
br
|
||||
p The citizens of the Flourishing Fields are working frantically to find their lost ruler, but many of their search parties have not returned. Those few that do make it back are empty-handed. Worse yet, they report frightening hordes of spirits drifting at the edges of the Fields.
|
||||
br
|
||||
p Redphoenix grimly gathers her scouts. "If these spirits are a threat, we need to face them directly," she says. "There's no point in procrastination." Without fuss or fanfare, they set off into the night.
|
||||
|
||||
h2 10/1/2015 - SPOOKY HATCHING POTION; NEW ITEMS IN THE ENCHANTED ARMOIRE
|
||||
tr
|
||||
td
|
||||
|
||||
@@ -21,7 +21,7 @@ div(ng-if='::profile.backer.npc')
|
||||
hr
|
||||
|
||||
div(ng-if='::profile.contributor.level || user._id == profile._id')
|
||||
.achievement.achievement-firefox(ng-if='::profile.contributor.level')
|
||||
.achievement.achievement-boot(ng-if='::profile.contributor.level')
|
||||
div(ng-class='::{muted: !profile.contributor.level}')
|
||||
h5
|
||||
span.label.label-default(ng-if='::profile.contributor.level', class='label-contributor-{{::profile.contributor.level}}') {{::contribText(profile.contributor, profile.backer)}}
|
||||
@@ -219,6 +219,13 @@ div(ng-if='::profile.achievements.quests.stressbeast')
|
||||
=env.t('achievementStressbeastText')
|
||||
hr
|
||||
|
||||
div(ng-if='::profile.achievements.quests.burnout')
|
||||
.achievement.achievement-burnout
|
||||
h5=env.t('achievementBurnout')
|
||||
small
|
||||
=env.t('achievementBurnoutText')
|
||||
hr
|
||||
|
||||
div(ng-if='::profile.achievements.costumeContest')
|
||||
.achievement.achievement-costumeContest
|
||||
h5=env.t('costumeContest')
|
||||
|
||||
@@ -19,7 +19,7 @@ block content
|
||||
br
|
||||
=env.t('reportBug')
|
||||
| :
|
||||
a(target='_blank', href='https://github.com/HabitRPG/habitrpg/issues?q=is%3Aopen') Github
|
||||
a(target='_blank', href='https://github.com/HabitRPG/habitrpg/issues?q=is%3Aopen') GitHub
|
||||
br
|
||||
=env.t('reportCommunityIssues')
|
||||
| :
|
||||
|
||||
@@ -18,7 +18,7 @@ block content
|
||||
=env.t('September')
|
||||
| 4, 2015
|
||||
|
||||
- var headings = ['overview', 'set-up-tasks', 'sample-tasks', 'task-color', 'health', 'party-with-friends', 'pets-mounts', 'character-classes', 'blue-mana-bar', 'monsters-quests', 'gems', 'bugs-features']
|
||||
- var headings = ['overview', 'set-up-tasks', 'sample-tasks', 'task-color', 'health', 'party-with-friends', 'pets-mounts', 'character-classes', 'blue-mana-bar', 'monsters-quests', 'gems', 'bugs-features', 'world-boss']
|
||||
- for heading, index in headings
|
||||
a.h2.accordion.collapsed(ng-href='#' + heading, data-toggle='collapse')=env.t('faqQuestion' + index)
|
||||
.clearfix.collapse(id=heading)
|
||||
|
||||
@@ -2,9 +2,6 @@ doctype html
|
||||
html(ng-app='habitrpg', ng-controller='RootCtrl')
|
||||
head
|
||||
meta(charset='utf-8')
|
||||
script(type='text/javascript').
|
||||
window.env = !{JSON.stringify(env._.pick(env, env.clientVars))};
|
||||
!= env.getManifestFiles("tmp_static_front")
|
||||
link(href='//fonts.googleapis.com/css?family=Lato:400,700', rel='stylesheet', type='text/css')
|
||||
title Habitica | Your Life the Role Playing Game
|
||||
meta(name='description', content='')
|
||||
@@ -12,16 +9,19 @@ html(ng-app='habitrpg', ng-controller='RootCtrl')
|
||||
meta(name='author', content='')
|
||||
meta(name='geo.placename', content='')
|
||||
meta(name='viewport', content='width=device-width, maximum-scale=1')
|
||||
meta(property='og:title', content='')
|
||||
meta(property='og:description', content='')
|
||||
meta(property='og:url', content='')
|
||||
meta(property='og:image', content='')
|
||||
meta(property='og:site_name', content='')
|
||||
meta(property='og:url', content='https://habitica.com/static/front')
|
||||
meta(property='og:type', content='website')
|
||||
meta(property='og:title', content='Habitica: Your Life the Role Playing Game')
|
||||
meta(property='og:description', content='Habitica is a free habit building and productivity app that treats your real life like a game. With in-game rewards and punishments to motivate you and a strong social network to inspire you, Habitica can help you achieve your goals to become healthy, hard-working, and happy.')
|
||||
meta(property='og:site_name', content='Habitica')
|
||||
meta(property='og:image', content='https://s3.amazonaws.com/habitica-assets/assets/gryphon_logo_300x300.png')
|
||||
meta(property='fb:app_id', content='128307497299777')
|
||||
meta(name='twitter:card' content='summary')
|
||||
meta(name='twitter:site' content='@habitica')
|
||||
meta(name='twitter:title' content='Habitica: Your Life the Role Playing Game')
|
||||
meta(name='twitter:description' content='Habitica is a free habit-building and productivity app that treats your real life like a game! Motivate yourself with in-game rewards and social encouragement.')
|
||||
meta(name='twitter:image' content='https://s3.amazonaws.com/habitica-assets/assets/gryphon_logo.png')
|
||||
meta(name='apple-itunes-app' content='app-id=994882113')
|
||||
link(rel='canonical', href='', type='text/html')
|
||||
link(rel='shortcut icon', href='/favicon.ico')
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ block content
|
||||
p= env.t('presskitDownload')
|
||||
|
|
||||
a.btn.btn-success(href='/presskit/presskit.zip') presskit.zip
|
||||
- var imgs = ['habitrpg_pixel', 'HabitRPGdevices', 'Market Sample Screen', 'Challenges Sample Screen', 'Equipment Sample Screen', 'Guilds Sample Screen', 'HabitRPGPromoPostCard6', 'HabitRPGPromoThin', 'Laundromancer_by_Arcosine', 'promo mobile inventorySTILL1', 'promo mobile inventorySTILL2', 'PROMOdevices2', 'SnackLessMonster_by_Arcosine', 'stag_battle_by_leephon', 'stagnant_dishes_by_kiwibot', 'vice_reborn_by_baconsaur']
|
||||
- var imgs = ['Habitica Text Logo', 'Habitica Logo Promo', 'Habitica Gryphon', 'Dread Drag\'on of Dilatory', 'Market Sample Screen', 'Challenges Sample Screen', 'Equipment Sample Screen', 'Guilds Sample Screen', 'HabiticaPromoThin', 'Laundromancer', 'Tasks Page', 'SnackLess Monster', 'Battling the Ghost Stag', 'Stagnant Dishes Boss', 'Necro-Vice']
|
||||
|
||||
//-#carousel.carousel.slide(data-ride='carousel')
|
||||
ol.carousel-indicators
|
||||
|
||||