From 76a7ab5bcce2d486dab3f447f0659ba870d1ff7e Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Tue, 28 Jan 2014 08:57:29 -0600 Subject: [PATCH 01/32] feat(attributes): Add backfill button in flat and classbased allocation modes Partial implementation of a "distribute points now" button for backfilling accrued Attribute points. Works with flat and classbased allocation so far; taskbased will require much fancier handling. --- views/options/profile.jade | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/views/options/profile.jade b/views/options/profile.jade index 5c3d80ee3b..a3cd75f21d 100644 --- a/views/options/profile.jade +++ b/views/options/profile.jade @@ -155,6 +155,11 @@ script(id='partials/options.profile.stats.html', type='text/ng-template') =env.t('taskallocation')   i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('taskallocationpopover')) + div(ng-show='user.preferences.automaticAllocation && !(user.preferences.allocationMode === "taskbased") && (user.stats.points > 0)') + a.btn.btn-primary.btn-mini(ng-click='user.ops.allocateNow(user.stats)', popover-trigger='mouseenter', popover-placement='right', popover=env.t('distributepointspopover')) + i.icon-download.icon-white +   + =env.t('distributepoints') tr td= env.t('allocatestr') + ' {{user.stats.str}}' td From cd4cb0a552474488dfcea99b202df8981536ac14 Mon Sep 17 00:00:00 2001 From: Marcelo Sizer Date: Tue, 28 Jan 2014 22:30:05 -0200 Subject: [PATCH 02/32] i18n(subscriber): replace subscriber hard-coded strings for env.t --- views/options/settings.jade | 120 +++++++++++++++++++++++++----- views/shared/modals/buy-gems.jade | 5 +- 2 files changed, 104 insertions(+), 21 deletions(-) diff --git a/views/options/settings.jade b/views/options/settings.jade index 5a330cda07..2ee1c997b6 100644 --- a/views/options/settings.jade +++ b/views/options/settings.jade @@ -10,8 +10,7 @@ script(id='partials/options.settings.html', type="text/ng-template") a(ui-sref='options.settings.export') =env.t('dataexport') li(ng-class="{ active: $state.includes('options.settings.subscription') }") - a(ui-sref='options.settings.subscription') - | Subscription + a(ui-sref='options.settings.subscription')=env.t('subscription') .tab-content .tab-pane.active @@ -28,7 +27,10 @@ script(type='text/ng-template', id='partials/options.settings.settings.html') div small div=env.t('clockInfo') - .alert.alert-danger Warning: this is a highly-experimental feature, and many experience issues with it. + .alert.alert-danger + =env.t('subWarning1') + a(href='https://github.com/HabitRPG/habitrpg/issues/1057' target='_blank')=env.t('subWarning2') + =env.t('subWarning3') hr h4=env.t('language') select(ng-model='language.code', ng-options='lang.code as lang.name for lang in avalaibleLanguages', ng-change='changeLanguage()') @@ -116,28 +118,24 @@ script(id='partials/options.settings.subscription.perks.html',type='text/ng-temp table.table.table-striped tr td - span.dashed-underline(popover="Ads will stay disabled while you have an active account (original users with disabled ads are grandfathered).",popover-trigger='mouseenter',popover-placement='right') - | Disable ads + span.dashed-underline(popover=env.t('disableAdsText'),popover-trigger='mouseenter',popover-placement='right')=env.t('disableAds') tr td - span.dashed-underline(popover="(1 Gem costs {{Shared.planGemLimits.convRate}} Gold) Addresses the \"pay to win\" concern, as everything is now achievable through hard work. There's a {{Shared.planGemLimits.convCap}}G monthly conversion cap to prevent farming.",popover-trigger='mouseenter',popover-placement='right') - | Buy Gems with Gold + span.dashed-underline(popover=env.t('buyGemsGoldText', {gemCost: "{{Shared.planGemLimits.convRate}}", gemLimit: "{{Shared.planGemLimits.convCap}}"}),popover-trigger='mouseenter',popover-placement='right')=env.t('buyGemsGold') tr td - span.dashed-underline(popover="Makes your full history available in graphs and export. Non-subscriber histories get consolidated for database optimization.",popover-trigger='mouseenter',popover-placement='right') - | Retain full history entries + span.dashed-underline(popover=env.t('retainHistoryText'),popover-trigger='mouseenter',popover-placement='right')=env.t('retainHistory') tr td - span.dashed-underline(popover="Complete your stable faster!",popover-trigger='mouseenter',popover-placement='right') - | Daily drop-caps doubled + span.dashed-underline(popover=env.t('doubleDropsText'),popover-trigger='mouseenter',popover-placement='right')=env.t('doubleDrops') //-tr //- td +20 gems to your account tr td - span.dashed-underline(popover="This open source project can use all the help it can get. Help us keep Habit alive!",popover-trigger='mouseenter',popover-placement='right') - | Supports the developers + span.dashed-underline(popover=env.t('supportDevsText'),popover-trigger='mouseenter',popover-placement='right')=env.t('supportDevs') tr - td.alert.alert-info $5 USD / Month + td.alert.alert-info $5  + =env.t('monthUSD') script(id='partials/feature-matrix-check.html',type='text/ng-template') span.task-checker.action-yesno @@ -147,16 +145,100 @@ script(id='partials/feature-matrix-check.html',type='text/ng-template') script(id='partials/options.settings.subscription.html',type='text/ng-template') .well - h2 Individual Subscription + h2=env.t('individualSub') div(ng-if='!user.purchased.plan.customerId') div(ng-include="'partials/options.settings.subscription.perks.html'") - .btn.btn-primary(ng-click='showStripe(true)') Subscribe + .btn.btn-primary(ng-click='showStripe(true)')=env.t('subscribe') //-small.muted PayPal coming soon. div(ng-if='user.purchased.plan.customerId') .well div(style='font-size:22px') - i.icon-ok - | Subscribed + i.icon-ok=env.t('subscribed') div(ng-include="'partials/options.settings.subscription.perks.html'") - .btn.btn-small.btn-danger(ng-click='cancelSubscription()') Cancel Subscription \ No newline at end of file + .btn.btn-small.btn-danger(ng-click='cancelSubscription()')=env.t('cancelSub') + + + //-.well.subscription-features + h2=env.t('adminSub') + + table.table.table-striped + tr + th.feature-name + //th.feature-name.muted Family (Coming Soon) + //th.feature-name.muted Bronze (Coming Soon) + //th.feature-name.muted Silver (Coming Soon) + th.feature-name=env.t('gold') + th.feature-name.muted + !=env.t('morePlans') + tr + th + span.dashed-underline(popover=env.t('organizationSubText'),popover-trigger='mouseenter',popover-placement='right')=env.t('organizationSub') + //td(ng-include="'partials/feature-matrix-check.html'") + //td(ng-include="'partials/feature-matrix-check.html'") + //td(ng-include="'partials/feature-matrix-check.html'") + td(ng-include="'partials/feature-matrix-check.html'") + td + tr + th + span.dashed-underline(popover=env.t('hostingTypeText'),popover-trigger='mouseenter',popover-placement='right')=env.t('hostingType') + //td Shared + //td Shared + //td Shared + td=env.t('dedicated') + td + + tr + th + span.dashed-underline(popover=env.t('customDomainText'),popover-trigger='mouseenter',popover-placement='right')=env.t('customDomain') + //td.muted + //td.muted + //td.muted + td(ng-include="'partials/feature-matrix-check.html'") + td + tr + th + span.dashed-underline(popover=env.t('maxPlayersText'),popover-trigger='mouseenter',popover-placement='right')=env.t('maxPlayers') + //td.muted 10 + //td.muted 50 + //td.muted 150 + td=env.t('unlimited') + td + tr + th + span.dashed-underline(popover=env.t('priSupportText'),popover-trigger='mouseenter',popover-placement='right')=env.t('priSupport') + //td.muted + //td.muted + //td.muted + td(ng-include="'partials/feature-matrix-check.html'") + td + tr + th + span.dashed-underline(popover=env.t('timeSupportText'),popover-trigger='mouseenter',popover-placement='right')=env.t('timeSupport') + //td.muted 10 + //td.muted 20 + //td.muted 30 + td=env.t('unlimited') + td + tr + th + h5=env.t('gameFeatures') + ul(style='margin:0 0 10px 25px;') + li=env.t('gameNoAds') + li + span.dashed-underline(popover=env.t('gold2GemText'),popover-trigger='mouseenter',popover-placement='right')=env.t('gold2Gem') + li + span.dashed-underline(popover=env.t('infiniteGemText'),popover-trigger='mouseenter',popover-placement='right')=env.t('infiniteGem') + //td(ng-include="'partials/feature-matrix-check.html'") + //td(ng-include="'partials/feature-matrix-check.html'") + //td(ng-include="'partials/feature-matrix-check.html'") + td(ng-include="'partials/feature-matrix-check.html'") + td + tr + th=env.t('price') + //td.alert.alert-info ??? USD / Month + //td.alert.alert-info ??? USD / Month + //td.alert.alert-info ??? USD / Month + td.alert.alert-info ???  + =env.t('monthUSD') + td \ No newline at end of file diff --git a/views/shared/modals/buy-gems.jade b/views/shared/modals/buy-gems.jade index a7bd1375f8..63b5127abb 100644 --- a/views/shared/modals/buy-gems.jade +++ b/views/shared/modals/buy-gems.jade @@ -3,7 +3,7 @@ div(modal='modals.buyGems') .buy-gems include ../gems .well - h3 Buy Gems + h3=env.t('buyGems') table.table tr td @@ -14,7 +14,8 @@ div(modal='modals.buyGems') span.dashed-underline(popover=env.t('donateText3'),popover-trigger='mouseenter',popover-placement='right') =env.t('donateText2') tr - td.alert.alert-info=env.t('fiveUSD1') + td.alert.alert-info $5  + =env.t('USD') tr td .btn.btn-primary(ng-click='showStripe()',style='margin-left:10px;')=env.t('payWithCard') From aeb705b65266efa4e4a579238f02591812f28ca0 Mon Sep 17 00:00:00 2001 From: Marcelo Sizer Date: Wed, 29 Jan 2014 17:38:32 -0200 Subject: [PATCH 03/32] style(tavern): add space between 2 and gems --- views/options/social/tavern.jade | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/views/options/social/tavern.jade b/views/options/social/tavern.jade index 1193639838..6f30847450 100644 --- a/views/options/social/tavern.jade +++ b/views/options/social/tavern.jade @@ -68,35 +68,35 @@ a.label.label-contributor-1(ng-click='toggleUserTier($event)')=env.t('friendBadge') div(style='display:none;') p - != ' ' + env.t('friendText') + ' ' + env.t('first') + ' ' + env.t('friend1Text') + ' 2' + env.t('gems') + '.' + != ' ' + env.t('friendText') + ' ' + env.t('first') + ' ' + env.t('friend1Text') + ' 2 ' + env.t('gems') + '.' hr p - != ' ' + env.t('friendText') + ' ' + env.t('second') + ' ' + env.t('friendText2') + ' ' + env.t('crystalArmor') + ' ' + env.t('friendText3') + ' 2' + env.t('gems') + '.' + != ' ' + env.t('friendText') + ' ' + env.t('second') + ' ' + env.t('friendText2') + ' ' + env.t('crystalArmor') + ' ' + env.t('friendText3') + ' 2 ' + env.t('gems') + '.' tr td a.label.label-contributor-3(ng-click='toggleUserTier($event)')=env.t('eliteBadge') div(style='display:none;') p - != ' ' + env.t('friendText') + ' ' + env.t('third') + ' ' + env.t('friendText2') + ' ' + env.t('crystalHelmet') + ' ' + env.t('friendText3') + ' 2' + env.t('gems') + '.' + != ' ' + env.t('friendText') + ' ' + env.t('third') + ' ' + env.t('friendText2') + ' ' + env.t('crystalHelmet') + ' ' + env.t('friendText3') + ' 2 ' + env.t('gems') + '.' hr p - != ' ' + env.t('friendText') + ' ' + env.t('fourth') + ' ' + env.t('friendText2') + ' ' + env.t('crystalSword') + ' ' + env.t('friendText3') + ' 2' + env.t('gems') + '.' + != ' ' + env.t('friendText') + ' ' + env.t('fourth') + ' ' + env.t('friendText2') + ' ' + env.t('crystalSword') + ' ' + env.t('friendText3') + ' 2 ' + env.t('gems') + '.' tr td a.label.label-contributor-5(ng-click='toggleUserTier($event)')=env.t('championBadge') div(style='display:none;') p - != ' ' + env.t('friendText') + ' ' + env.t('fifth') + ' ' + env.t('friendText2') + ' ' + env.t('crystalShield') + ' ' + env.t('friendText3') + ' 2' + env.t('gems') + '.' + != ' ' + env.t('friendText') + ' ' + env.t('fifth') + ' ' + env.t('friendText2') + ' ' + env.t('crystalShield') + ' ' + env.t('friendText3') + ' 2 ' + env.t('gems') + '.' hr p - != '
' + env.t('friendText') + ' ' + env.t('sixth') + ' ' + env.t('friendText2b') + ' ' + env.t('hydraPet') + ' ' + env.t('friendText3b') + ' 2' + env.t('gems') + '.' + != '
' + env.t('friendText') + ' ' + env.t('sixth') + ' ' + env.t('friendText2b') + ' ' + env.t('hydraPet') + ' ' + env.t('friendText3b') + ' 2 ' + env.t('gems') + '.' tr td a.label.label-contributor-7(ng-click='toggleUserTier($event)')=env.t('legendaryBadge') div(style='display:none;') p - != env.t('friendText') + ' ' + env.t('seventh') + ' ' + env.t('friendText2c') + ' 2' + env.t('gems') + ' ' + env.t('friendText3c') + != env.t('friendText') + ' ' + env.t('seventh') + ' ' + env.t('friendText2c') + ' 2 ' + env.t('gems') + ' ' + env.t('friendText3c') tr td a.label.label-contributor-8(ng-click='toggleUserTier($event)')=env.t('heroicBadge') From 241d04140f5db77929d9f597d232f55843bb0f5d Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 29 Jan 2014 21:37:59 -0700 Subject: [PATCH 04/32] fix(spells): temp workaround for spell & task being undefined. #2649 #2640 --- src/controllers/user.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controllers/user.js b/src/controllers/user.js index 13fa2d00b4..5174d6f955 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -326,6 +326,7 @@ api.cast = function(req, res) { var targetId = req.query.targetId; var klass = shared.content.spells.special[req.params.spell] ? 'special' : user.stats.class var spell = shared.content.spells[klass][req.params.spell]; + if (!spell) return res.json(404, {err: 'Spell "' + req.params.spell + '" not found.'}); var done = function(){ var err = arguments[0]; @@ -336,6 +337,7 @@ api.cast = function(req, res) { switch (targetType) { case 'task': + if (!user.tasks[targetId]) return res.json(404, {err: 'Task "' + targetId + '" not found.'}); spell.cast(user, user.tasks[targetId]); user.save(done); break; From f6e12fa25e4366622db3e6f1b6ab03e848b49e10 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Thu, 30 Jan 2014 07:55:27 -0600 Subject: [PATCH 05/32] fix(allocateNow): Send empty object to user.ops per @colegleason suggestion Tested with both implemented allocation modes, looks good! --- views/options/profile.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/options/profile.jade b/views/options/profile.jade index a3cd75f21d..22052ecf53 100644 --- a/views/options/profile.jade +++ b/views/options/profile.jade @@ -156,7 +156,7 @@ script(id='partials/options.profile.stats.html', type='text/ng-template')   i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('taskallocationpopover')) div(ng-show='user.preferences.automaticAllocation && !(user.preferences.allocationMode === "taskbased") && (user.stats.points > 0)') - a.btn.btn-primary.btn-mini(ng-click='user.ops.allocateNow(user.stats)', popover-trigger='mouseenter', popover-placement='right', popover=env.t('distributepointspopover')) + a.btn.btn-primary.btn-mini(ng-click='user.ops.allocateNow({})', popover-trigger='mouseenter', popover-placement='right', popover=env.t('distributepointspopover')) i.icon-download.icon-white   =env.t('distributepoints') From 53917bc9f0112bcc23d12d33338b182912ab61a7 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 10:17:46 -0700 Subject: [PATCH 06/32] chore(cluster): remove all cluster code, just in case `require('cluster')` is flagging issues, even if not in use. Revert this in the near future when we can peg VersionError. @colegleason --- src/server.js | 221 +++++++++++++++++++++++--------------------------- 1 file changed, 103 insertions(+), 118 deletions(-) diff --git a/src/server.js b/src/server.js index d5a94b1963..a5013dcc44 100644 --- a/src/server.js +++ b/src/server.js @@ -1,141 +1,126 @@ // Only do the minimal amount of work before forking just in case of a dyno restart -var cluster = require("cluster"); var nconf = require('nconf'); -if (false && cluster.isMaster && (nconf.get('NODE_ENV') == 'development' || nconf.get('NODE_ENV') == 'production')) { - var numCPUs = require('os').cpus().length; +require('coffee-script') // remove this once we've fully converted over +var express = require("express"); +var http = require("http"); +var path = require("path"); +var domainMiddleware = require('domain-middleware'); +var swagger = require("swagger-node-express"); - // Fork workers. - for (var i = 0; i < numCPUs; i++) { - cluster.fork(); - } +var utils = require('./utils'); +var middleware = require('./middleware'); - cluster.on('exit', function(worker, code, signal) { - cluster.fork(); // replace the dead worker - }); +var TWO_WEEKS = 1000 * 60 * 60 * 24 * 14; +var app = express(); +var server; -} 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 domainMiddleware = require('domain-middleware'); - var swagger = require("swagger-node-express"); +// ------------ Setup configurations ------------ +utils.setupConfig(); - var utils = require('./utils'); - var middleware = require('./middleware'); - - var TWO_WEEKS = 1000 * 60 * 60 * 24 * 14; - var app = express(); - var server; - - // ------------ Setup configurations ------------ - utils.setupConfig(); - - // ------------ MongoDB Configuration ------------ - mongoose = require('mongoose'); - require('./models/user'); //load up the user schema - TODO is this necessary? - require('./models/group'); - require('./models/challenge'); - mongoose.connect(nconf.get('NODE_DB_URI'), {auto_reconnect:true}, function(err) { - if (err) throw err; - console.info('Connected with Mongoose'); - }); +// ------------ MongoDB Configuration ------------ +mongoose = require('mongoose'); +require('./models/user'); //load up the user schema - TODO is this necessary? +require('./models/group'); +require('./models/challenge'); +mongoose.connect(nconf.get('NODE_DB_URI'), {auto_reconnect:true}, function(err) { + if (err) throw err; + console.info('Connected with Mongoose'); +}); - // ------------ Passport Configuration ------------ - var passport = require('passport') - var util = require('util') - var FacebookStrategy = require('passport-facebook').Strategy; - // Passport session setup. - // To support persistent login sessions, Passport needs to be able to - // serialize users into and deserialize users out of the session. Typically, - // this will be as simple as storing the user ID when serializing, and finding - // the user by ID when deserializing. However, since this example does not - // have a database of user records, the complete Facebook profile is serialized - // and deserialized. - passport.serializeUser(function(user, done) { - done(null, user); - }); +// ------------ Passport Configuration ------------ +var passport = require('passport') +var util = require('util') +var FacebookStrategy = require('passport-facebook').Strategy; +// Passport session setup. +// To support persistent login sessions, Passport needs to be able to +// serialize users into and deserialize users out of the session. Typically, +// this will be as simple as storing the user ID when serializing, and finding +// the user by ID when deserializing. However, since this example does not +// have a database of user records, the complete Facebook profile is serialized +// and deserialized. +passport.serializeUser(function(user, done) { + done(null, user); +}); - passport.deserializeUser(function(obj, done) { - done(null, obj); - }); +passport.deserializeUser(function(obj, done) { + done(null, obj); +}); - // Use the FacebookStrategy within Passport. - // Strategies in Passport require a `verify` function, which accept - // credentials (in this case, an accessToken, refreshToken, and Facebook - // profile), and invoke a callback with a user object. - passport.use(new FacebookStrategy({ - clientID: nconf.get("FACEBOOK_KEY"), - clientSecret: nconf.get("FACEBOOK_SECRET"), - callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback" - }, - function(accessToken, refreshToken, profile, done) { - // asynchronous verification, for effect... - //process.nextTick(function () { +// Use the FacebookStrategy within Passport. +// Strategies in Passport require a `verify` function, which accept +// credentials (in this case, an accessToken, refreshToken, and Facebook +// profile), and invoke a callback with a user object. +passport.use(new FacebookStrategy({ + clientID: nconf.get("FACEBOOK_KEY"), + clientSecret: nconf.get("FACEBOOK_SECRET"), + callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback" +}, + function(accessToken, refreshToken, profile, done) { + // asynchronous verification, for effect... + //process.nextTick(function () { - // To keep the example simple, the user's Facebook profile is returned to - // represent the logged-in user. In a typical application, you would want - // to associate the Facebook account with a user record in your database, - // and return that user instead. - return done(null, profile); - //}); - } - )); + // To keep the example simple, the user's Facebook profile is returned to + // represent the logged-in user. In a typical application, you would want + // to associate the Facebook account with a user record in your database, + // and return that user instead. + return done(null, profile); + //}); + } + )); - // ------------ Server Configuration ------------ - app.set("port", nconf.get('PORT')); +// ------------ Server Configuration ------------ +app.set("port", nconf.get('PORT')); - if (!process.env.SUPPRESS) app.use(express.logger("dev")); - app.use(express.compress()); - app.set("views", __dirname + "/../views"); - app.set("view engine", "jade"); - app.use(express.favicon()); - app.use(middleware.cors); - app.use(middleware.forceSSL); - app.use(express.urlencoded()); - app.use(express.json()); - app.use(express.methodOverride()); - //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()); +if (!process.env.SUPPRESS) app.use(express.logger("dev")); +app.use(express.compress()); +app.set("views", __dirname + "/../views"); +app.set("view engine", "jade"); +app.use(express.favicon()); +app.use(middleware.cors); +app.use(middleware.forceSSL); +app.use(express.urlencoded()); +app.use(express.json()); +app.use(express.methodOverride()); +//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()); - // Initialize Passport! Also use passport.session() middleware, to support - // persistent login sessions (recommended). - app.use(passport.initialize()); - app.use(passport.session()); +// 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); +app.use(app.router); - var maxAge = (nconf.get('NODE_ENV') === 'production') ? 31536000000 : 0; - app.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge })); - app.use(express['static'](path.join(__dirname, "/../public"))); +var maxAge = (nconf.get('NODE_ENV') === 'production') ? 31536000000 : 0; +app.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge })); +app.use(express['static'](path.join(__dirname, "/../public"))); - // development only - //if ("development" === app.get("env")) { - // app.use(express.errorHandler()); - //} +// development only +//if ("development" === app.get("env")) { +// app.use(express.errorHandler()); +//} - // Custom Directives - app.use(require('./routes/pages').middleware); - app.use(require('./routes/auth').middleware); - var v2 = express(); - app.use('/api/v2', v2); - app.use('/api/v1', require('./routes/apiv1').middleware); - app.use('/export', require('./routes/dataexport').middleware); +// Custom Directives +app.use(require('./routes/pages').middleware); +app.use(require('./routes/auth').middleware); +var v2 = express(); +app.use('/api/v2', v2); +app.use('/api/v1', require('./routes/apiv1').middleware); +app.use('/export', require('./routes/dataexport').middleware); - app.use(utils.errorHandler); +app.use(utils.errorHandler); - require('./routes/apiv2.coffee')(swagger, v2); +require('./routes/apiv2.coffee')(swagger, v2); - server = http.createServer(app).listen(app.get("port"), function() { - return console.log("Express server listening on port " + app.get("port")); - }); - app.use(domainMiddleware({ - server: server - //killTimeout: 30000, - })); -} +server = http.createServer(app).listen(app.get("port"), function() { + return console.log("Express server listening on port " + app.get("port")); +}); +app.use(domainMiddleware({ + server: server + //killTimeout: 30000, +})); \ No newline at end of file From 5f0cf6575c0dc4cfc041956e3dc27898d8b4242d Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Thu, 30 Jan 2014 18:44:53 +0100 Subject: [PATCH 07/32] fix(settings): remove unnecessary code --- views/options/settings.jade | 87 +------------------------------------ 1 file changed, 1 insertion(+), 86 deletions(-) diff --git a/views/options/settings.jade b/views/options/settings.jade index 2ee1c997b6..3c18cf12a1 100644 --- a/views/options/settings.jade +++ b/views/options/settings.jade @@ -156,89 +156,4 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template') div(style='font-size:22px') i.icon-ok=env.t('subscribed') div(ng-include="'partials/options.settings.subscription.perks.html'") - .btn.btn-small.btn-danger(ng-click='cancelSubscription()')=env.t('cancelSub') - - - //-.well.subscription-features - h2=env.t('adminSub') - - table.table.table-striped - tr - th.feature-name - //th.feature-name.muted Family (Coming Soon) - //th.feature-name.muted Bronze (Coming Soon) - //th.feature-name.muted Silver (Coming Soon) - th.feature-name=env.t('gold') - th.feature-name.muted - !=env.t('morePlans') - tr - th - span.dashed-underline(popover=env.t('organizationSubText'),popover-trigger='mouseenter',popover-placement='right')=env.t('organizationSub') - //td(ng-include="'partials/feature-matrix-check.html'") - //td(ng-include="'partials/feature-matrix-check.html'") - //td(ng-include="'partials/feature-matrix-check.html'") - td(ng-include="'partials/feature-matrix-check.html'") - td - tr - th - span.dashed-underline(popover=env.t('hostingTypeText'),popover-trigger='mouseenter',popover-placement='right')=env.t('hostingType') - //td Shared - //td Shared - //td Shared - td=env.t('dedicated') - td - - tr - th - span.dashed-underline(popover=env.t('customDomainText'),popover-trigger='mouseenter',popover-placement='right')=env.t('customDomain') - //td.muted - //td.muted - //td.muted - td(ng-include="'partials/feature-matrix-check.html'") - td - tr - th - span.dashed-underline(popover=env.t('maxPlayersText'),popover-trigger='mouseenter',popover-placement='right')=env.t('maxPlayers') - //td.muted 10 - //td.muted 50 - //td.muted 150 - td=env.t('unlimited') - td - tr - th - span.dashed-underline(popover=env.t('priSupportText'),popover-trigger='mouseenter',popover-placement='right')=env.t('priSupport') - //td.muted - //td.muted - //td.muted - td(ng-include="'partials/feature-matrix-check.html'") - td - tr - th - span.dashed-underline(popover=env.t('timeSupportText'),popover-trigger='mouseenter',popover-placement='right')=env.t('timeSupport') - //td.muted 10 - //td.muted 20 - //td.muted 30 - td=env.t('unlimited') - td - tr - th - h5=env.t('gameFeatures') - ul(style='margin:0 0 10px 25px;') - li=env.t('gameNoAds') - li - span.dashed-underline(popover=env.t('gold2GemText'),popover-trigger='mouseenter',popover-placement='right')=env.t('gold2Gem') - li - span.dashed-underline(popover=env.t('infiniteGemText'),popover-trigger='mouseenter',popover-placement='right')=env.t('infiniteGem') - //td(ng-include="'partials/feature-matrix-check.html'") - //td(ng-include="'partials/feature-matrix-check.html'") - //td(ng-include="'partials/feature-matrix-check.html'") - td(ng-include="'partials/feature-matrix-check.html'") - td - tr - th=env.t('price') - //td.alert.alert-info ??? USD / Month - //td.alert.alert-info ??? USD / Month - //td.alert.alert-info ??? USD / Month - td.alert.alert-info ???  - =env.t('monthUSD') - td \ No newline at end of file + .btn.btn-small.btn-danger(ng-click='cancelSubscription()')=env.t('cancelSub') \ No newline at end of file From 3660f1a85c1447de118f334a145d0d7698b93981 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 10:51:32 -0700 Subject: [PATCH 08/32] feat(premium): backport server code for premium subs (it's just ccard handling & uer model stuff) --- src/controllers/user.js | 67 +++++++++++++++++++++++++----- src/models/user.js | 7 ++++ src/routes/apiv2.coffee | 5 +++ views/shared/modals/new-stuff.jade | 2 +- 4 files changed, 70 insertions(+), 11 deletions(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index 5174d6f955..063aa5cceb 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -166,6 +166,9 @@ acceptablePUTPaths = _.reduce(require('./../models/user').schema.paths, function if (found) m[leaf]=true; return m; }, {}) +_.each('stats.gp'.split(' '), function(removePath){ + delete acceptablePUTPaths[removePath]; +}) /** * Update user @@ -257,6 +260,8 @@ api.addTenGems = function(req, res) { }) } +// TODO delete plan + /* Setup Stripe response when posting payment */ @@ -264,20 +269,39 @@ api.buyGems = function(req, res) { var api_key = nconf.get('STRIPE_API_KEY'); var stripe = require("stripe")(api_key); var token = req.body.id; - // console.dir {token:token, req:req}, 'stripe' + var user = res.locals.user; async.waterfall([ function(cb){ - stripe.charges.create({ - amount: "500", // $5 - currency: "usd", - card: token - }, cb); + if (req.query.plan) { + stripe.customers.create({ + email: req.body.email, + metadata: {uuid: res.locals.user._id}, + card: token, + plan: req.query.plan, + }, cb); + } else { + stripe.charges.create({ + amount: "500", // $5 + currency: "usd", + card: token + }, cb); + } }, function(response, cb) { - res.locals.user.balance += 5; - res.locals.user.purchased.ads = true; - res.locals.user.save(cb); + //user.purchased.ads = true; + if (req.query.plan) { + user.purchased.plan = { + planId:'basic_earned', + customerId: response.id, + dateCreated: new Date, + dateUpdated: new Date, + gemsBought: 0 + }; + } else { + user.balance += 5; + } + user.save(cb); } ], function(err, saved){ if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors @@ -285,6 +309,29 @@ api.buyGems = function(req, res) { }); }; +api.cancelSubscription = function(req, res) { + var api_key = nconf.get('STRIPE_API_KEY'); + var stripe = require("stripe")(api_key); + var user = res.locals.user; + if (!user.purchased.plan.customerId) + return res.json(401, {err: "User does not have a plan subscription"}); + + async.waterfall([ + function(cb) { + stripe.customers.del(user.purchased.plan.customerId, cb); + }, + function(response, cb) { + user.purchased.plan = {}; + user.markModified('purchased.plan'); + user.save(cb); + } + ], function(err, saved){ + if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors + res.send(200, saved); + }); + +} + api.buyGemsPaypalIPN = function(req, res, next) { res.send(200); ipn.verify(req.body, function callback(err, msg) { @@ -298,7 +345,7 @@ api.buyGemsPaypalIPN = function(req, res, next) { if (_.isEmpty(user)) err = "user not found with uuid " + uuid + " when completing paypal transaction"; if (err) return nex(err); user.balance += 5; - user.purchased.ads = true; + //user.purchased.ads = true; user.save(); console.log('PayPal transaction completed and user updated'); }); diff --git a/src/models/user.js b/src/models/user.js index ef8a8b3e18..391810fb54 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -78,6 +78,13 @@ var UserSchema = new Schema({ skin: {type: Schema.Types.Mixed, 'default': {}}, // eg, {skeleton: true, pumpkin: true, eb052b: true} hair: {type: Schema.Types.Mixed, 'default': {}}, shirt: {type: Schema.Types.Mixed, 'default': {}}, + plan: { + planId: String, + customerId: String, + dateCreated: Date, + dateUpdated: Date, + gemsBought: {type: Number, 'default': 0} + } }, flags: { diff --git a/src/routes/apiv2.coffee b/src/routes/apiv2.coffee index 2d8bc073ab..513fb7e312 100644 --- a/src/routes/apiv2.coffee +++ b/src/routes/apiv2.coffee @@ -304,6 +304,11 @@ module.exports = (swagger, v2) -> middleware: auth.auth action:user.buyGems + "/user/cancel-subscription": + spec: method: 'POST', description: "Do not use this route" + middleware: auth.auth + action:user.cancelSubscription + "/user/buy-gems/paypal-ipn": spec: method: 'POST' diff --git a/views/shared/modals/new-stuff.jade b/views/shared/modals/new-stuff.jade index 096590cbce..9eb471415b 100644 --- a/views/shared/modals/new-stuff.jade +++ b/views/shared/modals/new-stuff.jade @@ -42,7 +42,7 @@ div(modal='modals.newStuff') tr td h5 Spread The Word Challenge Update - p We have 2k+ submissions, holy cow! Great job everyone! Now, we need to go through these manually, so it will take a few days to a couple weeks to process. The challenge will stay open until we're done choosing our winners, but be sure to edit the To-Do with your submission URL before 1/31, as that's the cut-off date for processing. We'll send a Tweet out when the winner has been selected, so follow @habitrpg and stay tuned. + p We have 1k+ submissions, holy cow! Great job everyone! Now, we need to go through these manually, so it will take a few days to a couple weeks to process. The challenge will stay open until we're done choosing our winners, but be sure to edit the To-Do with your submission URL before 1/31, as that's the cut-off date for processing. We'll send a Tweet out when the winner has been selected, so follow @habitrpg and stay tuned. From 5b0639287999b366d19cfde158d320f26b4d501e Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 10:55:28 -0700 Subject: [PATCH 09/32] chore(newrelic): add newrelic --- .gitignore | 1 + .nodemonignore | 1 + newrelic.js | 25 +++++++++++++++++++++++++ package.json | 3 ++- src/server.js | 2 ++ 5 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 newrelic.js diff --git a/.gitignore b/.gitignore index a7d8e7d2e6..ef989eaea0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ npm-debug.log lib public/bower_components build +newrelic_agent.log src/*/*.map src/*/*/*.map diff --git a/.nodemonignore b/.nodemonignore index 216c52f8b0..f4e0b53926 100644 --- a/.nodemonignore +++ b/.nodemonignore @@ -6,3 +6,4 @@ Gruntfile.js CHANGELOG.md .idea* .git* +newrelic_agent.log diff --git a/newrelic.js b/newrelic.js new file mode 100644 index 0000000000..48127ad3bc --- /dev/null +++ b/newrelic.js @@ -0,0 +1,25 @@ +/** + * New Relic agent configuration. + * + * See lib/config.defaults.js in the agent distribution for a more complete + * description of configuration variables and their potential values. + */ +var nconf = require('nconf') +exports.config = { + /** + * Array of application names. + */ + app_name : ['HabitRPG'], + /** + * Your New Relic license key. + */ + license_key : nconf.get('NEWRELIC'), + logging : { + /** + * Level at which to log. 'trace' is most useful to New Relic when diagnosing + * issues with the agent, 'info' and higher will impose the least overhead on + * production applications. + */ + level : 'trace' + } +}; diff --git a/package.json b/package.json index 2ecac913bc..9e3e9a52d4 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "domain-middleware": "~0.1.0", "swagger-node-express": "git://github.com/lefnire/swagger-node-express#habitrpg", "passport": "~0.1.18", - "passport-facebook": "~1.0.2" + "passport-facebook": "~1.0.2", + "newrelic": "~1.3.0" }, "private": true, "subdomain": "habitrpg", diff --git a/src/server.js b/src/server.js index a5013dcc44..5ad08abe4b 100644 --- a/src/server.js +++ b/src/server.js @@ -17,6 +17,8 @@ var server; // ------------ Setup configurations ------------ utils.setupConfig(); +if (nconf.get('NODE_ENV') === 'production') + require('newrelic'); // ------------ MongoDB Configuration ------------ mongoose = require('mongoose'); From 2d4d103ca6104d7d60ac355f42269db2b5a7db74 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 10:56:40 -0700 Subject: [PATCH 10/32] chore(bower): strange temporary fix. Can't remember where I picked this up, but I was having issues deploying to prod with bower referencing a private repo (using github oauth key, see private#premium) --- .bowerrc | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.bowerrc b/.bowerrc index 9c9ea40600..20772d5d24 100644 --- a/.bowerrc +++ b/.bowerrc @@ -1,3 +1,8 @@ { - "directory": "public/bower_components" + "directory": "public/bower_components", + "storage": { + "packages": ".bower-cache", + "registry": ".bower-registry" + }, + "tmp": ".bower-tmp" } \ No newline at end of file From 85e2ce5c4a10068ab2d2879968d08c8e52ee1769 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 11:04:17 -0700 Subject: [PATCH 11/32] chore(logging): don't log in production --- src/server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server.js b/src/server.js index 5ad08abe4b..0dec3210ac 100644 --- a/src/server.js +++ b/src/server.js @@ -76,7 +76,8 @@ passport.use(new FacebookStrategy({ // ------------ Server Configuration ------------ app.set("port", nconf.get('PORT')); -if (!process.env.SUPPRESS) app.use(express.logger("dev")); +if (nconf.get('NODE_ENV') !== 'production') + app.use(express.logger("dev")); app.use(express.compress()); app.set("views", __dirname + "/../views"); app.set("view engine", "jade"); From 416feca42e6c9d2dd42a66701bbf2a0ab0374a3e Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 11:52:58 -0700 Subject: [PATCH 12/32] chore(newrelic): use license key in accordance with https://docs.newrelic.com/docs/nodejs/nodejs-agent-on-heroku --- config.json.example | 3 ++- newrelic.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config.json.example b/config.json.example index 5cf560aae6..634d22c7a7 100644 --- a/config.json.example +++ b/config.json.example @@ -13,5 +13,6 @@ "SMTP_SERVICE":"Gmail", "STRIPE_API_KEY":"aaaabbbbccccddddeeeeffff00001111", "STRIPE_PUB_KEY":"22223333444455556666777788889999", - "PAYPAL_MERCHANT":"paypal-merchant@gmail.com" + "PAYPAL_MERCHANT":"paypal-merchant@gmail.com", + "NEW_RELIC_LICENSE_KEY":"NEW_RELIC_LICENSE_KEY" } diff --git a/newrelic.js b/newrelic.js index 48127ad3bc..7763ba2269 100644 --- a/newrelic.js +++ b/newrelic.js @@ -13,7 +13,7 @@ exports.config = { /** * Your New Relic license key. */ - license_key : nconf.get('NEWRELIC'), + license_key : nconf.get('NEW_RELIC_LICENSE_KEY'), logging : { /** * Level at which to log. 'trace' is most useful to New Relic when diagnosing From 44d9ea2a1511606c761999ef71e65c38fec2543c Mon Sep 17 00:00:00 2001 From: Marcelo Sizer Date: Thu, 30 Jan 2014 18:25:25 -0200 Subject: [PATCH 13/32] i18n(interpolation): Add string interpolation --- views/options/inventory/inventory.jade | 21 +----- views/options/inventory/stable.jade | 2 +- views/options/profile.jade | 100 ++++++++++++------------- views/options/settings.jade | 80 ++++++++++---------- views/options/social/challenges.jade | 4 +- views/options/social/group.jade | 10 +-- views/options/social/hall.jade | 4 +- views/options/social/tavern.jade | 25 ++++--- views/shared/header/avatar.jade | 2 +- views/shared/modals/achievements.jade | 13 +--- views/shared/modals/buy-gems.jade | 5 +- views/shared/modals/settings.jade | 4 +- views/shared/tasks/task.jade | 4 +- views/static/about.jade | 2 +- views/static/contact.jade | 2 +- views/static/layout.jade | 10 +-- views/static/plans.jade | 59 ++++++++------- 17 files changed, 163 insertions(+), 184 deletions(-) diff --git a/views/options/inventory/inventory.jade b/views/options/inventory/inventory.jade index cc0c8d0d56..ee228041dc 100644 --- a/views/options/inventory/inventory.jade +++ b/views/options/inventory/inventory.jade @@ -78,24 +78,9 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html') .popover-content p=env.t('welcomeMarket') p - button.btn.btn-primary(ng-show='selectedEgg', ng-click='sellInventory()') - =env.t('sell') - |  {{selectedEgg.text}}  - =env.t('for') - |  {{selectedEgg.value}}  - =env.t('gold') - button.btn.btn-primary(ng-show='selectedPotion', ng-click='sellInventory()') - =env.t('sell') - |  {{selectedPotion.text}}  - =env.t('for') - |  {{selectedPotion.value}}  - =env.t('gold') - button.btn.btn-primary(ng-show='selectedFood', ng-click='sellInventory()') - =env.t('sell') - |  {{selectedFood.text}}  - =env.t('for') - |  {{selectedFood.value}}  - =env.t('gold') + button.btn.btn-primary(ng-show='selectedEgg', ng-click='sellInventory()')=env.t('sellForGold', {item: "{{selectedEgg.text}}", gold: "{{selectedEgg.value}}"}) + button.btn.btn-primary(ng-show='selectedPotion', ng-click='sellInventory()')=env.t('sellForGold', {item: "{{selectedPotion.text}}", gold: "{{selectedPotion.value}}"}) + button.btn.btn-primary(ng-show='selectedFood', ng-click='sellInventory()')=env.t('sellForGold', {item: "{{selectedFood.text}}", gold: "{{selectedFood.value}}"}) menu.inventory-list(type='list') li.customize-menu menu.pets-menu(label=env.t('eggs')) diff --git a/views/options/inventory/stable.jade b/views/options/inventory/stable.jade index 457881e42c..328be90ebb 100644 --- a/views/options/inventory/stable.jade +++ b/views/options/inventory/stable.jade @@ -10,7 +10,7 @@ script(type='text/ng-template', id='partials/options.inventory.mounts.html') h3.popover-title a(target='_blank', href='http://www.kickstarter.com/profile/mattboch')=env.t('mattBoch') .popover-content - p=env.t('mattShall1') + ' {{user.profile.name}} ' + env.t('mattShall2') + p=env.t('mattShall', {name: "{{user.profile.name}}"}) h4= '{{mountCount}} / {{totalPets}} ' + env.t('mountsTamed') menu.pets(type='list') li.customize-menu(ng-repeat='egg in Content.eggs') diff --git a/views/options/profile.jade b/views/options/profile.jade index 36eec2f599..d1d730b738 100644 --- a/views/options/profile.jade +++ b/views/options/profile.jade @@ -1,15 +1,15 @@ script(id='partials/options.profile.avatar.html', type='text/ng-template') .row-fluid .span4 - h3=env.t('bodybody') + h3=env.t('bodyBody') small | 2 / = ' ' + env.t('locked') - h5=env.t('bodysize') + h5=env.t('bodySize') .btn-group - button.btn.btn-small(ng-class='{active: user.preferences.size=="slim"}', ng-click='set({"preferences.size":"slim"})')=env.t('bodyslim') - button.btn.btn-small(ng-class='{active: user.preferences.size=="broad"}', ng-click='set({"preferences.size":"broad"})')=env.t('bodybroad') + button.btn.btn-small(ng-class='{active: user.preferences.size=="slim"}', ng-click='set({"preferences.size":"slim"})')=env.t('bodySlim') + button.btn.btn-small(ng-class='{active: user.preferences.size=="broad"}', ng-click='set({"preferences.size":"broad"})')=env.t('bodyBroad') menu(type='list') li.customize-menu @@ -17,15 +17,15 @@ script(id='partials/options.profile.avatar.html', type='text/ng-template') each shirt in ['black', 'blue', 'green', 'pink', 'white', 'yellow'] button.customize-option(class='{{user.preferences.size}}_shirt_'+shirt, type='button', ng-click='set({"preferences.shirt":"'+shirt+'"})') - menu(label=env.t('specialshirts')) + menu(label=env.t('specialShirts')) each shirt in ['convict', 'cross', 'fire', 'horizon', 'ocean', 'purple', 'rainbow', 'redblue', 'thunder', 'tropical', 'zombie'] button.customize-option(type='button', class='{{user.preferences.size}}_shirt_'+shirt, ng-class='{locked: !user.purchased.shirt.'+shirt+'}', ng-click='unlock("shirt.'+shirt+'")') menu - button.btn.btn-small.btn-primary(ng-hide="user.purchased.shirt.convict && user.purchased.shirt.cross && user.purchased.shirt.fire && user.purchased.shirt.horizon && user.purchased.shirt.ocean && user.purchased.shirt.purple && user.purchased.shirt.rainbow && user.purchased.shirt.redblue && user.purchased.shirt.thunder && user.purchased.shirt.tropical && user.purchased.shirt.zombie", ng-click='unlock("shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie")')!= env.t('unlockset5') + ' ' + button.btn.btn-small.btn-primary(ng-hide="user.purchased.shirt.convict && user.purchased.shirt.cross && user.purchased.shirt.fire && user.purchased.shirt.horizon && user.purchased.shirt.ocean && user.purchased.shirt.purple && user.purchased.shirt.rainbow && user.purchased.shirt.redblue && user.purchased.shirt.thunder && user.purchased.shirt.tropical && user.purchased.shirt.zombie", ng-click='unlock("shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie")')!= env.t('unlockSet5') + ' ' .span4 - h3=env.t('bodyhead') + h3=env.t('bodyHead') small | 2 / = ' ' + env.t('locked') @@ -38,35 +38,35 @@ script(id='partials/options.profile.avatar.html', type='text/ng-template') // Special Events li.customize-menu.well.limited-edition - .label.label-info.pull-right(popover=env.t('limited31Jan'), popover-title=env.t('limitededition'), popover-placement='right', popover-trigger='mouseenter') - =env.t('limitededition') + .label.label-info.pull-right(popover=env.t('limited31Jan'), popover-title=env.t('limitedEdition'), popover-placement='right', popover-trigger='mouseenter') + =env.t('limitedEdition') | i.icon.icon-question-sign - menu(label=env.t('wintercolors')) + menu(label=env.t('winterColors')) each color in ['candycane','frost','winternight','holly'] button(type='button', ng-class='{locked: !user.purchased.hair.color.#{color}}', class='customize-option hair_bangs_1_#{color}', style='width: 40px; height: 40px;', ng-click='unlock("hair.color.#{color}")') - button.btn.btn-small.btn-primary(ng-hide='user.purchased.hair.color.candycane && user.purchased.hair.color.frost && user.purchased.hair.color.winternight && user.purchased.hair.color.holly', ng-click='unlock("hair.color.candycane,hair.color.frost,hair.color.winternight,hair.color.holly")')!= env.t('unlockset5') + ' ' + button.btn.btn-small.btn-primary(ng-hide='user.purchased.hair.color.candycane && user.purchased.hair.color.frost && user.purchased.hair.color.winternight && user.purchased.hair.color.holly', ng-click='unlock("hair.color.candycane,hair.color.frost,hair.color.winternight,hair.color.holly")')!= env.t('unlockSet5') + ' ' - h5=env.t('bodyhair') + h5=env.t('bodyHair') // Bangs li.customize-menu - menu(label=env.t('hairbangs')) + menu(label=env.t('hairBangs')) button(class='head_0 customize-option', type='button', ng-click='set({"preferences.hair.bangs":0})') each num in [1,2,3] button(class='hair_bangs_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-click='set({"preferences.hair.bangs":#{num}})') // Base li.customize-menu - menu(label=env.t('hairbase')) + menu(label=env.t('hairBase')) button(class='head_0 customize-option', type='button', ng-click='set({"preferences.hair.base":0})') each v,k in {1:true,2:false,3:true,4:false,5:false,6:false,7:false,8:false} case v when true: button(class='hair_base_#{k}_{{user.preferences.hair.color}} customize-option', type='button', ng-click='set({"preferences.hair.base":#{k}})') when false: button(class='hair_base_#{k}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.base.#{k}}', ng-click='unlock("hair.base.#{k}")') - button.btn.btn-small.btn-primary(ng-hide='user.purchased.hair.base.2 && user.purchased.hair.base.4 && user.purchased.hair.base.5 && user.purchased.hair.base.6 && user.purchased.hair.base.7 && user.purchased.hair.base.8', ng-click='unlock("hair.base.2,hair.base.4,hair.base.5,hair.base.6,hair.base.7,hair.base.8")')!= env.t('unlockset5') + ' ' + button.btn.btn-small.btn-primary(ng-hide='user.purchased.hair.base.2 && user.purchased.hair.base.4 && user.purchased.hair.base.5 && user.purchased.hair.base.6 && user.purchased.hair.base.7 && user.purchased.hair.base.8', ng-click='unlock("hair.base.2,hair.base.4,hair.base.5,hair.base.6,hair.base.7,hair.base.8")')!= env.t('unlockSet5') + ' ' - h5=env.t('bodyfacialhair') + h5=env.t('bodyFacialHair') // Beard li.customize-menu @@ -82,31 +82,31 @@ script(id='partials/options.profile.avatar.html', type='text/ng-template') each num in [1,2] button(class='hair_mustache_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.mustache.#{num}}', ng-click='unlock("hair.mustache.#{num}")') - button.btn.btn-small.btn-primary(ng-hide='user.purchased.hair.mustache.1 && user.purchased.hair.mustache.2 && user.purchased.hair.beard.1 && user.purchased.hair.beard.2 && user.purchased.hair.beard.3', ng-click='unlock("hair.mustache.1,hair.mustache.2,hair.beard.1,hair.beard.2,hair.beard.3")')!= env.t('unlockset5') + ' ' + button.btn.btn-small.btn-primary(ng-hide='user.purchased.hair.mustache.1 && user.purchased.hair.mustache.2 && user.purchased.hair.beard.1 && user.purchased.hair.beard.2 && user.purchased.hair.beard.3', ng-click='unlock("hair.mustache.1,hair.mustache.2,hair.beard.1,hair.beard.2,hair.beard.3")')!= env.t('unlockSet5') + ' ' .span4 - h3=env.t('bodyskin') + h3=env.t('bodySkin') small | 2 / = ' ' + env.t('locked') // skin li.customize-menu - menu(label=env.t('basicskins')) + menu(label=env.t('basicSkins')) each color in ['ddc994','f5a76e','ea8349','c06534','98461a','915533','c3e1dc','6bd049'] button.customize-option(type='button', class='skin_#{color}', ng-click='set({"preferences.skin":"#{color}"})') // Rainbow Skin - h5=env.t('rainbowskins') + h5=env.t('rainbowSkins') //menu(label='Rainbow Skins (2G / skin)') menu each color in ['eb052b','f69922','f5d70f','0ff591','2b43f6','d7a9f7','800ed0','rainbow'] button.customize-option(type='button', class='skin_#{color}', ng-class='{locked: !user.purchased.skin.#{color}}', ng-click='unlock("skin.#{color}")') - button.btn.btn-small.btn-primary(ng-hide='user.purchased.skin.eb052b && user.purchased.skin.f69922 && user.purchased.skin.f5d70f && user.purchased.skin.0ff591 && user.purchased.skin.2b43f6 && user.purchased.skin.d7a9f7 && user.purchased.skin.800ed0 && user.purchased.skin.rainbow', ng-click='unlock("skin.eb052b,skin.f69922,skin.f5d70f,skin.0ff591,skin.2b43f6,skin.d7a9f7,skin.800ed0,skin.rainbow")')!= env.t('unlockset5') + ' ' + button.btn.btn-small.btn-primary(ng-hide='user.purchased.skin.eb052b && user.purchased.skin.f69922 && user.purchased.skin.f5d70f && user.purchased.skin.0ff591 && user.purchased.skin.2b43f6 && user.purchased.skin.d7a9f7 && user.purchased.skin.800ed0 && user.purchased.skin.rainbow', ng-click='unlock("skin.eb052b,skin.f69922,skin.f5d70f,skin.0ff591,skin.2b43f6,skin.d7a9f7,skin.800ed0,skin.rainbow")')!= env.t('unlockSet5') + ' ' // Special Events // restore to d4df481 to see purchasing + "limited edition" code div(ng-if='user.purchased.skin.monster || user.purchased.skin.pumpkin || user.purchased.skin.skeleton || user.purchased.skin.zombie || user.purchased.skin.ghost || user.purchased.skin.shadow') - h5=env.t('spookyskins') + h5=env.t('spookySkins') menu each color in ['monster','pumpkin','skeleton','zombie','ghost','shadow'] button.customize-option(type='button', class='skin_#{color}', ng-if='user.purchased.skin.#{color}', ng-click='unlock("skin.#{color}")') @@ -116,11 +116,11 @@ script(id='partials/options.profile.stats.html', type='text/ng-template') .border-right(ng-class='user.flags.classSelected && !user.preferences.disableClasses ? "span4" : "span6"') include ../shared/profiles/stats .span4.border-right.allocate-stats(ng-if='user.flags.classSelected && !user.preferences.disableClasses') - h3=env.t('characterbuild') + h3=env.t('characterBuild') h4 =env.t('class') + ': ' span {{ {warrior:'Warrior',wizard:'Mage',healer:'Healer',rogue:'Rogue'}[user.stats.class] }}  - a.btn.btn-danger.btn-mini(ng-click='changeClass(null)')=env.t('changeclass') + a.btn.btn-danger.btn-mini(ng-click='changeClass(null)')=env.t('changeClass') small 3 table.table.table-striped tr @@ -129,48 +129,48 @@ script(id='partials/options.profile.stats.html', type='text/ng-template') {{user.stats.points}}  =env.t('unallocated')   - i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('levelpopover')) + i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('levelPopover')) td tr td(colspan=2) fieldset.auto-allocate label.checkbox input(type='checkbox', ng-model='user.preferences.automaticAllocation', ng-change='set({"preferences.automaticAllocation": user.preferences.automaticAllocation?true: false})', ng-click='set({"preferences.allocationMode":"taskbased"})') - =env.t('automaticallocation') + =env.t('autoAllocation')   - i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('automaticallocationpopover')) + i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('autoAllocationPop')) form(ng-show='user.preferences.automaticAllocation',style='margin-left:1em') label.radio input(type='radio', name='allocationMode', value='flat', ng-model='user.preferences.allocationMode', ng-change='set({"preferences.allocationMode": "flat"})') - =env.t('evenallocation') + =env.t('evenAllocation')   - i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('evenallocationpopover')) + i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('evenAllocationPop')) label.radio input(type='radio', name='allocationMode', value='classbased', ng-model='user.preferences.allocationMode', ng-change='set({"preferences.allocationMode": "classbased"})') - =env.t('classallocation') + =env.t('classAllocation')   - i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('classallocationpopover')) + i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('classAllocationPop')) label.radio input(type='radio', name='allocationMode', value='taskbased', ng-model='user.preferences.allocationMode', ng-change='set({"preferences.allocationMode": "taskbased"})') - =env.t('taskallocation') + =env.t('taskAllocation')   - i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('taskallocationpopover')) + i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('taskAllocationPop')) tr - td= env.t('allocatestr') + ' {{user.stats.str}}' + td= env.t('allocateStr') + ' {{user.stats.str}}' td - a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("str")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocatestrpop')) + + a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("str")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocateStrPop')) + tr - td= env.t('allocatecon') + ' {{user.stats.con}}' + td= env.t('allocateCon') + ' {{user.stats.con}}' td - a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("con")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocateconpop')) + + a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("con")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocateConPop')) + tr - td= env.t('allocateper') + ' {{user.stats.per}}' + td= env.t('allocatePer') + ' {{user.stats.per}}' td - a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("per")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocateperpop')) + + a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("per")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocatePerPop')) + tr - td= env.t('allocateint') + ' {{user.stats.int}}' + td= env.t('allocateInt') + ' {{user.stats.int}}' td - a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("int")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocateintpop')) + + a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("int")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocateIntPop')) + div(ng-class='user.flags.classSelected && !user.preferences.disableClasses ? "span4" : "span6"') include ../shared/profiles/achievements @@ -178,19 +178,19 @@ script(id='partials/options.profile.profile.html', type='text/ng-template') button.btn.btn-default(ng-click='_editing.profile = true', ng-show='!_editing.profile')= env.t('edit') button.btn.btn-primary(ng-click='save()', ng-show='_editing.profile')= env.t('save') div(ng-show='!_editing.profile') - h4=env.t('displayname') + h4=env.t('displayName') span(ng-show='profile.profile.name') {{profile.profile.name}} span.muted(ng-hide='profile.profile.name') -  =env.t('none') |  - - h4=env.t('displayphoto') + h4=env.t('displayPhoto') img(ng-show='profile.profile.imageUrl', ng-src='{{profile.profile.imageUrl}}') span.muted(ng-hide='profile.profile.imageUrl') -  =env.t('none') |  - - h4=env.t('displayblurb') + h4=env.t('displayBlurb') markdown(ng-show='profile.profile.blurb', ng-model='profile.profile.blurb') span.muted(ng-hide='profile.profile.blurb') -  =env.t('none') @@ -200,14 +200,14 @@ script(id='partials/options.profile.profile.html', type='text/ng-template') div.whatever-options(ng-show='_editing.profile') // TODO use photo-upload instead: https://groups.google.com/forum/?fromgroups=#!topic/derbyjs/xMmADvxBOak .control-group.option-large - label.control-label=env.t('displayname') + label.control-label=env.t('displayName') input.option-content(type='text', placeholder=env.t('fullName'), ng-model='editingProfile.name') .control-group.option-large - label.control-label=env.t('photourl') - input.option-content(type='url', ng-model='editingProfile.imageUrl', placeholder=env.t('imageurl')) + label.control-label=env.t('photoUrl') + input.option-content(type='url', ng-model='editingProfile.imageUrl', placeholder=env.t('imageUrl')) .control-group.option-large - label.control-label=env.t('displayblurb') - textarea.option-content(style='height:15em;', placeholder=env.t("displayblurb"), ng-model='editingProfile.blurb') + label.control-label=env.t('displayBlurb') + textarea.option-content(style='height:15em;', placeholder=env.t('displayBlurb'), ng-model='editingProfile.blurb') include ../shared/formatting-help script(id='partials/options.profile.html', type="text/ng-template") @@ -217,7 +217,7 @@ script(id='partials/options.profile.html', type="text/ng-template") =env.t('avatar') li(ng-class="{ active: $state.includes('options.profile.stats') }") a(ui-sref='options.profile.stats') - =env.t('statsach') + =env.t('statsAch') li(ng-class="{ active: $state.includes('options.profile.profile') }") a(ui-sref='options.profile.profile') =env.t('profile') diff --git a/views/options/settings.jade b/views/options/settings.jade index 5a330cda07..cd808f4336 100644 --- a/views/options/settings.jade +++ b/views/options/settings.jade @@ -8,10 +8,10 @@ script(id='partials/options.settings.html', type="text/ng-template") =env.t('API') li(ng-class="{ active: $state.includes('options.settings.export') }") a(ui-sref='options.settings.export') - =env.t('dataexport') + =env.t('dataExport') li(ng-class="{ active: $state.includes('options.settings.subscription') }") a(ui-sref='options.settings.subscription') - | Subscription + =env.t('subscription') .tab-content .tab-pane.active @@ -28,7 +28,10 @@ script(type='text/ng-template', id='partials/options.settings.settings.html') div small div=env.t('clockInfo') - .alert.alert-danger Warning: this is a highly-experimental feature, and many experience issues with it. + .alert.alert-danger + =env.t('subWarning1') + a(href='https://github.com/HabitRPG/habitrpg/issues/1057' target='_blank')=env.t('subWarning2') + =env.t('subWarning3') hr h4=env.t('language') select(ng-model='language.code', ng-options='lang.code as lang.name for lang in avalaibleLanguages', ng-change='changeLanguage()') @@ -38,36 +41,36 @@ script(type='text/ng-template', id='partials/options.settings.settings.html') input(type='checkbox', ng-click='set({"preferences.hideHeader":user.preferences.hideHeader?false:true})', ng-checked='user.preferences.hideHeader!==true') =env.t('showHeader')   - i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('showHeaderpop')) + i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('showHeaderPop')) label.checkbox input(type='checkbox', ng-click='toggleStickyHeader()', ng-checked='user.preferences.stickyHeader!==false') =env.t('stickyHeader')   - i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('stickyHeaderpop')) + i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('stickyHeaderPop')) label.checkbox input(type='checkbox', ng-model='user.preferences.newTaskEdit', ng-change='set({"preferences.newTaskEdit": user.preferences.newTaskEdit?true: false})') - =env.t('newtaskedit') + =env.t('newTaskEdit')   - i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('newtaskeditpop')) + i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('newTaskEditPop')) label.checkbox input(type='checkbox', ng-model='user.preferences.tagsCollapsed', ng-change='set({"preferences.tagsCollapsed": user.preferences.tagsCollapsed?true: false})') - =env.t('startcollapsed') + =env.t('startCollapsed')   - i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('startcollapsedpop')) + i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('startCollapsedPop')) label.checkbox input(type='checkbox', ng-model='user.preferences.advancedCollapsed', ng-change='set({"preferences.advancedCollapsed": user.preferences.advancedCollapsed?true: false})') - =env.t('startacollapsed') + =env.t('startAdvCollapsed')   - i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('startacollapsedpop')) - button.btn(ng-click='showTour()', popover-placement='right', popover-trigger='mouseenter', popover=env.t('showtourpop1'))= env.t('showtour') + i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('startAdvCollapsedPop')) + button.btn(ng-click='showTour()', popover-placement='right', popover-trigger='mouseenter', popover=env.t('restartTour'))= env.t('showTour') br - button.btn(ng-click='showBailey()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('showtourpop'))= env.t('showtour2') + button.btn(ng-click='showBailey()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('showBaileyPop'))= env.t('showBailey') br - button.btn(ng-click='modals.restore = true', popover-trigger='mouseenter', popover-placement='right', popover=env.t('fixvalpop'))= env.t('fixval') + button.btn(ng-click='modals.restore = true', popover-trigger='mouseenter', popover-placement='right', popover=env.t('fixValPop'))= env.t('fixVal') div(ng-if='user.preferences.disableClasses==true') - button.btn(ng-click='user.ops.changeClass({})', popover-trigger='mouseenter', popover-placement='right', popover=env.t('enableclasspop'))= env.t('enableclass') + button.btn(ng-click='user.ops.changeClass({})', popover-trigger='mouseenter', popover-placement='right', popover=env.t('enableClassPop'))= env.t('enableClass') div(ng-if='!user.preferences.disableClasses && user.flags.classSelected') - button.btn(ng-click='showClassesTour()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('classtourpop'))= env.t('showclass') + button.btn(ng-click='showClassesTour()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('classTourPop'))= env.t('showClass') //- Why is ng-if='user.auth.local' validating for users *without* user.auth.local (facebook users)? adding .username here for extra div(ng-if='user.auth.local.username') @@ -84,8 +87,8 @@ script(type='text/ng-template', id='partials/options.settings.settings.html') hr h4=env.t('dangerZone') - a.btn.btn-danger(ng-click='modals.reset = true', popover-trigger='mouseenter', popover-placement='right', popover=env.t('resetaccpop'))= env.t('resetaccount') - a.btn.btn-danger(ng-click='modals.delete = true', popover-trigger='mouseenter', popover=env.t('deleteaccpop'))= env.t('deleteaccount') + a.btn.btn-danger(ng-click='modals.reset = true', popover-trigger='mouseenter', popover-placement='right', popover=env.t('resetAccPop'))= env.t('resetAccount') + a.btn.btn-danger(ng-click='modals.delete = true', popover-trigger='mouseenter', popover=env.t('deleteAccPop'))= env.t('deleteAccount') script(type='text/ng-template', id='partials/options.settings.api.html') .row.fluid @@ -96,19 +99,19 @@ script(type='text/ng-template', id='partials/options.settings.api.html') pre.prettyprint {{user.id}} h6=env.t('APIToken') pre.prettyprint {{user.apiToken}} - h6=env.t('qrcode') + h6=env.t('qrCode') img(src='https://chart.googleapis.com/chart?cht=qr&chs=200x200&chl=%7B%22address%22%3A%22https%3A%2F%2Fhabitrpg.com%22%2C%22user%22%3A%22{{user.id}}%22%2C%22key%22%3A%22{{user.apiToken}}%22%7D&choe=UTF-8&chld=L', alt='qrcode') script(id='partials/options.settings.export.html', type="text/ng-template") .row.fluid .span6 - h2=env.t('dataexport') - small=env.t('dataexports') - h4=env.t('habithistory') - =env.t('exporthistory') + h2=env.t('dataExport') + small=env.t('saveData') + h4=env.t('habitHistory') + =env.t('exportHistory') a(href="/export/history.csv")= ' ' + env.t('csv') - h4=env.t('userdata') - =env.t('exportuserdata') + h4=env.t('userData') + =env.t('exportUserData') a(href="/export/userdata.xml")= ' ' + env.t('xml') + ' ' a(href="/export/userdata.json")= env.t('json') @@ -116,28 +119,24 @@ script(id='partials/options.settings.subscription.perks.html',type='text/ng-temp table.table.table-striped tr td - span.dashed-underline(popover="Ads will stay disabled while you have an active account (original users with disabled ads are grandfathered).",popover-trigger='mouseenter',popover-placement='right') - | Disable ads + span.dashed-underline(popover=env.t('disableAdsText'),popover-trigger='mouseenter',popover-placement='right')=env.t('disableAds') tr td - span.dashed-underline(popover="(1 Gem costs {{Shared.planGemLimits.convRate}} Gold) Addresses the \"pay to win\" concern, as everything is now achievable through hard work. There's a {{Shared.planGemLimits.convCap}}G monthly conversion cap to prevent farming.",popover-trigger='mouseenter',popover-placement='right') - | Buy Gems with Gold + span.dashed-underline(popover=env.t('buyGemsGoldText', {gemCost: "{{Shared.planGemLimits.convRate}}", gemLimit: "{{Shared.planGemLimits.convCap}}"}),popover-trigger='mouseenter',popover-placement='right')=env.t('buyGemsGold') tr td - span.dashed-underline(popover="Makes your full history available in graphs and export. Non-subscriber histories get consolidated for database optimization.",popover-trigger='mouseenter',popover-placement='right') - | Retain full history entries + span.dashed-underline(popover=env.t('retainHistoryText'),popover-trigger='mouseenter',popover-placement='right')=env.t('retainHistory') tr td - span.dashed-underline(popover="Complete your stable faster!",popover-trigger='mouseenter',popover-placement='right') - | Daily drop-caps doubled + span.dashed-underline(popover=env.t('doubleDropsText'),popover-trigger='mouseenter',popover-placement='right')=env.t('doubleDrops') //-tr //- td +20 gems to your account tr td - span.dashed-underline(popover="This open source project can use all the help it can get. Help us keep Habit alive!",popover-trigger='mouseenter',popover-placement='right') - | Supports the developers + span.dashed-underline(popover=env.t('supportDevsText'),popover-trigger='mouseenter',popover-placement='right')=env.t('supportDevs') tr - td.alert.alert-info $5 USD / Month + td.alert.alert-info $5  + =env.t('monthUSD') script(id='partials/feature-matrix-check.html',type='text/ng-template') span.task-checker.action-yesno @@ -147,16 +146,15 @@ script(id='partials/feature-matrix-check.html',type='text/ng-template') script(id='partials/options.settings.subscription.html',type='text/ng-template') .well - h2 Individual Subscription + h2=env.t('individualSub') div(ng-if='!user.purchased.plan.customerId') div(ng-include="'partials/options.settings.subscription.perks.html'") - .btn.btn-primary(ng-click='showStripe(true)') Subscribe + .btn.btn-primary(ng-click='showStripe(true)')=env.t('subscribe') //-small.muted PayPal coming soon. div(ng-if='user.purchased.plan.customerId') .well div(style='font-size:22px') - i.icon-ok - | Subscribed + i.icon-ok=env.t('subscribed') div(ng-include="'partials/options.settings.subscription.perks.html'") - .btn.btn-small.btn-danger(ng-click='cancelSubscription()') Cancel Subscription \ No newline at end of file + .btn.btn-small.btn-danger(ng-click='cancelSubscription()')=env.t('cancelSub') \ No newline at end of file diff --git a/views/options/social/challenges.jade b/views/options/social/challenges.jade index fe2769b6a1..ae90ff588e 100644 --- a/views/options/social/challenges.jade +++ b/views/options/social/challenges.jade @@ -1,6 +1,6 @@ script(type='text/ng-template', id='partials/options.social.challenges.detail.close.html') a.btn.btn-small.btn-danger(ng-click="delete(closingChal)")=env.t('delete') - h5=env.t('minusOr') + h5= '- ' + env.t('or') + ' -' select(ui-select2, ng-required=true, ng-model='closingChal.winner', data-placeholder=env.t('selectWinner'), ng-change='selectWinner(closingChal)', ) option(value='') option(ng-repeat='u in closingChal.members', value='{{u._id}}') {{u.profile.name}} @@ -111,7 +111,7 @@ script(type='text/ng-template', id='partials/options.social.challenges.html') span.input-suffix.Pet_Currency_Gem1x.inline-gems i.icon-question-sign(popover=env.t('prizePop'), popover-trigger='mouseenter', popover-placement='right') span(ng-show='newChallenge.group=="habitrpg"') - !=env.t('min1Gem') + ' ' + env.t('publicChallenges') + ' ' + env.t('helpsPrevent') + !=env.t('publicChallenges') .option-medium(ng-if='user.contributor.admin') label.checkbox diff --git a/views/options/social/group.jade b/views/options/social/group.jade index 95a9871e43..ca7359a5f2 100644 --- a/views/options/social/group.jade +++ b/views/options/social/group.jade @@ -11,7 +11,7 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title=env.t('guild // ------ Bosses ------- .modal.inline-modal(bindonce='group', ng-if='group.type==="party" && group.quest.key') .modal-header - h3(ng-if='group.quest.active==false')=env.t('questInv') + '{{Content.quests[group.quest.key].text}}' + h3(ng-if='group.quest.active==false')=env.t('questInvitation') + '{{Content.quests[group.quest.key].text}}' h3(ng-if='group.quest.active==true') {{Content.quests[group.quest.key].text}} .modal-body div(ng-if='group.quest.active==false') @@ -61,9 +61,9 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title=env.t('guild div(ng-if='Content.quests[group.quest.key].boss') .npc_ian.pull-left - p!=env.t('bossDmg1') + ' ' + env.t('bossDmg2') + '' + p!=env.t('bossDmg1') br - p=env.t('bossDmg3') + p=env.t('bossDmg2') div(ng-if='Content.quests[group.quest.key].collect') .npc_ian.pull-left @@ -88,7 +88,7 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title=env.t('guild input.option-content(type='text', ng-model='group.name', placeholder=env.t('groupName')) .control-group.option-large label.control-label=env.t('description') - textarea.option-content(style='height:15em;', placeholder=env.t('groupdescr'), ng-model='group.description') + textarea.option-content(style='height:15em;', placeholder=env.t('groupDescr'), ng-model='group.description') .control-group.option-large label.control-label=env.t('logoUrl') input.option-content(type='url', placeholder=env.t('logoUrl'), ng-model='group.logo') @@ -131,7 +131,7 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title=env.t('guild tr(ng-if='group.memberCount > group.members.length') td |{{group.memberCount - group.members.length}} - = ' ' + env.t('moremembers') + = ' ' + env.t('moreMembers') | h4(ng-show='group.invites.length > 0')=env.t('invited') diff --git a/views/options/social/hall.jade b/views/options/social/hall.jade index cfda892f3e..adebd5e3ee 100644 --- a/views/options/social/hall.jade +++ b/views/options/social/hall.jade @@ -42,9 +42,9 @@ script(type='text/ng-template', id='partials/options.social.hall.heroes.html') .control-group.option-medium input.option-content(type='number', step="any", ng-model='hero.balance') - span.input-suffix Balance + span.input-suffix=env.t('balance') p - small!= '`user.balance`' + env.t('USD1') + ' ' + env.t('not') + ' ' + env.t('USD2') + small!= '`user.balance`' + env.t('notGems') .control-group.option-medium label.checkbox diff --git a/views/options/social/tavern.jade b/views/options/social/tavern.jade index 6f30847450..349065f739 100644 --- a/views/options/social/tavern.jade +++ b/views/options/social/tavern.jade @@ -68,35 +68,40 @@ a.label.label-contributor-1(ng-click='toggleUserTier($event)')=env.t('friendBadge') div(style='display:none;') p - != ' ' + env.t('friendText') + ' ' + env.t('first') + ' ' + env.t('friend1Text') + ' 2 ' + env.t('gems') + '.' + span.achievement.achievement-firefox + !=env.t('friendFirst') hr p - != ' ' + env.t('friendText') + ' ' + env.t('second') + ' ' + env.t('friendText2') + ' ' + env.t('crystalArmor') + ' ' + env.t('friendText3') + ' 2 ' + env.t('gems') + '.' + span.shop-sprite.item-img(class='shop_armor_special_1') + !=env.t('friendSecond') tr td a.label.label-contributor-3(ng-click='toggleUserTier($event)')=env.t('eliteBadge') div(style='display:none;') p - != ' ' + env.t('friendText') + ' ' + env.t('third') + ' ' + env.t('friendText2') + ' ' + env.t('crystalHelmet') + ' ' + env.t('friendText3') + ' 2 ' + env.t('gems') + '.' + span.shop-sprite.item-img(class='shop_head_special_1') + !=env.t('eliteThird') hr p - != ' ' + env.t('friendText') + ' ' + env.t('fourth') + ' ' + env.t('friendText2') + ' ' + env.t('crystalSword') + ' ' + env.t('friendText3') + ' 2 ' + env.t('gems') + '.' - + span.shop-sprite.item-img(class='shop_weapon_special_1') + !=env.t('eliteFourth') tr td a.label.label-contributor-5(ng-click='toggleUserTier($event)')=env.t('championBadge') div(style='display:none;') p - != ' ' + env.t('friendText') + ' ' + env.t('fifth') + ' ' + env.t('friendText2') + ' ' + env.t('crystalShield') + ' ' + env.t('friendText3') + ' 2 ' + env.t('gems') + '.' + span.shop-sprite.item-img(class='shop_shield_special_1') + !=env.t('championFifth') hr p - != '
' + env.t('friendText') + ' ' + env.t('sixth') + ' ' + env.t('friendText2b') + ' ' + env.t('hydraPet') + ' ' + env.t('friendText3b') + ' 2 ' + env.t('gems') + '.' + div(class='Pet-Dragon-Hydra pull-left') + !=env.t('championSixth') tr td a.label.label-contributor-7(ng-click='toggleUserTier($event)')=env.t('legendaryBadge') div(style='display:none;') p - != env.t('friendText') + ' ' + env.t('seventh') + ' ' + env.t('friendText2c') + ' 2 ' + env.t('gems') + ' ' + env.t('friendText3c') + !=env.t('legSeventh') tr td a.label.label-contributor-8(ng-click='toggleUserTier($event)')=env.t('heroicBadge') @@ -111,12 +116,12 @@ include ./challenge-box .span8(ng-controller='ChatCtrl') - h3=env.t('taverntalk') + h3=env.t('tavernTalk') include ./chat-box small(style='position: relative; top: -20px; left: 25px;') include ../../shared/formatting-help small(style='position: relative; top: 18px; left: 0px') alert.alert-info - !=env.t('tavernalert1') + ' ' + env.t('tavernalert2') + '.' + !=env.t('tavernAlert1') + ' ' + env.t('tavernAlert2') + '.' ul.unstyled.tavern-chat include ./chat-message diff --git a/views/shared/header/avatar.jade b/views/shared/header/avatar.jade index 5491806c35..ef266bbb2f 100644 --- a/views/shared/header/avatar.jade +++ b/views/shared/header/avatar.jade @@ -47,4 +47,4 @@ figure.herobox(ng-click='spell ? castEnd(profile, "user", $event) : clickMember( i.icon-circle-arrow-up.icon-white(ng-show='profile.stats.buffs.str || profile.stats.buffs.per || profile.stats.buffs.con || profile.stats.buffs.int || profile.stats.buffs.stealth', tooltip=env.t('buffed'), style='margin-right:5px;') =env.t('lvl') | {{profile.stats.lvl}} - i.icon-plus-sign.icon-white(ng-show='profile.achievements.rebirths', tooltip=env.t('reborn') + '{{profile.achievements.rebirthLevel}}', style='margin-left:5px') + i.icon-plus-sign.icon-white(ng-show='profile.achievements.rebirths', tooltip=env.t('reborn', {reLevel: "{{profile.achievements.rebirthLevel}}"}), style='margin-left:5px') diff --git a/views/shared/modals/achievements.jade b/views/shared/modals/achievements.jade index 3526cb9dc4..ed4ba27b57 100644 --- a/views/shared/modals/achievements.jade +++ b/views/shared/modals/achievements.jade @@ -36,11 +36,8 @@ div(modal='user.flags.contributor') .modal-body .npc_justin.float-left p - | {{user.profile.name}}, - =env.t('contribText1') - | {{user.contributor.level}} - =env.t('contribText2') - a(href='http://habitrpg.wikia.com/wiki/Contributor_Rewards' target='_blank')=env.t('contribText3') + !=env.t('contribModal', {name: "{{user.profile.name}}", level: "{{user.contributor.level}}"}) + a(href='http://habitrpg.wikia.com/wiki/Contributor_Rewards' target='_blank')=env.t('contribLink') .modal-footer button.btn.btn-default.cancel(ng-click='set({"flags.contributor":false})')=env.t('ok') @@ -50,10 +47,6 @@ div(modal='modals.achievements.rebirth') h3=env.t('modalAchievement') .modal-body .achievement.achievement-sun - =env.t('rebirthAchievement1') - | {{user.achievements.rebirths}} - =env.t('rebirthAchievement2') - | {{user.achievements.rebirthLevel}}. - =env.t('rebirthAchievement3') + =env.t('rebirthAchievement', {number: "{{user.achievements.rebirths}}", level: "{{user.achievements.rebirthLevel}}"}) .modal-footer button.btn.btn-default.cancel(ng-click='modals.achievements.rebirth = false')=env.t('ok') diff --git a/views/shared/modals/buy-gems.jade b/views/shared/modals/buy-gems.jade index a7bd1375f8..63b5127abb 100644 --- a/views/shared/modals/buy-gems.jade +++ b/views/shared/modals/buy-gems.jade @@ -3,7 +3,7 @@ div(modal='modals.buyGems') .buy-gems include ../gems .well - h3 Buy Gems + h3=env.t('buyGems') table.table tr td @@ -14,7 +14,8 @@ div(modal='modals.buyGems') span.dashed-underline(popover=env.t('donateText3'),popover-trigger='mouseenter',popover-placement='right') =env.t('donateText2') tr - td.alert.alert-info=env.t('fiveUSD1') + td.alert.alert-info $5  + =env.t('USD') tr td .btn.btn-primary(ng-click='showStripe()',style='margin-left:10px;')=env.t('payWithCard') diff --git a/views/shared/modals/settings.jade b/views/shared/modals/settings.jade index 956b47a96d..0eefd1dd05 100644 --- a/views/shared/modals/settings.jade +++ b/views/shared/modals/settings.jade @@ -66,9 +66,7 @@ div(ng-controller='SettingsCtrl') .modal-header h3=env.t('deleteaccount') .modal-body - p=env.t('deleteText1') - strong=env.t('deleteText2') - =env.t('deleteText3') + p!=env.t('deleteText') p input(type='text', ng-model='_deleteAccount') .modal-footer diff --git a/views/shared/tasks/task.jade b/views/shared/tasks/task.jade index 1546e08faa..5fd8d64baa 100644 --- a/views/shared/tasks/task.jade +++ b/views/shared/tasks/task.jade @@ -93,9 +93,7 @@ li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s a(ng-click='unlink(task, "remove-all")')=env.t('removeThem') div(ng-if='task.challenge.broken=="CHALLENGE_CLOSED"') p - =env.t('challengeCompleted1') - span.badge {{task.challenge.winner}} - =env.t('challengeCompleted2') + !=env.t('challengeCompleted', {user: "{{task.challenge.winner}}"}) p a(ng-click='unlink(task, "keep-all")')=env.t('keepThem') |  |  diff --git a/views/static/about.jade b/views/static/about.jade index 6340ef31af..dfe6a20451 100644 --- a/views/static/about.jade +++ b/views/static/about.jade @@ -5,7 +5,7 @@ block vars - var menuItem = 'about' block title - title About + title=env.t('companyAbout') block content .container diff --git a/views/static/contact.jade b/views/static/contact.jade index 43591138b2..129734d3de 100644 --- a/views/static/contact.jade +++ b/views/static/contact.jade @@ -3,7 +3,7 @@ block vars - var layoutEnv = env - var menuItem = 'contact' block title - title Contact + title=env.t('contact') block content // Probably just add linkks to the respective contact locations? // Bugs? Github diff --git a/views/static/layout.jade b/views/static/layout.jade index fad18bac00..5492c2365f 100644 --- a/views/static/layout.jade +++ b/views/static/layout.jade @@ -27,15 +27,15 @@ html ul.nav li(class='#{menuItem=="home" ? "active" : ""}') - a(href='/static/front') Home + a(href='/static/front')=env.t('home') li(class='#{menuItem=="about" ? "active" : ""}') - a(href='/static/about') Learn More + a(href='/static/about')=env.t('learnMore') li - a(href='http://blog.habitrpg.com/') Blog + a(href='http://blog.habitrpg.com/')=env.t('companyBlog') li(class='#{menuItem=="plans" ? "active" : ""}') - a(href='/static/plans') Group Plans + a(href='/static/plans')=env.t('groupPlans') //li(class='#{menuItem=="contact" ? "active" : ""}') - a(href='/static/contact') Contact + a(href='/static/contact')=env.t('contact') .container block content diff --git a/views/static/plans.jade b/views/static/plans.jade index 1699d5a2b3..5f88d21333 100644 --- a/views/static/plans.jade +++ b/views/static/plans.jade @@ -5,25 +5,33 @@ block vars - var menuItem = 'plans' block title - title Group Plans + title=env.t('groupPlans') block content - h2 Group Plans + h2=env.t('groupPlans') - p For individuals, HabitRPG is free to play. Even for small interest groups, free (or cheap) guilds and challenges can be used to motivate participants in behavioral modification. Think writing groups, art challenges, and more. - p But some group leaders will want more control, privacy, security, and support. Examples of such groups are families, health and wellness groups, employee groups, and more. These plans provide private instances of HabitRPG for your group or organization, secure and independent of Habitica. See below for additional plan perks, and contact us for more information! + p + =env.t('indivPlan1') + a(href='http://habitrpg.wikia.com/wiki/Guilds' target='_blank')=env.t('guilds') + = ' ' + env.t('and') + ' ' + a(href='http://habitrpg.wikia.com/wiki/Challenges' target='_blank')=env.t('challenges') + =env.t('indivPlan2') + p + =env.t('groupText1') + a(href='http://habitrpg.wikia.com/wiki/Habitica' target='_blank')=env.t('habitica') + |.  + =env.t('groupText2') .subscription-features table.table.table-striped tr th.feature-name - th.feature-name.muted Family (Coming Soon) - th.feature-name.muted Group (Coming Soon) - th.feature-name Organization + th.feature-name.muted=env.t('planFamily') + th.feature-name.muted=env.t('planGroup') + th.feature-name=env.t('organization') tr th - span.dashed-underline(popover="Members of the organization participate outside of HabitRPG proper, providing focus for your participants.",popover-trigger='mouseenter',popover-placement='right') - | Private Organization + span.dashed-underline(popover=env.t('organizationSubText'),popover-trigger='mouseenter',popover-placement='right')=env.t('organizationSub') td.muted i.icon-ok td.muted @@ -32,53 +40,46 @@ block content i.icon-ok tr th - span.dashed-underline(popover="Dedicated Hosting: you get your own database and server hosted by HabitRPG, or optionally we'll install it in your organization's network. If not checked, the plan uses \"Shared Hosting\": your organization uses the same database as HabitRPG proper while performing independently Habitica. Your members are shielded from Tavern & Guilds, but still on the same server/database.",popover-trigger='mouseenter',popover-placement='right') - | Dedicated Hosting + span.dashed-underline(popover=env.t('dedicatedHostText'),popover-trigger='mouseenter',popover-placement='right')=env.t('dedicatedHost') td td td i.icon-ok tr th - span.dashed-underline(popover="We can optionally give you your own domain for the installation.",popover-trigger='mouseenter',popover-placement='right') - | Custom Domain + span.dashed-underline(popover=env.t('customDomainText'),popover-trigger='mouseenter',popover-placement='right')=env.t('customDomain') td td td i.icon-ok tr th - span.dashed-underline(popover="The maximum number of players in your private organization.",popover-trigger='mouseenter',popover-placement='right') - | Max Participants + span.dashed-underline(popover=env.t('maxPlayersText'),popover-trigger='mouseenter',popover-placement='right')=env.t('maxPlayers') td.muted 10 td.muted 75 - td Unlimited + td=env.t('unlimited') tr th - span.dashed-underline(popover="First to be provided for with support.",popover-trigger='mouseenter',popover-placement='right') - | Priority Support On Tickets & Hosting + span.dashed-underline(popover=env.t('priSupportText'),popover-trigger='mouseenter',popover-placement='right')=env.t('priSupport') td td td i.icon-ok tr th - span.dashed-underline(popover="We will provide support for training, bugs, installation, and feature requests. Additional hours available at an hourly rate.",popover-trigger='mouseenter',popover-placement='right') - | Support Hours / Month + span.dashed-underline(popover=env.t('timeSupportText'),popover-trigger='mouseenter',popover-placement='right')=env.t('timeSupport') td.muted - td.muted 5 td 10 tr th - h5 Game features: + h5=env.t('gameFeatures') + ':' ul - li Ads disabled for members + li=env.t('gameNoAds') li - span.dashed-underline(popover="Members will be able to purchase gems with gold, meaning none of your participants need to buy anything with real money.",popover-trigger='mouseenter',popover-placement='right') - | Gems purchasable with gold + span.dashed-underline(popover=env.t('gold2GemText'),popover-trigger='mouseenter',popover-placement='right')=env.t('gold2Gem') li - span.dashed-underline(popover="We will provide the organization leaders with as many gems as they need, for things like challenge prizes, guild-creation, etc.",popover-trigger='mouseenter',popover-placement='right') - | Infinite leader gems + span.dashed-underline(popover=env.t('infiniteGemText'),popover-trigger='mouseenter',popover-placement='right')=env.t('infiniteGem') td.muted i.icon-ok td.muted @@ -89,8 +90,8 @@ block content th //| Price td - a.btn(href='https://docs.google.com/forms/d/17torT7OlxgtbHAPdBDRQNG2lTdIQyrGXk4O2YXvUMig/viewform',popover="Plan not yet available, but click to contact us and we'll keep you updated.",popover-placement='right',popover-trigger='mouseenter') Contact Us + a.btn.muted(href='https://docs.google.com/forms/d/17torT7OlxgtbHAPdBDRQNG2lTdIQyrGXk4O2YXvUMig/viewform',popover=env.t('notYetPlan'),popover-placement='right',popover-trigger='mouseenter')=env.t('contactUs') td - a.btn.muted(href='https://docs.google.com/forms/d/17torT7OlxgtbHAPdBDRQNG2lTdIQyrGXk4O2YXvUMig/viewform',popover="Plan not yet available, but click to contact us and we'll keep you updated.",popover-placement='right',popover-trigger='mouseenter') Contact Us + a.btn.muted(href='https://docs.google.com/forms/d/17torT7OlxgtbHAPdBDRQNG2lTdIQyrGXk4O2YXvUMig/viewform',popover=env.t('notYetPlan'),popover-placement='right',popover-trigger='mouseenter')=env.t('contactUs') td - a.btn.btn-primary(href='https://docs.google.com/forms/d/12Jqj_8f3oQS0B3ZUHewHbK61uLjBdzBeB0zyEqB9lxM/viewform') Contact Us \ No newline at end of file + a.btn.btn-primary(href='https://docs.google.com/forms/d/12Jqj_8f3oQS0B3ZUHewHbK61uLjBdzBeB0zyEqB9lxM/viewform')=env.t('contactUs') \ No newline at end of file From 6f2f3dd71e5b0419ceec326d2209df03c4831094 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 13:43:30 -0700 Subject: [PATCH 14/32] chore(newrelic): some newrelic code location experiments --- newrelic.js | 2 +- src/server.js | 9 ++------- src/utils.js | 1 + 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/newrelic.js b/newrelic.js index 7763ba2269..682f608e42 100644 --- a/newrelic.js +++ b/newrelic.js @@ -20,6 +20,6 @@ exports.config = { * issues with the agent, 'info' and higher will impose the least overhead on * production applications. */ - level : 'trace' + level : 'warning' } }; diff --git a/src/server.js b/src/server.js index 0dec3210ac..fb76862fad 100644 --- a/src/server.js +++ b/src/server.js @@ -1,5 +1,6 @@ -// Only do the minimal amount of work before forking just in case of a dyno restart var nconf = require('nconf'); +var utils = require('./utils'); +utils.setupConfig(); require('coffee-script') // remove this once we've fully converted over var express = require("express"); @@ -8,18 +9,12 @@ var path = require("path"); var domainMiddleware = require('domain-middleware'); var swagger = require("swagger-node-express"); -var utils = require('./utils'); var middleware = require('./middleware'); var TWO_WEEKS = 1000 * 60 * 60 * 24 * 14; var app = express(); var server; -// ------------ Setup configurations ------------ -utils.setupConfig(); -if (nconf.get('NODE_ENV') === 'production') - require('newrelic'); - // ------------ MongoDB Configuration ------------ mongoose = require('mongoose'); require('./models/user'); //load up the user schema - TODO is this necessary? diff --git a/src/utils.js b/src/utils.js index 791ba3c57c..e903b3b91c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -56,6 +56,7 @@ module.exports.setupConfig = function(){ if (nconf.get('NODE_ENV') === "development") { Error.stackTraceLimit = Infinity; } + if (nconf.get('NODE_ENV') === 'production') require('newrelic'); }; From 7c3d85dbbee6c4d5467ab9b0843d357311749d86 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 15:24:14 -0700 Subject: [PATCH 15/32] chore(cluster): try cluster again --- src/server.js | 217 +++++++++++++++++++++++++++----------------------- 1 file changed, 116 insertions(+), 101 deletions(-) diff --git a/src/server.js b/src/server.js index fb76862fad..4bcf9d4fc0 100644 --- a/src/server.js +++ b/src/server.js @@ -1,124 +1,139 @@ +// Only do the minimal amount of work before forking just in case of a dyno restart +var cluster = require("cluster"); var nconf = require('nconf'); var utils = require('./utils'); utils.setupConfig(); -require('coffee-script') // remove this once we've fully converted over -var express = require("express"); -var http = require("http"); -var path = require("path"); -var domainMiddleware = require('domain-middleware'); -var swagger = require("swagger-node-express"); +if (cluster.isMaster && (nconf.get('NODE_ENV') == 'development' || nconf.get('NODE_ENV') == 'production')) { + var numCPUs = require('os').cpus().length; -var middleware = require('./middleware'); + // Fork workers. + for (var i = 0; i < numCPUs; i++) { + cluster.fork(); + } -var TWO_WEEKS = 1000 * 60 * 60 * 24 * 14; -var app = express(); -var server; + cluster.on('exit', function(worker, code, signal) { + cluster.fork(); // replace the dead worker + }); -// ------------ MongoDB Configuration ------------ -mongoose = require('mongoose'); -require('./models/user'); //load up the user schema - TODO is this necessary? -require('./models/group'); -require('./models/challenge'); -mongoose.connect(nconf.get('NODE_DB_URI'), {auto_reconnect:true}, function(err) { - if (err) throw err; - console.info('Connected with Mongoose'); -}); +} 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 domainMiddleware = require('domain-middleware'); + var swagger = require("swagger-node-express"); + + var middleware = require('./middleware'); + + var TWO_WEEKS = 1000 * 60 * 60 * 24 * 14; + var app = express(); + var server; + + // ------------ MongoDB Configuration ------------ + mongoose = require('mongoose'); + require('./models/user'); //load up the user schema - TODO is this necessary? + require('./models/group'); + require('./models/challenge'); + mongoose.connect(nconf.get('NODE_DB_URI'), {auto_reconnect:true}, function(err) { + if (err) throw err; + console.info('Connected with Mongoose'); + }); -// ------------ Passport Configuration ------------ -var passport = require('passport') -var util = require('util') -var FacebookStrategy = require('passport-facebook').Strategy; -// Passport session setup. -// To support persistent login sessions, Passport needs to be able to -// serialize users into and deserialize users out of the session. Typically, -// this will be as simple as storing the user ID when serializing, and finding -// the user by ID when deserializing. However, since this example does not -// have a database of user records, the complete Facebook profile is serialized -// and deserialized. -passport.serializeUser(function(user, done) { - done(null, user); -}); + // ------------ Passport Configuration ------------ + var passport = require('passport') + var util = require('util') + var FacebookStrategy = require('passport-facebook').Strategy; + // Passport session setup. + // To support persistent login sessions, Passport needs to be able to + // serialize users into and deserialize users out of the session. Typically, + // this will be as simple as storing the user ID when serializing, and finding + // the user by ID when deserializing. However, since this example does not + // have a database of user records, the complete Facebook profile is serialized + // and deserialized. + passport.serializeUser(function(user, done) { + done(null, user); + }); -passport.deserializeUser(function(obj, done) { - done(null, obj); -}); + passport.deserializeUser(function(obj, done) { + done(null, obj); + }); -// Use the FacebookStrategy within Passport. -// Strategies in Passport require a `verify` function, which accept -// credentials (in this case, an accessToken, refreshToken, and Facebook -// profile), and invoke a callback with a user object. -passport.use(new FacebookStrategy({ - clientID: nconf.get("FACEBOOK_KEY"), - clientSecret: nconf.get("FACEBOOK_SECRET"), - callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback" -}, - function(accessToken, refreshToken, profile, done) { - // asynchronous verification, for effect... - //process.nextTick(function () { + // Use the FacebookStrategy within Passport. + // Strategies in Passport require a `verify` function, which accept + // credentials (in this case, an accessToken, refreshToken, and Facebook + // profile), and invoke a callback with a user object. + passport.use(new FacebookStrategy({ + clientID: nconf.get("FACEBOOK_KEY"), + clientSecret: nconf.get("FACEBOOK_SECRET"), + callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback" + }, + function(accessToken, refreshToken, profile, done) { + // asynchronous verification, for effect... + //process.nextTick(function () { - // To keep the example simple, the user's Facebook profile is returned to - // represent the logged-in user. In a typical application, you would want - // to associate the Facebook account with a user record in your database, - // and return that user instead. - return done(null, profile); - //}); - } - )); + // To keep the example simple, the user's Facebook profile is returned to + // represent the logged-in user. In a typical application, you would want + // to associate the Facebook account with a user record in your database, + // and return that user instead. + return done(null, profile); + //}); + } + )); -// ------------ Server Configuration ------------ -app.set("port", nconf.get('PORT')); + // ------------ Server Configuration ------------ + app.set("port", nconf.get('PORT')); -if (nconf.get('NODE_ENV') !== 'production') - app.use(express.logger("dev")); -app.use(express.compress()); -app.set("views", __dirname + "/../views"); -app.set("view engine", "jade"); -app.use(express.favicon()); -app.use(middleware.cors); -app.use(middleware.forceSSL); -app.use(express.urlencoded()); -app.use(express.json()); -app.use(express.methodOverride()); -//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()); + if (!process.env.SUPPRESS) app.use(express.logger("dev")); + app.use(express.compress()); + app.set("views", __dirname + "/../views"); + app.set("view engine", "jade"); + app.use(express.favicon()); + app.use(middleware.cors); + app.use(middleware.forceSSL); + app.use(express.urlencoded()); + app.use(express.json()); + app.use(express.methodOverride()); + //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()); -// Initialize Passport! Also use passport.session() middleware, to support -// persistent login sessions (recommended). -app.use(passport.initialize()); -app.use(passport.session()); + // 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); + app.use(app.router); -var maxAge = (nconf.get('NODE_ENV') === 'production') ? 31536000000 : 0; -app.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge })); -app.use(express['static'](path.join(__dirname, "/../public"))); + var maxAge = (nconf.get('NODE_ENV') === 'production') ? 31536000000 : 0; + app.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge })); + app.use(express['static'](path.join(__dirname, "/../public"))); -// development only -//if ("development" === app.get("env")) { -// app.use(express.errorHandler()); -//} + // development only + //if ("development" === app.get("env")) { + // app.use(express.errorHandler()); + //} -// Custom Directives -app.use(require('./routes/pages').middleware); -app.use(require('./routes/auth').middleware); -var v2 = express(); -app.use('/api/v2', v2); -app.use('/api/v1', require('./routes/apiv1').middleware); -app.use('/export', require('./routes/dataexport').middleware); + // Custom Directives + app.use(require('./routes/pages').middleware); + app.use(require('./routes/auth').middleware); + var v2 = express(); + app.use('/api/v2', v2); + app.use('/api/v1', require('./routes/apiv1').middleware); + app.use('/export', require('./routes/dataexport').middleware); -app.use(utils.errorHandler); + app.use(utils.errorHandler); -require('./routes/apiv2.coffee')(swagger, v2); + require('./routes/apiv2.coffee')(swagger, v2); -server = http.createServer(app).listen(app.get("port"), function() { - return console.log("Express server listening on port " + app.get("port")); -}); -app.use(domainMiddleware({ - server: server - //killTimeout: 30000, -})); \ No newline at end of file + server = http.createServer(app).listen(app.get("port"), function() { + return console.log("Express server listening on port " + app.get("port")); + }); + app.use(domainMiddleware({ + server: server + //killTimeout: 30000, + })); +} \ No newline at end of file From 4f02ccb3b0779c330e326f72726ba0e9e935d354 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 16:33:25 -0700 Subject: [PATCH 16/32] chore(cluster): kill & respawn pids on cluster fail, using domain-middleware --- src/server.js | 223 ++++++++++++++++++++++++++------------------------ src/utils.js | 1 + 2 files changed, 115 insertions(+), 109 deletions(-) diff --git a/src/server.js b/src/server.js index 4bcf9d4fc0..fe81fb76cd 100644 --- a/src/server.js +++ b/src/server.js @@ -1,139 +1,144 @@ // Only do the minimal amount of work before forking just in case of a dyno restart var cluster = require("cluster"); +var _ = require('lodash'); var nconf = require('nconf'); var utils = require('./utils'); utils.setupConfig(); if (cluster.isMaster && (nconf.get('NODE_ENV') == 'development' || nconf.get('NODE_ENV') == 'production')) { - var numCPUs = require('os').cpus().length; + // Fork workers. + _.times(require('os').cpus().length, function(){ + cluster.fork(); + }) - // Fork workers. - for (var i = 0; i < numCPUs; i++) { - cluster.fork(); - } - - cluster.on('exit', function(worker, code, signal) { - cluster.fork(); // replace the dead worker - }); + cluster.on('exit', function(worker, code, signal) { + var w = cluster.fork(); // replace the dead worker + if (nconf.get('NODE_ENV') === 'development') + console.error('[%s] [master:%s] worker:%s disconnect! new worker:%s fork', new Date(), process.pid, worker.process.pid, w.process.pid); + }); } 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 domainMiddleware = require('domain-middleware'); - var swagger = require("swagger-node-express"); + require('coffee-script'); // remove this once we've fully converted over + var express = require("express"); + var http = require("http"); + var path = require("path"); + var domainMiddleware = require('domain-middleware'); + var swagger = require("swagger-node-express"); - var middleware = require('./middleware'); + var middleware = require('./middleware'); - var TWO_WEEKS = 1000 * 60 * 60 * 24 * 14; - var app = express(); - var server; + var TWO_WEEKS = 1000 * 60 * 60 * 24 * 14; + var app = express(); + var server = http.createServer(); - // ------------ MongoDB Configuration ------------ - mongoose = require('mongoose'); - require('./models/user'); //load up the user schema - TODO is this necessary? - require('./models/group'); - require('./models/challenge'); - mongoose.connect(nconf.get('NODE_DB_URI'), {auto_reconnect:true}, function(err) { - if (err) throw err; - console.info('Connected with Mongoose'); - }); + // ------------ MongoDB Configuration ------------ + mongoose = require('mongoose'); + require('./models/user'); //load up the user schema - TODO is this necessary? + require('./models/group'); + require('./models/challenge'); + mongoose.connect(nconf.get('NODE_DB_URI'), {auto_reconnect:true}, function(err) { + if (err) throw err; + console.info('Connected with Mongoose'); + }); - // ------------ Passport Configuration ------------ - var passport = require('passport') - var util = require('util') - var FacebookStrategy = require('passport-facebook').Strategy; - // Passport session setup. - // To support persistent login sessions, Passport needs to be able to - // serialize users into and deserialize users out of the session. Typically, - // this will be as simple as storing the user ID when serializing, and finding - // the user by ID when deserializing. However, since this example does not - // have a database of user records, the complete Facebook profile is serialized - // and deserialized. - passport.serializeUser(function(user, done) { - done(null, user); - }); + // ------------ Passport Configuration ------------ + var passport = require('passport') + var util = require('util') + var FacebookStrategy = require('passport-facebook').Strategy; + // Passport session setup. + // To support persistent login sessions, Passport needs to be able to + // serialize users into and deserialize users out of the session. Typically, + // this will be as simple as storing the user ID when serializing, and finding + // the user by ID when deserializing. However, since this example does not + // have a database of user records, the complete Facebook profile is serialized + // and deserialized. + passport.serializeUser(function(user, done) { + done(null, user); + }); - passport.deserializeUser(function(obj, done) { - done(null, obj); - }); + passport.deserializeUser(function(obj, done) { + done(null, obj); + }); - // Use the FacebookStrategy within Passport. - // Strategies in Passport require a `verify` function, which accept - // credentials (in this case, an accessToken, refreshToken, and Facebook - // profile), and invoke a callback with a user object. - passport.use(new FacebookStrategy({ - clientID: nconf.get("FACEBOOK_KEY"), - clientSecret: nconf.get("FACEBOOK_SECRET"), - callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback" - }, - function(accessToken, refreshToken, profile, done) { - // asynchronous verification, for effect... - //process.nextTick(function () { + // Use the FacebookStrategy within Passport. + // Strategies in Passport require a `verify` function, which accept + // credentials (in this case, an accessToken, refreshToken, and Facebook + // profile), and invoke a callback with a user object. + passport.use(new FacebookStrategy({ + clientID: nconf.get("FACEBOOK_KEY"), + clientSecret: nconf.get("FACEBOOK_SECRET"), + callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback" + }, + function(accessToken, refreshToken, profile, done) { + // asynchronous verification, for effect... + //process.nextTick(function () { - // To keep the example simple, the user's Facebook profile is returned to - // represent the logged-in user. In a typical application, you would want - // to associate the Facebook account with a user record in your database, - // and return that user instead. - return done(null, profile); - //}); - } - )); + // To keep the example simple, the user's Facebook profile is returned to + // represent the logged-in user. In a typical application, you would want + // to associate the Facebook account with a user record in your database, + // and return that user instead. + return done(null, profile); + //}); + } + )); - // ------------ Server Configuration ------------ - app.set("port", nconf.get('PORT')); + // ------------ Server Configuration ------------ + domainMiddleware({ + server: server, + killTimeout: 3000 + }), - if (!process.env.SUPPRESS) app.use(express.logger("dev")); - app.use(express.compress()); - app.set("views", __dirname + "/../views"); - app.set("view engine", "jade"); - app.use(express.favicon()); - app.use(middleware.cors); - app.use(middleware.forceSSL); - app.use(express.urlencoded()); - app.use(express.json()); - app.use(express.methodOverride()); - //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.set("port", nconf.get('PORT')); - // Initialize Passport! Also use passport.session() middleware, to support - // persistent login sessions (recommended). - app.use(passport.initialize()); - app.use(passport.session()); + if (!process.env.SUPPRESS) app.use(express.logger("dev")); + app.use(express.compress()); + app.set("views", __dirname + "/../views"); + app.set("view engine", "jade"); + app.use(express.favicon()); + app.use(middleware.cors); + app.use(middleware.forceSSL); + app.use(express.urlencoded()); + app.use(express.json()); + app.use(express.methodOverride()); + //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(app.router); + // Initialize Passport! Also use passport.session() middleware, to support + // persistent login sessions (recommended). + app.use(passport.initialize()); + app.use(passport.session()); - var maxAge = (nconf.get('NODE_ENV') === 'production') ? 31536000000 : 0; - app.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge })); - app.use(express['static'](path.join(__dirname, "/../public"))); + app.use(app.router); - // development only - //if ("development" === app.get("env")) { - // app.use(express.errorHandler()); - //} + var maxAge = (nconf.get('NODE_ENV') === 'production') ? 31536000000 : 0; + app.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge })); + app.use(express['static'](path.join(__dirname, "/../public"))); - // Custom Directives - app.use(require('./routes/pages').middleware); - app.use(require('./routes/auth').middleware); - var v2 = express(); - app.use('/api/v2', v2); - app.use('/api/v1', require('./routes/apiv1').middleware); - app.use('/export', require('./routes/dataexport').middleware); + // development only + //if ("development" === app.get("env")) { + // app.use(express.errorHandler()); + //} - app.use(utils.errorHandler); + // Custom Directives + app.use(require('./routes/pages').middleware); + app.use(require('./routes/auth').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(utils.errorHandler); - server = http.createServer(app).listen(app.get("port"), function() { - return console.log("Express server listening on port " + app.get("port")); - }); - app.use(domainMiddleware({ - server: server - //killTimeout: 30000, - })); + require('./routes/apiv2.coffee')(swagger, v2); + + server.on('request', app); + server.listen(app.get("port"), function() { + return console.log("Express server listening on port " + app.get("port")); + }); + + module.exports = server; } \ No newline at end of file diff --git a/src/utils.js b/src/utils.js index e903b3b91c..b8a15cd26d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -79,4 +79,5 @@ module.exports.errorHandler = function(err, req, res, next) { var shortMessage = (err.message.length < 200) ? err.message : err.message.substring(0,100) + err.message.substring(err.message.length-100,err.message.length); res.json(500,{err:shortMessage}); //res.end(err.message); + process.exit(0); } \ No newline at end of file From 52df5341967ef294da75e5853d1c9a188088e5c1 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 17:18:48 -0700 Subject: [PATCH 17/32] chore(cluster): don't kill process until email sent --- src/utils.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/utils.js b/src/utils.js index b8a15cd26d..8e2b326936 100644 --- a/src/utils.js +++ b/src/utils.js @@ -3,7 +3,7 @@ var nconf = require('nconf'); var crypto = require('crypto'); var path = require("path"); -module.exports.sendEmail = function(mailData) { +module.exports.sendEmail = function(mailData, cb) { var smtpTransport = nodemailer.createTransport("SMTP",{ service: nconf.get('SMTP_SERVICE'), auth: { @@ -15,6 +15,7 @@ module.exports.sendEmail = function(mailData) { if(error) console.log(error); else console.log("Message sent: " + response.message); smtpTransport.close(); // shut down the connection pool, no more messages + if (cb) cb(); }); } @@ -69,15 +70,16 @@ module.exports.errorHandler = function(err, req, res, next) { "\n\nheaders: " + JSON.stringify(req.headers) + "\n\nbody: " + JSON.stringify(req.body) + (res.locals.ops ? "\n\ncompleted ops: " + JSON.stringify(res.locals.ops) : ""); + var shortMessage = (err.message.length < 200) ? err.message : + err.message.substring(0,100) + err.message.substring(err.message.length-100,err.message.length); + res.json(500,{err:shortMessage}); //res.end(err.message); + console.error(stack); module.exports.sendEmail({ from: "HabitRPG <" + nconf.get('SMTP_USER') + ">", to: nconf.get('ADMIN_EMAIL') || nconf.get('SMTP_USER'), subject: "HabitRPG Error", text: stack + }, function(){ + process.exit(0); }); - console.error(stack); - var shortMessage = (err.message.length < 200) ? err.message : - err.message.substring(0,100) + err.message.substring(err.message.length-100,err.message.length); - res.json(500,{err:shortMessage}); //res.end(err.message); - process.exit(0); } \ No newline at end of file From 8a42e0c392993801cd0ca742820a76bdc0366128 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 17:21:17 -0700 Subject: [PATCH 18/32] chore(logging): no production logging (how did this commit get removed?) --- src/server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server.js b/src/server.js index fe81fb76cd..3d275a0abd 100644 --- a/src/server.js +++ b/src/server.js @@ -92,7 +92,8 @@ if (cluster.isMaster && (nconf.get('NODE_ENV') == 'development' || nconf.get('NO app.set("port", nconf.get('PORT')); - if (!process.env.SUPPRESS) app.use(express.logger("dev")); + if (nconf.get('NODE_ENV') !== 'production') + app.use(express.logger("dev")); app.use(express.compress()); app.set("views", __dirname + "/../views"); app.set("view engine", "jade"); From aff885c05c03bd70beeb0db8d68922671fc46309 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 19:20:07 -0700 Subject: [PATCH 19/32] feat(habitBirthday): add habitrpg birthday event. includes cakes for all pets, absurd party robes, npc swap, badge, etc. @lemoness --- migrations/20140130_birthdayEnd.js | 1 + migrations/20140130_birthdayStart.js | 13 ++++++++ src/models/user.js | 3 +- views/options/inventory/inventory.jade | 2 +- views/shared/modals/new-stuff.jade | 44 ++++++++++++++++++++----- views/shared/profiles/achievements.jade | 8 +++++ 6 files changed, 60 insertions(+), 11 deletions(-) create mode 100644 migrations/20140130_birthdayEnd.js create mode 100644 migrations/20140130_birthdayStart.js diff --git a/migrations/20140130_birthdayEnd.js b/migrations/20140130_birthdayEnd.js new file mode 100644 index 0000000000..a9a8eb93bc --- /dev/null +++ b/migrations/20140130_birthdayEnd.js @@ -0,0 +1 @@ +db.users.update({},{$set:{'achievements.habitBirthday':true}},{multi:1}) diff --git a/migrations/20140130_birthdayStart.js b/migrations/20140130_birthdayStart.js new file mode 100644 index 0000000000..458f699c5c --- /dev/null +++ b/migrations/20140130_birthdayStart.js @@ -0,0 +1,13 @@ +db.users.update({},{$set:{ + 'items.food.Cake_Skeleton':1, + 'items.food.Cake_Base':1, + 'items.food.Cake_Base':1, + 'items.food.Cake_CottonCandyBlue':1, + 'items.food.Cake_CottonCandyPink':1, + 'items.food.Cake_Shade':1, + 'items.food.Cake_White':1, + 'items.food.Cake_Golden':1, + 'items.food.Cake_Zombie':1, + 'items.food.Cake_Desert':1, + 'items.food.Cake_Red':1 +}},{multi:1}) diff --git a/src/models/user.js b/src/models/user.js index 391810fb54..506fb505b7 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -41,7 +41,8 @@ var UserSchema = new Schema({ quests: Schema.Types.Mixed, rebirths: Number, rebirthLevel: Number, - perfect: Number + perfect: Number, + habitBirthday: Boolean }, auth: { facebook: Schema.Types.Mixed, diff --git a/views/options/inventory/inventory.jade b/views/options/inventory/inventory.jade index ee228041dc..66bc97c7f1 100644 --- a/views/options/inventory/inventory.jade +++ b/views/options/inventory/inventory.jade @@ -100,7 +100,7 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html') li.customize-menu menu.pets-menu(label=env.t('food')) - div(ng-repeat='food in Content.food', ng-show='food.key !== "Saddle"') + div(ng-repeat='food in Content.food', ng-if='food.key !== "Saddle" && food.canBuy') button.customize-option(popover='{{food.notes}}', popover-title='{{food.text}}', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("food", food)', class='Pet_Food_{{food.key}}') p | {{food.value}} diff --git a/views/shared/modals/new-stuff.jade b/views/shared/modals/new-stuff.jade index 9eb471415b..e2a584d133 100644 --- a/views/shared/modals/new-stuff.jade +++ b/views/shared/modals/new-stuff.jade @@ -29,24 +29,50 @@ div(modal='modals.newStuff') table.table.table-striped tr td - h5 Group Plans - p We've begun adding plans for groups (parents, teachers, health & wellness administrators, etc). These plans will provide group leaders with more control, privacy, security, and support. Currently only the Organization Plan (top tier) is available (due to tech limitations believe it or not), and we'll be releasing the Family & Group plans later. Click the "Contact Us" buttons if you're interested, and we'll keep you updated! + h5 Happy Birthday, HabitRPG! + p The fair land of Habitica is two years old today! The NPCs are celebrating in style, and it looks like some of the staff is, too! Won't you join in? tr td - h5 Individual Plan - p We've introduced a $5/mo basic subscription plan. It comes with a number of perks, which you can see here. We'll likely add more benefits over time, follow the conversation here. + h5 Absurd Party Robtes + p As part of the festivities, Absurd Party Robes are available free of charge in the Item Store! Swath yourself in those silly garbs and don your matching hats to celebrate this momentous day. tr td - h5 Perfect Day Achievement - p Now when you complete all your dailies, you stack this badge, plus and additional perk: you get a +(level/2) buff to all stats! + h5 Delicious Cake + p What would a birthday be without birthday cake in a myriad of flavors? Of course, pets are very picky, but luckily Lemoness and her team of bakers have plenty of slices to go around. Mmm, delicious! tr td - h5 Spread The Word Challenge Update - p We have 1k+ submissions, holy cow! Great job everyone! Now, we need to go through these manually, so it will take a few days to a couple weeks to process. The challenge will stay open until we're done choosing our winners, but be sure to edit the To-Do with your submission URL before 1/31, as that's the cut-off date for processing. We'll send a Tweet out when the winner has been selected, so follow @habitrpg and stay tuned. + h5 Last Day of Winter Wonderland Event + p Also, just a reminder - this is the final day of the Winter Wonderland event, so it's your last day to get the Limited Edition Winter Hair Colors, the Winter Outfits, the snowballs, and the Trapper Santa and Find the Cub quest scrolls. (Remember that if you are in the middle of a Trapper Santa or Find the Cub Quest, it will continue! Only the scrolls will disappear.) We hope that you've had a wonderful winter! + tr + td + h5 Birthday Bash Badge + p Finally, to commemorate the fun, all party participants receive a birthday badge! Polish it frequently and wear it fondly. + p Thanks so much for being a part of the HabitRPG community. We love you guys, and we can't wait to have you at our sides in the upcoming year! Stay productive, Habiteers, and have an awesome day. + p.muted By @lemoness - small.muted 01/28/2014 + small.muted 01/30/2014 + + hr + h5 01/28/2014 + table.table.table-striped + tr + td + h5 Group Plans + p We've begun adding plans for groups (parents, teachers, health & wellness administrators, etc). These plans will provide group leaders with more control, privacy, security, and support. Currently only the Organization Plan (top tier) is available (due to tech limitations believe it or not), and we'll be releasing the Family & Group plans later. Click the "Contact Us" buttons if you're interested, and we'll keep you updated! + tr + td + h5 Individual Plan + p We've introduced a $5/mo basic subscription plan. It comes with a number of perks, which you can see here. We'll likely add more benefits over time, follow the conversation here. + tr + td + h5 Perfect Day Achievement + p Now when you complete all your dailies, you stack this badge, plus and additional perk: you get a +(level/2) buff to all stats! + tr + td + h5 Spread The Word Challenge Update + p We have 1k+ submissions, holy cow! Great job everyone! Now, we need to go through these manually, so it will take a few days to a couple weeks to process. The challenge will stay open until we're done choosing our winners, but be sure to edit the To-Do with your submission URL before 1/31, as that's the cut-off date for processing. We'll send a Tweet out when the winner has been selected, so follow @habitrpg and stay tuned. hr h5 01/25/2014 diff --git a/views/shared/profiles/achievements.jade b/views/shared/profiles/achievements.jade index a2628a7801..2ef6789a86 100644 --- a/views/shared/profiles/achievements.jade +++ b/views/shared/profiles/achievements.jade @@ -123,3 +123,11 @@ small =env.t('annoyingFriendsText', {snowballs: "{{profile.achievements.snowball}}"}) hr + + div(ng-if='profile.achievements.habitBirthday') + .achievement.achievement-habitBirthday + h5=env.t('habitBirthday') + small + =env.t('habitBirthdayText') + hr + From 5b6c4427b504b6143f24bfee314f562b9803c5a4 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 21:11:11 -0700 Subject: [PATCH 20/32] fix(groups): send error if +1 errored --- migrations/20140130_birthdayStart.js | 1 - src/controllers/groups.js | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/20140130_birthdayStart.js b/migrations/20140130_birthdayStart.js index 458f699c5c..f022168bcd 100644 --- a/migrations/20140130_birthdayStart.js +++ b/migrations/20140130_birthdayStart.js @@ -1,7 +1,6 @@ db.users.update({},{$set:{ 'items.food.Cake_Skeleton':1, 'items.food.Cake_Base':1, - 'items.food.Cake_Base':1, 'items.food.Cake_CottonCandyBlue':1, 'items.food.Cake_CottonCandyPink':1, 'items.food.Cake_Shade':1, diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 254d8e7b2f..6cc090ea5d 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -264,6 +264,7 @@ api.likeChatMessage = function(req, res) { } group.markModified('chat'); group.save(function(err,_saved){ + if (err) return next(err); return res.send(_saved.chat); }) } From f967962996be69a5335454610af76d10e1db08b8 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 21:33:28 -0700 Subject: [PATCH 21/32] fix(batch-update): send errors to client if batch-update found an error, crash pid. --- src/controllers/user.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index 063aa5cceb..89116bd3ac 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -472,9 +472,8 @@ api.batchUpdate = function(req, res, next) { var callOp = function(_req, cb) { res.send = res.json = function(code, data) { - if (_.isNumber(code) && code >= 400) - console.error({code: code, data: data}); - //FIXME send error messages down + if (_.isNumber(code) && code >= 500) + return cb(code+": "+ (data.message ? data.message : data.err ? data.err : JSON.stringify(data))); return cb(); }; api[_req.op](_req, res); @@ -495,7 +494,7 @@ api.batchUpdate = function(req, res, next) { async.series(ops, function(err) { res.json = oldJson; res.send = oldSend; - if (err) return res.json(500, {err: err}); + if (err) return next(err); var response = user.toJSON(); response.wasModified = res.locals.wasModified; From 0a0c6c73a541ebce595ace837d744ec0cc09fba1 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 21:34:06 -0700 Subject: [PATCH 22/32] chore(bailey): fix bailey notifs to say Jan 31 for cut off dates @lemoness --- views/shared/modals/new-stuff.jade | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/views/shared/modals/new-stuff.jade b/views/shared/modals/new-stuff.jade index e2a584d133..7349015eb1 100644 --- a/views/shared/modals/new-stuff.jade +++ b/views/shared/modals/new-stuff.jade @@ -30,7 +30,7 @@ div(modal='modals.newStuff') tr td h5 Happy Birthday, HabitRPG! - p The fair land of Habitica is two years old today! The NPCs are celebrating in style, and it looks like some of the staff is, too! Won't you join in? + p The fair land of Habitica is two years old on January 31st! The NPCs are celebrating in style, and it looks like some of the staff is, too! Won't you join in? tr td h5 Absurd Party Robtes @@ -42,7 +42,7 @@ div(modal='modals.newStuff') tr td h5 Last Day of Winter Wonderland Event - p Also, just a reminder - this is the final day of the Winter Wonderland event, so it's your last day to get the Limited Edition Winter Hair Colors, the Winter Outfits, the snowballs, and the Trapper Santa and Find the Cub quest scrolls. (Remember that if you are in the middle of a Trapper Santa or Find the Cub Quest, it will continue! Only the scrolls will disappear.) We hope that you've had a wonderful winter! + p Also, just a reminder - January 31st is the final day of the Winter Wonderland event, so it's your last day to get the Limited Edition Winter Hair Colors, the Winter Outfits, the snowballs, and the Trapper Santa and Find the Cub quest scrolls. Remember that mid-progress Trapper Santa and Find the Cub quests will not abort, nor will you lose your scrolls - they will simply be removed from Alexander's marketplace. We hope that you've had a wonderful winter! tr td h5 Birthday Bash Badge From 8e86fddc64a228bb5e0db7c3e93ca9d5b99d49e5 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 21:34:14 -0700 Subject: [PATCH 23/32] Revert "chore(cluster): don't kill process until email sent" This reverts commit 52df5341967ef294da75e5853d1c9a188088e5c1. --- src/utils.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/utils.js b/src/utils.js index 8e2b326936..b8a15cd26d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -3,7 +3,7 @@ var nconf = require('nconf'); var crypto = require('crypto'); var path = require("path"); -module.exports.sendEmail = function(mailData, cb) { +module.exports.sendEmail = function(mailData) { var smtpTransport = nodemailer.createTransport("SMTP",{ service: nconf.get('SMTP_SERVICE'), auth: { @@ -15,7 +15,6 @@ module.exports.sendEmail = function(mailData, cb) { if(error) console.log(error); else console.log("Message sent: " + response.message); smtpTransport.close(); // shut down the connection pool, no more messages - if (cb) cb(); }); } @@ -70,16 +69,15 @@ module.exports.errorHandler = function(err, req, res, next) { "\n\nheaders: " + JSON.stringify(req.headers) + "\n\nbody: " + JSON.stringify(req.body) + (res.locals.ops ? "\n\ncompleted ops: " + JSON.stringify(res.locals.ops) : ""); - var shortMessage = (err.message.length < 200) ? err.message : - err.message.substring(0,100) + err.message.substring(err.message.length-100,err.message.length); - res.json(500,{err:shortMessage}); //res.end(err.message); - console.error(stack); module.exports.sendEmail({ from: "HabitRPG <" + nconf.get('SMTP_USER') + ">", to: nconf.get('ADMIN_EMAIL') || nconf.get('SMTP_USER'), subject: "HabitRPG Error", text: stack - }, function(){ - process.exit(0); }); + console.error(stack); + var shortMessage = (err.message.length < 200) ? err.message : + err.message.substring(0,100) + err.message.substring(err.message.length-100,err.message.length); + res.json(500,{err:shortMessage}); //res.end(err.message); + process.exit(0); } \ No newline at end of file From 0aec4caa785c3b12e15f1c2e19c5b67b20d1a6e1 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 21:39:19 -0700 Subject: [PATCH 24/32] fix(user): PUT user retricted path errors are 401, not 500 --- src/controllers/user.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index 89116bd3ac..346adf1a31 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -189,8 +189,8 @@ api.update = function(req, res, next) { return true; }); user.save(function(err) { - if (!_.isEmpty(errors)) return res.json(500, {err: errors}); - if (err) {return res.json(500, {err: err})} + if (!_.isEmpty(errors)) return res.json(401, {err: errors}); + if (err) return res.json(500, {err: err}); res.json(200, user); }); }; From a960c63f00f090055049bc4fefd6cbc3157c2d04 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 21:53:23 -0700 Subject: [PATCH 25/32] refactor(groups): small groups-creation cleanup --- src/controllers/groups.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 6cc090ea5d..470209170e 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -147,16 +147,15 @@ api.create = function(req, res, next) { group.balance = 1; user.balance--; - user.save(function(err){ - if(err) return res.json(500,{err:err}); - group.save(function(err, saved){ - if (err) return res.json(500,{err:err}); - saved.populate('members', nameFields, function(err, populated){ - if (err) return res.json(500,{err:err}); - return res.json(populated); - }); - }); - }); + async.waterfall([ + function(cb){user.save(cb)}, + function(saved,ct,cb){group.save(cb)}, + function(saved,ct,cb){saved.populate('members',nameFields,cb)} + ],function(err,saved){ + if (err) return next(err); + res.json(saved); + }); + }else{ async.waterfall([ function(cb){ From b42dacf2035d62453b585cfcf453829a423b59de Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 22:13:51 -0700 Subject: [PATCH 26/32] fix(errors): handle if err.message == undefined, send err --- src/utils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils.js b/src/utils.js index b8a15cd26d..eacb9e7c59 100644 --- a/src/utils.js +++ b/src/utils.js @@ -76,8 +76,8 @@ module.exports.errorHandler = function(err, req, res, next) { text: stack }); console.error(stack); - var shortMessage = (err.message.length < 200) ? err.message : - err.message.substring(0,100) + err.message.substring(err.message.length-100,err.message.length); - res.json(500,{err:shortMessage}); //res.end(err.message); + var message = err.message ? err.message : err; + message = (message.length < 200) ? message : message.substring(0,100) + message.substring(message.length-100,message.length); + res.json(500,{err:message}); //res.end(err.message); process.exit(0); } \ No newline at end of file From afee0968f8f6923847e186d3e11b9745ced9606e Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 30 Jan 2014 23:31:58 -0700 Subject: [PATCH 27/32] fix(groups): pass missing next into Like function --- src/controllers/groups.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 470209170e..53d0a6f040 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -249,7 +249,7 @@ api.deleteChatMessage = function(req, res){ }); } -api.likeChatMessage = function(req, res) { +api.likeChatMessage = function(req, res, next) { var user = res.locals.user; var group = res.locals.group; var message = _.find(group.chat, {id: req.params.mid}); From ab86ba11bdb3379a8d8fa1814879640d61c57227 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 Jan 2014 10:27:02 -0700 Subject: [PATCH 28/32] fix(user): if need to upgrade site, send 501, not 400 --- src/controllers/user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index 346adf1a31..bd167da816 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -464,7 +464,7 @@ _.each(shared.wrap({}).ops, function(op,k){ api.batchUpdate = function(req, res, next) { if (_.isEmpty(req.body)) req.body = []; // cases of {} or null if (req.body[0] && req.body[0].data) - return res.json(400, {err: "API has been updated, please refresh your browser or upgrade your mobile app."}) + return res.json(501, {err: "API has been updated, please refresh your browser or upgrade your mobile app."}) var user = res.locals.user; var oldSend = res.send; From 92563915ef786e09f28831450b88d41270b64041 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 Jan 2014 10:32:33 -0700 Subject: [PATCH 29/32] chore(throttling): add api-throttling (request limiting) so users can only request routes 120times per minute (aka, 2req/second but spread out for sanity). fixes #2645 --- package.json | 3 ++- src/middleware.js | 18 ++++++++++++++++++ src/server.js | 1 + 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 9e3e9a52d4..338485de34 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "swagger-node-express": "git://github.com/lefnire/swagger-node-express#habitrpg", "passport": "~0.1.18", "passport-facebook": "~1.0.2", - "newrelic": "~1.3.0" + "newrelic": "~1.3.0", + "connect-ratelimit": "0.0.6" }, "private": true, "subdomain": "habitrpg", diff --git a/src/middleware.js b/src/middleware.js index 829e9e2cc0..19ada70aaf 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -3,6 +3,24 @@ var _ = require('lodash'); var fs = require('fs'); var path = require('path'); var User = require('./models/user').model +var limiter = require('connect-ratelimit'); + +module.exports.apiThrottle = function(app) { + app.use(limiter({ + end:false, + catagories:{ + normal: { + // 2 req/s, but split as minutes + totalRequests: 120, + every: 60000 + } + } + })).use(function(req,res,next){ + //console.log(res.ratelimit); + if (res.ratelimit.exceeded) return res.json(429,{err:'Rate limit exceeded'}); + next(); + }); +} module.exports.forceSSL = function(req, res, next){ var baseUrl = nconf.get("BASE_URL"); diff --git a/src/server.js b/src/server.js index 3d275a0abd..eaa527ad06 100644 --- a/src/server.js +++ b/src/server.js @@ -125,6 +125,7 @@ if (cluster.isMaster && (nconf.get('NODE_ENV') == 'development' || nconf.get('NO //} // Custom Directives + middleware.apiThrottle(app); app.use(require('./routes/pages').middleware); app.use(require('./routes/auth').middleware); var v2 = express(); From 1faec250d88b538ce20f49d4e7ce09eeea0bdf78 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 Jan 2014 10:46:40 -0700 Subject: [PATCH 30/32] chore(throttling): throttle all requests, not just API requests. Since we're 120req/min, this shouldn't pose an issue for normal usage. #2645 --- src/server.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/server.js b/src/server.js index eaa527ad06..0e0f54956a 100644 --- a/src/server.js +++ b/src/server.js @@ -5,7 +5,10 @@ var nconf = require('nconf'); var utils = require('./utils'); utils.setupConfig(); -if (cluster.isMaster && (nconf.get('NODE_ENV') == 'development' || nconf.get('NODE_ENV') == 'production')) { +var isProd = nconf.get('NODE_ENV') === 'production'; +var isDev = nconf.get('NODE_ENV') === 'development'; + +if (cluster.isMaster && (isDev || isProd)) { // Fork workers. _.times(require('os').cpus().length, function(){ cluster.fork(); @@ -13,8 +16,7 @@ if (cluster.isMaster && (nconf.get('NODE_ENV') == 'development' || nconf.get('NO cluster.on('exit', function(worker, code, signal) { var w = cluster.fork(); // replace the dead worker - if (nconf.get('NODE_ENV') === 'development') - console.error('[%s] [master:%s] worker:%s disconnect! new worker:%s fork', new Date(), process.pid, worker.process.pid, w.process.pid); + console.error('[%s] [master:%s] worker:%s disconnect! new worker:%s fork', new Date(), process.pid, worker.process.pid, w.process.pid); }); } else { @@ -85,6 +87,7 @@ if (cluster.isMaster && (nconf.get('NODE_ENV') == 'development' || nconf.get('NO )); // ------------ Server Configuration ------------ + domainMiddleware({ server: server, killTimeout: 3000 @@ -92,8 +95,9 @@ if (cluster.isMaster && (nconf.get('NODE_ENV') == 'development' || nconf.get('NO app.set("port", nconf.get('PORT')); - if (nconf.get('NODE_ENV') !== 'production') - app.use(express.logger("dev")); + if (isProd) + middleware.apiThrottle(app); + if (!isProd) app.use(express.logger("dev")); app.use(express.compress()); app.set("views", __dirname + "/../views"); app.set("view engine", "jade"); @@ -115,7 +119,7 @@ if (cluster.isMaster && (nconf.get('NODE_ENV') == 'development' || nconf.get('NO app.use(app.router); - var maxAge = (nconf.get('NODE_ENV') === 'production') ? 31536000000 : 0; + var maxAge = isProd ? 31536000000 : 0; app.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge })); app.use(express['static'](path.join(__dirname, "/../public"))); @@ -125,7 +129,6 @@ if (cluster.isMaster && (nconf.get('NODE_ENV') == 'development' || nconf.get('NO //} // Custom Directives - middleware.apiThrottle(app); app.use(require('./routes/pages').middleware); app.use(require('./routes/auth').middleware); var v2 = express(); From 87d196b0383db59abc1130b4331c1c048bb08cab Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 Jan 2014 11:11:16 -0700 Subject: [PATCH 31/32] chore(throttling): throttle in dev too --- src/controllers/auth.js | 2 +- src/server.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/controllers/auth.js b/src/controllers/auth.js index dd508039ad..ab141c268e 100644 --- a/src/controllers/auth.js +++ b/src/controllers/auth.js @@ -24,7 +24,7 @@ api.auth = function(req, res, next) { var token = req.headers['x-api-key']; if (!(uid && token)) return res.json(401, NO_TOKEN_OR_UID); User.findOne({_id: uid,apiToken: token}, function(err, user) { - if (err) return res.json(500, {err: err}); + if (err) return next(err); if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND); res.locals.wasModified = req.query._v ? +user._v !== +req.query._v : true; diff --git a/src/server.js b/src/server.js index 0e0f54956a..8d95852ad0 100644 --- a/src/server.js +++ b/src/server.js @@ -95,8 +95,7 @@ if (cluster.isMaster && (isDev || isProd)) { app.set("port", nconf.get('PORT')); - if (isProd) - middleware.apiThrottle(app); + middleware.apiThrottle(app); if (!isProd) app.use(express.logger("dev")); app.use(express.compress()); app.set("views", __dirname + "/../views"); From a72b013131cfc7fa5d3affdbfe59b5b3cb15ae89 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 31 Jan 2014 11:11:42 -0700 Subject: [PATCH 32/32] fix(hall): let's try $gt instead of $ne:null, the query is still slow --- src/controllers/hall.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/hall.js b/src/controllers/hall.js index 2498112785..068efdfc3e 100644 --- a/src/controllers/hall.js +++ b/src/controllers/hall.js @@ -25,7 +25,7 @@ api.getHeroes = function(req,res,next) { api.getPatrons = function(req,res,next){ var page = req.query.page || 0, perPage = 50; - User.find({'backer.tier':{$ne:null}}) + User.find({'backer.tier':{$gt:0}}) .select('contributor backer profile.name') .sort('-backer.tier') .skip(page*perPage)