From 9efaf39b2b74b919cfa47c1514d01eb346794bd7 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 25 Oct 2014 14:44:01 -0600 Subject: [PATCH 01/84] feat(webhooks): add webhooks to user.preferences (API settings) so we can post to external services (eg, Beeminder) --- public/js/controllers/settingsCtrl.js | 14 ++++++++++++++ src/controllers/user.js | 10 ++++++++++ src/models/user.js | 3 ++- views/options/settings.jade | 28 +++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/public/js/controllers/settingsCtrl.js b/public/js/controllers/settingsCtrl.js index b7a7aca592..8802a09b4a 100644 --- a/public/js/controllers/settingsCtrl.js +++ b/public/js/controllers/settingsCtrl.js @@ -162,5 +162,19 @@ habitrpg.controller('SettingsCtrl', User.user.ops.release2({}); $rootScope.$state.go('tasks'); } + + //FIXME push this all to habitrpg-shared so we have API routes for this + $scope.addWebhook = function(webhook) { + User.set({'preferences.webhooks':User.user.preferences.webhooks.concat({url:webhook, enabled:true})}); + $scope._newWebhook = ''; + } + $scope.saveWebhook = function(webhook){ + delete webhook._editing; + User.set({'preferences.webhooks':User.user.preferences.webhooks}); + } + $scope.deleteWebhook = function($index){ + User.user.preferences.webhooks.splice($index, 1); + User.set({'preferences.webhooks':User.user.preferences.webhooks}); + } } ]); diff --git a/src/controllers/user.js b/src/controllers/user.js index e2e7ec6b38..5b41cc78da 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -14,6 +14,7 @@ var moment = require('moment'); var logging = require('./../logging'); var acceptablePUTPaths; var api = module.exports; +var request = require('request'); // api.purchase // Shared.ops @@ -106,6 +107,15 @@ api.score = function(req, res, next) { _tmp: user._tmp }, saved.toJSON().stats)); + // Webhooks + _.each(user.preferences.webhooks, function(h){ + request.post({ + url: h.url, + //form: {task: task, delta: delta, user: _.pick(user, ['stats', '_tmp'])} // this is causing "Maximum Call Stack Exceeded" + body: {direction:direction, task: task, delta: delta, user: _.pick(user, ['_id', 'stats', '_tmp'])}, json:true + }); + }); + if ( (!task.challenge || !task.challenge.id || task.challenge.broken) // If it's a challenge task, sync the score. Do it in the background, we've already sent down a response and the user doesn't care what happens back there || (task.type == 'reward') // we don't want to update the reward GP cost diff --git a/src/models/user.js b/src/models/user.js index a0c5913045..f0cf5ee98f 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -274,7 +274,8 @@ var UserSchema = new Schema({ tagsCollapsed: {type: Boolean, 'default': false}, advancedCollapsed: {type: Boolean, 'default': false}, toolbarCollapsed: {type:Boolean, 'default':false}, - background: String + background: String, + webhooks: {type:Array, 'default': []} }, profile: { blurb: String, diff --git a/views/options/settings.jade b/views/options/settings.jade index f8f186c1fd..ed160a36d0 100644 --- a/views/options/settings.jade +++ b/views/options/settings.jade @@ -150,6 +150,34 @@ script(type='text/ng-template', id='partials/options.settings.api.html') 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') + hr + + h2 Webhooks + table.table.table-striped + thead(ng-if='user.preferences.webhooks[0]') + tr + th Enabled + th Webhook URL + th + tbody + tr(ng-repeat='webhook in user.preferences.webhooks') + td + input(type='checkbox', ng-model='webhook.enabled', ng-change='saveWebhook(webhook)') + td + input.form-control(type='url', ng-model='webhook.url', ng-change='webhook._editing=true', ui-keyup="{13:'saveWebhook(webhook)'}") + td + span.pull-left(ng-show='webhook._editing') * + a.checklist-icons(ng-click='deleteWebhook($index)') + span.glyphicon.glyphicon-trash(tooltip=env.t('delete')) + tr + td(colspan=2) + form.form-horizontal(ng-submit='addWebhook(_newWebhook)') + .form-group.col-sm-10 + input.form-control(type='url', ng-model='_newWebhook', placeholder='Webhook URL') + .col-sm-2 + button.btn.btn-sm.btn-primary(type='submit') Add + + script(id='partials/options.settings.export.html', type="text/ng-template") .container-fluid .row From 372869d709dc12232996cb64a615c99c708dcd7d Mon Sep 17 00:00:00 2001 From: Alice Harris Date: Sat, 1 Nov 2014 11:03:01 +1000 Subject: [PATCH 02/84] enable feature to block chat until user agrees to guidelines --- views/options/social/chat-box.jade | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/views/options/social/chat-box.jade b/views/options/social/chat-box.jade index 7953e7b1b6..a4224b92b8 100644 --- a/views/options/social/chat-box.jade +++ b/views/options/social/chat-box.jade @@ -1,18 +1,16 @@ -//div.chat-form.guidelines-not-accepted(ng-if='!user.flags.communityGuidelinesAccepted') -// p If you would like to post messages in the Tavern or any party or guild chat, please first read our -// |  -// a(target='_blank', href='/static/community-guidelines')=env.t('communityGuidelines') -// | and then click the button below to indicate that you accept them. -// .chat-controls -// div -// button.btn.btn-warning(ng-click='acceptCommunityGuidelines()')=env.t('iAcceptCommunityGuidelines') -// .chat-buttons -// button(type="button", ng-click='sync(group)', tooltip=env.t('toolTipMsg')) -// span.glyphicon.glyphicon-refresh +div.chat-form.guidelines-not-accepted(ng-if='!user.flags.communityGuidelinesAccepted') + p If you would like to post messages in the Tavern or any party or guild chat, please first read our + |  + a(target='_blank', href='/static/community-guidelines')=env.t('communityGuidelines') + | and then click the button below to indicate that you accept them. + .chat-controls + div + button.btn.btn-warning(ng-click='acceptCommunityGuidelines()')=env.t('iAcceptCommunityGuidelines') + .chat-buttons + button(type="button", ng-click='sync(group)', tooltip=env.t('toolTipMsg')) + span.glyphicon.glyphicon-refresh -//form.chat-form(ng-if='user.flags.communityGuidelinesAccepted' ng-submit='postChat(group,message.content)') -//////////// When we want to block the ability to chat until the Community Guidelines have been accepted, delete the one line immediately below this comment and un-comment all lines above. -form.chat-form(ng-submit='postChat(group,message.content)') +form.chat-form(ng-if='user.flags.communityGuidelinesAccepted' ng-submit='postChat(group,message.content)') div(ng-controller='AutocompleteCtrl') textarea.form-control(rows=4, ui-keypress='{13:"postChat(group,message.content)"}', ng-model='message.content', updateinterval='250', flag='@', at-user, auto-complete) span.user-list(ng-show='!isAtListHidden') From 876d1d435d18c1cef79b26ecf09d8caae84760c9 Mon Sep 17 00:00:00 2001 From: Alice Harris Date: Sat, 1 Nov 2014 11:42:48 +1000 Subject: [PATCH 03/84] change Fetch Recent Messages from tooltip to button text --- views/options/social/chat-box.jade | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/views/options/social/chat-box.jade b/views/options/social/chat-box.jade index 7953e7b1b6..df79a454b0 100644 --- a/views/options/social/chat-box.jade +++ b/views/options/social/chat-box.jade @@ -24,5 +24,4 @@ form.chat-form(ng-submit='postChat(group,message.content)') include ../../shared/formatting-help .chat-buttons input(type='submit', value=env.t('sendChat'), ng-class='{disabled: _sending == true}') - button(type="button", ng-click='sync(group)', tooltip=env.t('toolTipMsg')) - span.glyphicon.glyphicon-refresh + button(type="button", ng-click='sync(group)')=env.t('toolTipMsg') From 62f71fbdb81214f857ff53ac95ac3f029ab6af92 Mon Sep 17 00:00:00 2001 From: Alice Harris Date: Tue, 4 Nov 2014 16:43:27 +1000 Subject: [PATCH 04/84] merge strings to ease translations --- views/static/community-guidelines.jade | 239 +++++-------------------- 1 file changed, 43 insertions(+), 196 deletions(-) diff --git a/views/static/community-guidelines.jade b/views/static/community-guidelines.jade index 8fcf9a259a..845e3be458 100644 --- a/views/static/community-guidelines.jade +++ b/views/static/community-guidelines.jade @@ -42,22 +42,10 @@ block content div(class='clearfix') img(class='pull-right', src='/community-guidelines-images/beingHabitican.png', alt='') ul - li - strong=env.t('commGuideList01A') - |  - =env.t('commGuideList01Apart2') - li - strong=env.t('commGuideList01B') - |  - =env.t('commGuideList01Bpart2') - li - strong=env.t('commGuideList01C') - |  - =env.t('commGuideList01Cpart2') - li - strong=env.t('commGuideList01D') - |  - =env.t('commGuideList01Dpart2') + li!=env.t('commGuideList01A') + li!=env.t('commGuideList01B') + li!=env.t('commGuideList01C') + li!=env.t('commGuideList01D') h2=env.t('commGuideHeadingMeet') p=env.t('commGuidePara006') @@ -109,12 +97,7 @@ block content strong Megan li strong Daniel the Bard - - p=env.t('commGuidePara012') - | ( - a(href='mailto:leslie@habitrpg.com') leslie@habitrpg.com - |). - + p!=env.t('commGuidePara012') p=env.t('commGuidePara013') p=env.t('commGuidePara014') |  @@ -124,49 +107,17 @@ block content img(class='pull-right', src='/community-guidelines-images/publicSpaces.png', alt='') p=env.t('commGuidePara015') p=env.t('commGuidePara016') - p - strong=env.t('commGuidePara017') - |  - =env.t('commGuidePara017part2') + p!=env.t('commGuidePara017') ul - li - strong=env.t('commGuideList02A') - li - strong=env.t('commGuideList02B') - |  - =env.t('commGuideList02Bpart2') - li - strong=env.t('commGuideList02C') - |  - =env.t('commGuideList02Cpart2') - li - strong=env.t('commGuideList02D') - |  - =env.t('commGuideList02Dpart2') - li - strong=env.t('commGuideList02E') - |  - =env.t('commGuideList02Epart2') - li - strong=env.t('commGuideList02F') - |  - =env.t('commGuideList02Fpart2') - li - strong=env.t('commGuideList02G') - |  - =env.t('commGuideList02Gpart2') - li - strong=env.t('commGuideList02H') - |  - =env.t('commGuideList02Hpart2') - |  - a(href='mailto:leslie@habitrpg.com') leslie@habitrpg.com - |  - =env.t('commGuideList02Hpart3') - p - strong=env.t('commGuidePara019') - |  - =env.t('commGuidePara019part2') + li!=env.t('commGuideList02A') + li!=env.t('commGuideList02B') + li!=env.t('commGuideList02C') + li!=env.t('commGuideList02D') + li!=env.t('commGuideList02E') + li!=env.t('commGuideList02F') + li!=env.t('commGuideList02G') + li!=env.t('commGuideList02H') + p!=env.t('commGuidePara019') p=env.t('commGuidePara021') h3=env.t('commGuideHeadingTavern') @@ -175,112 +126,43 @@ block content p=env.t('commGuidePara022') p strong=env.t('commGuidePara023') - p=env.t('commGuidePara024') - |  - strong=env.t('commGuidePara024part2') - |  - =env.t('commGuidePara024part3') - p - strong=env.t('commGuidePara027') - |  - =env.t('commGuidePara027part2') + p!=env.t('commGuidePara024') + p!=env.t('commGuidePara027') h3=env.t('commGuideHeadingPublicGuilds') div(class='clearfix') img(class='pull-right', src='/community-guidelines-images/publicGuilds.png', alt='') - p - strong=env.t('commGuidePara029') - |  - =env.t('commGuidePara029part2') - |  - strong=env.t('commGuidePara029part3') - p - strong=env.t('commGuidePara031') - |  - =env.t('commGuidePara031part2') - p - strong=env.t('commGuidePara033') - |  - =env.t('commGuidePara033part2') - |  - a(href='mailto:leslie@habitrpg.com') leslie@habitrpg.com - |  - =env.t('commGuidePara033part3') - p - strong=env.t('commGuidePara035') - |  - =env.t('commGuidePara035part2') + p!=env.t('commGuidePara029') + p!=env.t('commGuidePara031') + p!=env.t('commGuidePara033') + p!=env.t('commGuidePara035') p strong=env.t('commGuidePara037') h3=env.t('commGuideHeadingBackCorner') div(class='clearfix') img(class='pull-left', src='/community-guidelines-images/backCorner.png', alt='') - p - strong=env.t('commGuidePara038') - |  - =env.t('commGuidePara038part2') - p=env.t('commGuidePara039') - |  - strong=env.t('commGuidePara039part2') - |  - =env.t('commGuidePara039part3') + p!=env.t('commGuidePara038') + p!=env.t('commGuidePara039') h3=env.t('commGuideHeadingTrello') div(class='clearfix') img(class='pull-right', src='/community-guidelines-images/trello.png', alt='') - p - strong=env.t('commGuidePara040') - |  - =env.t('commGuidePara040part2') - |  - strong=env.t('commGuidePara040part3') - |  - =env.t('commGuidePara040part4') + p!=env.t('commGuidePara040') p strong=env.t('commGuidePara041') ul - li=env.t('The') - |  - strong=env.t('commGuideList03A') - |  - =env.t('commGuideList03Apart2') - li=env.t('The') - |  - strong=env.t('commGuideList03B') - |  - =env.t('commGuideList03Bpart2') - li=env.t('The') - |  - strong=env.t('commGuideList03C') - |  - =env.t('commGuideList03Cpart2') - li=env.t('The') - |  - strong=env.t('commGuideList03D') - |  - =env.t('commGuideList03Dpart2') - li=env.t('The') - |  - strong=env.t('commGuideList03E') - |  - =env.t('commGuideList03Epart2') - p - strong=env.t('commGuidePara042') - |  - =env.t('commGuidePara042part2') + li!=env.t('commGuideList03A') + li!=env.t('commGuideList03B') + li!=env.t('commGuideList03C') + li!=env.t('commGuideList03D') + li!=env.t('commGuideList03E') + p!=env.t('commGuidePara042') h3=env.t('commGuideHeadingGitHub') div(class='clearfix') img(class='pull-left', src='/community-guidelines-images/github.gif', alt='') - p - strong=env.t('commGuidePara043') - |  - =env.t('commGuidePara043part2') - |  - strong=env.t('commGuidePara043part3') - |  - =env.t('commGuidePara043part4') + p!=env.t('commGuidePara043') p strong=env.t('commGuidePara044') ul(class='listColumns2 peopleList') @@ -304,10 +186,7 @@ block content h3=env.t('commGuideHeadingWiki') div(class='clearfix') img(class='pull-right', src='/community-guidelines-images/wiki.png', alt='') - p - strong=env.t('commGuidePara045') - |  - =env.t('commGuidePara045part2') + p!=env.t('commGuidePara045') p=env.t('commGuidePara046') p strong=env.t('commGuidePara047') @@ -338,10 +217,7 @@ block content div(class='clearfix') img(class='pull-left', src='/community-guidelines-images/infractions.png', alt='') p=env.t('commGuidePara050') - p - strong=env.t('commGuidePara051') - |  - =env.t('commGuidePara051part2') + p!=env.t('commGuidePara051') h4=env.t('commGuideHeadingSevereInfractions') p=env.t('commGuidePara052') p=env.t('commGuidePara053') @@ -355,10 +231,7 @@ block content p=env.t('commGuidePara054') p=env.t('commGuidePara055') ul - li=env.t('commGuideList06A') - | ( - a(href='mailto:leslie@habitrpg.com') leslie@habitrpg.com - |). + li!=env.t('commGuideList06A') li=env.t('commGuideList06B') li=env.t('commGuideList06C') li=env.t('commGuideList06D') @@ -373,10 +246,7 @@ block content div(class='clearfix') img(class='pull-right', src='/community-guidelines-images/consequences.png', alt='') p=env.t('commGuidePara058') - p - strong=env.t('commGuidePara059') - |  - =env.t('commGuidePara059part2') + p!=env.t('commGuidePara059') p strong=env.t('commGuidePara060') ul @@ -407,16 +277,9 @@ block content h3=env.t('commGuideHeadingRestoration') div(class='clearfix') img(class='pull-left', src='/community-guidelines-images/restoration.png', alt='') - p=env.t('commGuidePara061') - |  - strong=env.t('commGuidePara061part2') - p=env.t('commGuidePara062') - |  - strong=env.t('commGuidePara062part2') - p - strong=env.t('commGuidePara063') - |  - =env.t('commGuidePara063part2') + p!=env.t('commGuidePara061') + p!=env.t('commGuidePara062') + p!=env.t('commGuidePara063') h2=env.t('commGuideHeadingContributing') div(class='clearfix') @@ -433,29 +296,13 @@ block content p=env.t('commGuidePara065') p=env.t('commGuidePara066') ul - li - strong=env.t('commGuideList13A') - |  - =env.t('commGuideList13Apart2') - li - strong=env.t('commGuideList13B') - |  - =env.t('commGuideList13Bpart2') - li - strong=env.t('commGuideList13C') - |  - =env.t('commGuideList13Cpart2') - li - strong=env.t('commGuideList13D') - |  - =env.t('commGuideList13Dpart2') + li!=env.t('commGuideList13A') + li!=env.t('commGuideList13B') + li!=env.t('commGuideList13C') + li!=env.t('commGuideList13D') h2=env.t('commGuideHeadingFinal') - p=env.t('commGuidePara067') - | ( - a(href='mailto:leslie@habitrpg.com') leslie@habitrpg.com - |)  - =env.t('commGuidePara067part2') + p!=env.t('commGuidePara067') p=env.t('commGuidePara068') h2=env.t('commGuideHeadingLinks') From ee879decb974ba316d1aec9fa6d183a35c2dcfc7 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 4 Nov 2014 18:26:31 -0700 Subject: [PATCH 05/84] chore(upgrade): passport-facebook --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3a8e57c1ce..16b1adfd8b 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "nib": "~1.0.1", "nodemailer": "~0.5.2", "passport": "~0.2.1", - "passport-facebook": "~1.0.2", + "passport-facebook": "~1.0.3", "paypal-express-checkout": "git://github.com/HabitRPG/node-paypal-express-checkout#habitrpg", "paypal-ipn": "~1.0.1", "paypal-recurring": "git://github.com/jaybryant/paypal-recurring#656b496f43440893c984700191666a5c5c535dca", From 94a813fc6be17c262e12bd33255fbce7bd3d2c1b Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 4 Nov 2014 19:50:56 -0700 Subject: [PATCH 06/84] bailey: backgrounds, beeminder --- views/shared/new-stuff.jade | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/views/shared/new-stuff.jade b/views/shared/new-stuff.jade index 84e4b24079..a661981979 100644 --- a/views/shared/new-stuff.jade +++ b/views/shared/new-stuff.jade @@ -8,14 +8,28 @@ table h3.popover-title a(target='_blank', href='https://twitter.com/Mihakuu') Bailey .popover-content + h5 November Backgrounds And Beeminder Integration table.table.table-striped tr td - h5 November Mystery Item Set - .pull-right.inventory_present - p Cool! What could it be? All Habiticans who are subscribed during the month of November will receive the November Mystery Item Set! It will be revealed on the 25th, so keep your eyes peeled. Thanks for supporting the site <3 - p.small.muted 12/01/2014 + h5 November Backgrounds + p There are three new avatar backgrounds in the Background Shop! Now your avatar can enjoy a Harvest Feast, admire a Sunset Meadow, or gaze at the Starry Skies! + p.small.muted by Kiwibot, Holsety1, and Draayder + tr + td + h5 Beeminder Integration + p We've integrated with Beeminder! Now you can beemind your To-Dos automatically :) Check it out! + p If you've never heard of Beeminder or want to learn more about what we've integrated so far, check out our blog post about it. Enjoy! + p.small.muted by Alys and Alice Monday + p.small.muted 11/05/2014 +h5 11/01/2014 +table.table.table-striped + tr + td + h5 November Mystery Item Set + .pull-right.inventory_present + p Cool! What could it be? All Habiticans who are subscribed during the month of November will receive the November Mystery Item Set! It will be revealed on the 25th, so keep your eyes peeled. Thanks for supporting the site <3 h5 10/31/2014 - Monster Npcs, Last Day For Fall Festival Items, Last Day Of Community Costume Challenge, Last Day For Winged Goblin Item Set table.table.table-striped From cfbb564801c0de4549e50c989cd6f84e56106e5c Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Wed, 5 Nov 2014 17:56:10 +0100 Subject: [PATCH 07/84] feat(emails): add recaptureEmailsPhase path to flags --- src/models/user.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/models/user.js b/src/models/user.js index 263207d48f..0d377efa3c 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -117,6 +117,9 @@ var UserSchema = new Schema({ freeRebirth: {type: Boolean, 'default': false}, levelDrops: {type:Schema.Types.Mixed, 'default':{}}, chatRevoked: Boolean, + // Used to track the status of recapture emails sent to each user, + // can be 0 - no email sent - 1, 2 or 3 - 3 means no more email will be sent to the user + recaptureEmailsPhase: {type: Number, 'default': 0}, communityGuidelinesAccepted: {type: Boolean, 'default': false} }, history: { From 1f72721e6a093c35d9a68d05fcdd39685df827d5 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 5 Nov 2014 11:14:33 -0700 Subject: [PATCH 08/84] fix(facebook): attempt using a callback buster to generate a new authorization code #4221 --- src/server.js | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/server.js b/src/server.js index 064b88d5a6..005d307223 100644 --- a/src/server.js +++ b/src/server.js @@ -26,6 +26,7 @@ if (cluster.isMaster && (isDev || isProd)) { var path = require("path"); var swagger = require("swagger-node-express"); var autoinc = require('mongoose-id-autoinc'); + var shared = require('habitrpg-shared'); // Setup translations var i18n = require('./i18n'); @@ -65,11 +66,11 @@ if (cluster.isMaster && (isDev || isProd)) { // have a database of user records, the complete Facebook profile is serialized // and deserialized. passport.serializeUser(function(user, done) { - done(null, user); + done(null, user); }); passport.deserializeUser(function(obj, done) { - done(null, obj); + done(null, obj); }); // Use the FacebookStrategy within Passport. @@ -77,20 +78,16 @@ if (cluster.isMaster && (isDev || isProd)) { // 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" + clientID: nconf.get("FACEBOOK_KEY"), + clientSecret: nconf.get("FACEBOOK_SECRET"), + callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback?buster=" + shared.uuid() // see http://goo.gl/dwaqS9 }, 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. + done(null, profile); } )); From 8ff971b1ecacec854a77337118ab03da4e655429 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 5 Nov 2014 11:31:26 -0700 Subject: [PATCH 09/84] fix(facebook): try using facebook oauth route 2.1 #4221 --- package.json | 2 +- src/server.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 16b1adfd8b..002cd497b3 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "nib": "~1.0.1", "nodemailer": "~0.5.2", "passport": "~0.2.1", - "passport-facebook": "~1.0.3", + "passport-facebook": "Fonger/passport-facebook#a8f98adcddad99caa9a918bc7b76462c92c5c9fd", "paypal-express-checkout": "git://github.com/HabitRPG/node-paypal-express-checkout#habitrpg", "paypal-ipn": "~1.0.1", "paypal-recurring": "git://github.com/jaybryant/paypal-recurring#656b496f43440893c984700191666a5c5c535dca", diff --git a/src/server.js b/src/server.js index 005d307223..e3703a6fb3 100644 --- a/src/server.js +++ b/src/server.js @@ -80,7 +80,7 @@ if (cluster.isMaster && (isDev || isProd)) { passport.use(new FacebookStrategy({ clientID: nconf.get("FACEBOOK_KEY"), clientSecret: nconf.get("FACEBOOK_SECRET"), - callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback?buster=" + shared.uuid() // see http://goo.gl/dwaqS9 + callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback" }, function(accessToken, refreshToken, profile, done) { // To keep the example simple, the user's Facebook profile is returned to From 8651f0da25db33850f1dcd9c999da9245efbce5f Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 5 Nov 2014 16:31:52 -0700 Subject: [PATCH 10/84] fix(facebook): move from passport-facebook to Facebook JS SDK. Better security on FB login by validating accessToken. Create user from FB profile if none exists, allows 3rd-party apps to. Fixes #4221 --- bower.json | 3 +- public/js/controllers/authCtrl.js | 48 +++++++------ public/js/static.js | 9 ++- public/manifest.json | 1 + src/controllers/auth.js | 111 ++++++++++-------------------- src/middleware.js | 1 + src/server.js | 13 ++-- views/static/login-modal.jade | 3 +- 8 files changed, 78 insertions(+), 111 deletions(-) diff --git a/bower.json b/bower.json index b00287d040..9c346a0e90 100644 --- a/bower.json +++ b/bower.json @@ -40,7 +40,8 @@ "ngInfiniteScroll": "1.0.0", "jquery-colorbox": "~1.4.36", "pnotify": "~1.3.1", - "jquery-ui": "~1.10.3" + "jquery-ui": "~1.10.3", + "angular-facebook": "~0.2.3" }, "devDependencies": { "angular-mocks": "1.3.0-beta.11" diff --git a/public/js/controllers/authCtrl.js b/public/js/controllers/authCtrl.js index 64bf201a9f..8293f48c69 100644 --- a/public/js/controllers/authCtrl.js +++ b/public/js/controllers/authCtrl.js @@ -5,26 +5,15 @@ */ angular.module('authCtrl', []) - .controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$location', '$window','ApiUrlService', '$modal', - function($scope, $rootScope, User, $http, $location, $window, ApiUrlService, $modal) { - var runAuth; - var showedFacebookMessage; - - $scope.useUUID = false; - $scope.toggleUUID = function() { - if (showedFacebookMessage === false) { - alert(window.env.t('untilNoFace')); - showedFacebookMessage = true; - } - $scope.useUUID = !$scope.useUUID; - }; + .controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$location', '$window','ApiUrlService', '$modal', 'Facebook', + function($scope, $rootScope, User, $http, $location, $window, ApiUrlService, $modal, Facebook) { $scope.logout = function() { localStorage.clear(); window.location.href = '/logout'; }; - runAuth = function(id, token) { + var runAuth = function(id, token) { User.authenticate(id, token, function(err) { $window.location.href = '/'; }); @@ -59,14 +48,10 @@ angular.module('authCtrl', []) username: $scope.loginUsername || $('#login-tab input[name="username"]').val(), password: $scope.loginPassword || $('#login-tab input[name="password"]').val() }; - if ($scope.useUUID) { - runAuth($scope.loginUsername, $scope.loginPassword); - } else { - $http.post(ApiUrlService.get() + "/api/v2/user/auth/local", data) - .success(function(data, status, headers, config) { - runAuth(data.id, data.token); - }).error(errorAlert); - } + $http.post(ApiUrlService.get() + "/api/v2/user/auth/local", data) + .success(function(data, status, headers, config) { + runAuth(data.id, data.token); + }).error(errorAlert); }; $scope.playButtonClick = function(){ @@ -126,5 +111,24 @@ angular.module('authCtrl', []) $scope.hasNoNotifications = function() { return selectNotificationValue(false, false, false, false, true); } + + // ------ Facebook ---------- + // See https://developers.facebook.com/docs/facebook-login/login-flow-for-web/v2.2 for boilerplate + $scope.fbLogin = function(){ + var thenLogin = function(response){ + $http.post(ApiUrlService.get() + "/api/v2/user/auth/facebook", response.authResponse) + .success(function(data, status, headers, config) { + runAuth(data.id, data.token); + }).error(errorAlert); + } + Facebook.getLoginStatus(function(response) { + if (response.status === 'connected') { + thenLogin(response); + } else { + Facebook.login(thenLogin) + } + + }); + } } ]); diff --git a/public/js/static.js b/public/js/static.js index 84894ec5ab..606c7d18d7 100644 --- a/public/js/static.js +++ b/public/js/static.js @@ -1,18 +1,23 @@ "use strict"; -window.habitrpgStatic = angular.module('habitrpgStatic', ['notificationServices', 'userServices', 'chieffancypants.loadingBar', 'authCtrl', 'ui.bootstrap']) +window.habitrpgStatic = angular.module('habitrpgStatic', ['notificationServices', 'userServices', 'chieffancypants.loadingBar', 'authCtrl', 'ui.bootstrap', 'facebook']) .constant("API_URL", "") .constant("STORAGE_USER_ID", 'habitrpg-user') .constant("STORAGE_SETTINGS_ID", 'habit-mobile-settings') .constant("MOBILE_APP", false) -habitrpgStatic.controller("PlansCtrl", ['$rootScope', +.config(['FacebookProvider', function(FacebookProvider){ + FacebookProvider.init(window.env.FACEBOOK_KEY); +}]) + +.controller("PlansCtrl", ['$rootScope', function($rootScope) { $rootScope.clickContact = function(){ window.ga && ga('send', 'event', 'button', 'click', 'Contact Us (Plans)'); } } ]) + .controller('AboutCtrl',[function(){ $(document).ready(function(){ $('a.gallery').colorbox({ diff --git a/public/manifest.json b/public/manifest.json index e44d2b2d0a..59628b67aa 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -83,6 +83,7 @@ "bower_components/angular-bootstrap/ui-bootstrap-tpls.js", "bower_components/bootstrap/dist/js/bootstrap.js", "bower_components/jquery-colorbox/jquery.colorbox-min.js", + "bower_components/angular-facebook/lib/angular-facebook.js", "bower_components/angular-loading-bar/build/loading-bar.js", "js/env.js", diff --git a/src/controllers/auth.js b/src/controllers/auth.js index eae795a3ea..2a3c2fd83f 100644 --- a/src/controllers/auth.js +++ b/src/controllers/auth.js @@ -168,22 +168,43 @@ api.loginLocal = function(req, res, next) { /* POST /user/auth/facebook */ - - api.loginFacebook = function(req, res, next) { - var facebook_id = req.body.facebook_id; - if (!facebook_id) return res.json(401, {err: 'No facebook id provided'}); - User.findOne({'auth.facebook.id': facebook_id}, function(err, user) { - if (err) { - return res.json(401, {err: err}); - } else if (user) { - if (user.auth.blocked) return res.json(401, accountSuspended(user._id)); - return res.json(200, {id: user.id,token: user.apiToken}); - } else { - /* FIXME: create a new user instead*/ - return res.json(403, {err: "Please register with Facebook on https://habitrpg.com, then come back here and log in."}); + var accessToken = req.body.accessToken; + async.waterfall([ + function(cb){ + // TODO is this private function here safe to use? + passport._strategies.facebook.userProfile(accessToken, cb); + }, + function(profile, cb) { + User.findOne({'auth.facebook.id': profile.id}, {_id:1, apiToken:1, auth:1}, function(err, user){ + if (err) return cb(err); + cb(null, {user:user, profile:profile}); + }); + }, + function(data, cb){ + if (data.user) return cb(null, data.user); + // Create new user + var prof = data.profile; + var user = new User({ + preferences: { + language: req.language // User language detected from browser, not saved + }, + auth: { + facebook: prof, + timestamps: {created: +new Date(), loggedIn: +new Date()} + } + }); + user.save(cb); + if(isProd && prof.emails && prof.emails[0] && prof.emails[0].value){ + emailUser((prof.displayName || prof.username), prof.emails[0].value, 'welcome'); + } + ga.event('register', 'Facebook').send(); } - }); + ], function(err, user){ + if (err) return res.json(401, {err: err.toString ? err.toString() : err}); + if (user.auth.blocked) return res.json(401, accountSuspended(user._id)); + return res.json(200, {id: user.id, token:user.apiToken}); + }) }; api.resetPassword = function(req, res, next){ @@ -271,66 +292,4 @@ api.setupPassport = function(router) { res.redirect('/'); }) - // GET /auth/facebook - // Use passport.authenticate() as route middleware to authenticate the - // request. The first step in Facebook authentication will involve - // redirecting the user to facebook.com. After authorization, Facebook will - // redirect the user back to this application at /auth/facebook/callback - router.get('/auth/facebook', - passport.authenticate('facebook', {scope: 'email'}), - i18n.getUserLanguage, - function(req, res){ - // The request will be redirected to Facebook for authentication, so this - // function will not be called. - }); - - // GET /auth/facebook/callback - // Use passport.authenticate() as route middleware to authenticate the - // request. If authentication fails, the user will be redirected back to the - // login page. Otherwise, the primary route function function will be called, - // which, in this example, will redirect the user to the home page. - router.get('/auth/facebook/callback', - passport.authenticate('facebook', { failureRedirect: '/login' }), - i18n.getUserLanguage, - function(req, res) { - //res.redirect('/'); - - async.waterfall([ - function(cb){ - User.findOne({'auth.facebook.id':req.user.id}, cb) - }, - function(user, cb){ - if (user) return cb(null, user); - - user = new User({ - preferences: { - language: req.language // User language detected from browser, not saved - }, - auth: { - facebook: req.user, - timestamps: {created: +new Date(), loggedIn: +new Date()} - } - }); - user.save(cb); - if(isProd && req.user.emails && req.user.emails[0] && req.user.emails[0].value){ - emailUser((req.user.displayName || req.user.username), req.user.emails[0].value, 'welcome'); - } - ga.event('register', 'Facebook').send() - } - ], function(err, saved){ - if (err) return res.redirect('/static/front?err=' + err); - req.session.userId = saved._id; - res.redirect('/static/front?_id='+saved._id+'&apiToken='+saved.apiToken); - }) - }); - - // Simple route middleware to ensure user is authenticated. - // Use this route middleware on any resource that needs to be protected. If - // the request is authenticated (typically via a persistent login session), - // the request will proceed. Otherwise, the user will be redirected to the - // login page. -// function ensureAuthenticated(req, res, next) { -// if (req.isAuthenticated()) { return next(); } -// res.redirect('/login') -// } }; diff --git a/src/middleware.js b/src/middleware.js index 1781ef2394..4f4bece349 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -192,6 +192,7 @@ module.exports.locals = function(req, res, next) { siteVersion: siteVersion, Content: shared.content, mods: require('./models/user').mods, + FACEBOOK_KEY: nconf.get('FACEBOOK_KEY'), tavern: tavern, // for world boss worldDmg: (tavern && tavern.quest && tavern.quest.extra && tavern.quest.extra.worldDmg) || {} diff --git a/src/server.js b/src/server.js index e3703a6fb3..98e9fc0040 100644 --- a/src/server.js +++ b/src/server.js @@ -73,20 +73,15 @@ if (cluster.isMaster && (isDev || isProd)) { 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. + // FIXME + // This auth strategy is no longer used. It's just kept around for auth.js#loginFacebook() (passport._strategies.facebook.userProfile) + // The proper fix would be to move to a general OAuth module simply to verify accessTokens passport.use(new FacebookStrategy({ clientID: nconf.get("FACEBOOK_KEY"), clientSecret: nconf.get("FACEBOOK_SECRET"), - callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback" + //callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback" }, function(accessToken, refreshToken, profile, done) { - // 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. done(null, profile); } )); diff --git a/views/static/login-modal.jade b/views/static/login-modal.jade index 5332b972b8..1382914049 100644 --- a/views/static/login-modal.jade +++ b/views/static/login-modal.jade @@ -3,8 +3,9 @@ script(id='modals/login.html', type='text/ng-template') button.close(type='button', ng-click='$close()') × h4.modal-title=env.t('loginAndReg') .modal-body(ng-controller='AuthCtrl') - a(href='/auth/facebook') + a(href='#', ng-click='fbLogin()') img(src='/bower_components/habitrpg-shared/img/facebook-login-register.jpeg', alt=env.t('loginFacebookAlt')) + //can we add in google auth? I like google auth h3=env.t('or') ul.nav.nav-tabs From 551e5d4e838674aef4e4dad7c16a5799b8edc684 Mon Sep 17 00:00:00 2001 From: Alice Harris Date: Thu, 6 Nov 2014 12:24:57 +1000 Subject: [PATCH 11/84] add warning text for Beeminder users about deleting To-Dos --- views/shared/tasks/lists.jade | 1 + 1 file changed, 1 insertion(+) diff --git a/views/shared/tasks/lists.jade b/views/shared/tasks/lists.jade index fa8adb2e1d..ebac10c83c 100644 --- a/views/shared/tasks/lists.jade +++ b/views/shared/tasks/lists.jade @@ -58,6 +58,7 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template") .alert =env.t('lotOfToDos') button.task-action-btn.tile.spacious.bright(ng-click='user.ops.clearCompleted({})',popover=env.t('deleteToDosExplanation'),popover-trigger='mouseenter')=env.t('clearCompleted') + p!=env.t('beeminderDeleteWarning') // remaining/completed tabs ul.nav.nav-tabs li(ng-class='{active: !list.showCompleted}') From 6304dd2e15e28bf80206dd30b777c68eb8d53313 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 6 Nov 2014 13:08:41 -0700 Subject: [PATCH 12/84] feat(auth): move to hello.js for social auth, paves the way for multiple connectors. Prettier buttons via zocial --- bower.json | 6 ++++-- public/js/controllers/authCtrl.js | 28 +++++++++++++--------------- public/js/static.js | 6 +----- public/manifest.json | 5 ++++- src/controllers/auth.js | 31 +++++++++++++++++++++---------- src/routes/auth.js | 4 ++-- views/static/login-modal.jade | 14 +++++++++----- 7 files changed, 54 insertions(+), 40 deletions(-) diff --git a/bower.json b/bower.json index 9c346a0e90..30a785c817 100644 --- a/bower.json +++ b/bower.json @@ -41,12 +41,14 @@ "jquery-colorbox": "~1.4.36", "pnotify": "~1.3.1", "jquery-ui": "~1.10.3", - "angular-facebook": "~0.2.3" + "hello": "~1.3.1", + "css-social-buttons": "https://github.com/samcollins/css-social-buttons.git" }, "devDependencies": { "angular-mocks": "1.3.0-beta.11" }, "resolutions": { - "angular": "1.3.0-beta.11" + "angular": "1.3.0-beta.11", + "jquery": ">=1.9.0" } } diff --git a/public/js/controllers/authCtrl.js b/public/js/controllers/authCtrl.js index 8293f48c69..bacc41c28f 100644 --- a/public/js/controllers/authCtrl.js +++ b/public/js/controllers/authCtrl.js @@ -5,8 +5,8 @@ */ angular.module('authCtrl', []) - .controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$location', '$window','ApiUrlService', '$modal', 'Facebook', - function($scope, $rootScope, User, $http, $location, $window, ApiUrlService, $modal, Facebook) { + .controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$location', '$window','ApiUrlService', '$modal', + function($scope, $rootScope, User, $http, $location, $window, ApiUrlService, $modal) { $scope.logout = function() { localStorage.clear(); @@ -112,22 +112,20 @@ angular.module('authCtrl', []) return selectNotificationValue(false, false, false, false, true); } - // ------ Facebook ---------- - // See https://developers.facebook.com/docs/facebook-login/login-flow-for-web/v2.2 for boilerplate - $scope.fbLogin = function(){ - var thenLogin = function(response){ - $http.post(ApiUrlService.get() + "/api/v2/user/auth/facebook", response.authResponse) + // ------ Social ---------- + + hello.init({ + facebook : window.env.FACEBOOK_KEY, + }); + + $scope.socialLogin = function(network){ + hello(network).login({scope:'email'}).then(function(auth){ + $http.post(ApiUrlService.get() + "/api/v2/user/auth/social", auth) .success(function(data, status, headers, config) { runAuth(data.id, data.token); }).error(errorAlert); - } - Facebook.getLoginStatus(function(response) { - if (response.status === 'connected') { - thenLogin(response); - } else { - Facebook.login(thenLogin) - } - + }, function( e ){ + alert("Signin error: " + e.error.message ); }); } } diff --git a/public/js/static.js b/public/js/static.js index 606c7d18d7..66e2b8ee72 100644 --- a/public/js/static.js +++ b/public/js/static.js @@ -1,15 +1,11 @@ "use strict"; -window.habitrpgStatic = angular.module('habitrpgStatic', ['notificationServices', 'userServices', 'chieffancypants.loadingBar', 'authCtrl', 'ui.bootstrap', 'facebook']) +window.habitrpgStatic = angular.module('habitrpgStatic', ['notificationServices', 'userServices', 'chieffancypants.loadingBar', 'authCtrl', 'ui.bootstrap']) .constant("API_URL", "") .constant("STORAGE_USER_ID", 'habitrpg-user') .constant("STORAGE_SETTINGS_ID", 'habit-mobile-settings') .constant("MOBILE_APP", false) -.config(['FacebookProvider', function(FacebookProvider){ - FacebookProvider.init(window.env.FACEBOOK_KEY); -}]) - .controller("PlansCtrl", ['$rootScope', function($rootScope) { $rootScope.clickContact = function(){ diff --git a/public/manifest.json b/public/manifest.json index 59628b67aa..aaa890463c 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -21,6 +21,7 @@ "bower_components/ngInfiniteScroll/build/ng-infinite-scroll.min.js", "bower_components/select2/select2.js", "bower_components/angular-ui-select2/src/select2.js", + "bower_components/hello/dist/hello.all.min.js", "bower_components/angular-bootstrap/ui-bootstrap.js", "bower_components/angular-bootstrap/ui-bootstrap-tpls.js", @@ -66,6 +67,7 @@ ], "css": [ "bower_components/bootstrap/dist/css/bootstrap.css", + "bower_components/css-social-buttons/css/zocial.css", "app.css", "bower_components/pnotify/jquery.pnotify.default.css", "bower_components/pnotify/jquery.pnotify.default.icons.css", @@ -83,7 +85,7 @@ "bower_components/angular-bootstrap/ui-bootstrap-tpls.js", "bower_components/bootstrap/dist/js/bootstrap.js", "bower_components/jquery-colorbox/jquery.colorbox-min.js", - "bower_components/angular-facebook/lib/angular-facebook.js", + "bower_components/hello/dist/hello.all.min.js", "bower_components/angular-loading-bar/build/loading-bar.js", "js/env.js", @@ -95,6 +97,7 @@ ], "css": [ "bower_components/bootstrap/dist/css/bootstrap.css", + "bower_components/css-social-buttons/css/zocial.css", "bower_components/jquery-colorbox/example1/colorbox.css", "static.css" ] diff --git a/src/controllers/auth.js b/src/controllers/auth.js index 2a3c2fd83f..2b8a2c9cf6 100644 --- a/src/controllers/auth.js +++ b/src/controllers/auth.js @@ -166,17 +166,20 @@ api.loginLocal = function(req, res, next) { }; /* - POST /user/auth/facebook + POST /user/auth/social */ -api.loginFacebook = function(req, res, next) { - var accessToken = req.body.accessToken; +api.loginSocial = function(req, res, next) { + var access_token = req.body.authResponse.access_token, + network = req.body.network; + if (network!=='facebook') + return res.json(401, {err:"Only Facebook supported currently."}); async.waterfall([ function(cb){ - // TODO is this private function here safe to use? - passport._strategies.facebook.userProfile(accessToken, cb); + passport._strategies[network].userProfile(access_token, cb); }, function(profile, cb) { - User.findOne({'auth.facebook.id': profile.id}, {_id:1, apiToken:1, auth:1}, function(err, user){ + var q = {};q['auth.'+network+'.id'] = profile.id; + User.findOne(q, {_id:1, apiToken:1, auth:1}, function(err, user){ if (err) return cb(err); cb(null, {user:user, profile:profile}); }); @@ -185,20 +188,22 @@ api.loginFacebook = function(req, res, next) { if (data.user) return cb(null, data.user); // Create new user var prof = data.profile; - var user = new User({ + var user = { preferences: { language: req.language // User language detected from browser, not saved }, auth: { - facebook: prof, timestamps: {created: +new Date(), loggedIn: +new Date()} } - }); + }; + user.auth[network] = prof; + user = new User(user); user.save(cb); + if(isProd && prof.emails && prof.emails[0] && prof.emails[0].value){ emailUser((prof.displayName || prof.username), prof.emails[0].value, 'welcome'); } - ga.event('register', 'Facebook').send(); + ga.event('register', network).send(); } ], function(err, user){ if (err) return res.json(401, {err: err.toString ? err.toString() : err}); @@ -207,6 +212,12 @@ api.loginFacebook = function(req, res, next) { }) }; +/** + * DELETE /user/auth/social + * TODO implement + */ +api.deleteSocial = function(req,res,next){next()} + api.resetPassword = function(req, res, next){ var email = req.body.email, salt = utils.makeSalt(), diff --git a/src/routes/auth.js b/src/routes/auth.js index 616f9cf7d6..0405154db1 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -7,13 +7,13 @@ var router = new express.Router(); auth.setupPassport(router); //FIXME make this consistent with the others router.post('/api/v2/register', i18n.getUserLanguage, auth.registerUser); router.post('/api/v2/user/auth/local', i18n.getUserLanguage, auth.loginLocal); -router.post('/api/v2/user/auth/facebook', i18n.getUserLanguage, auth.loginFacebook); +router.post('/api/v2/user/auth/social', i18n.getUserLanguage, auth.loginSocial); router.post('/api/v2/user/reset-password', i18n.getUserLanguage, auth.resetPassword); router.post('/api/v2/user/change-password', i18n.getUserLanguage, auth.auth, auth.changePassword); router.post('/api/v2/user/change-username', i18n.getUserLanguage, auth.auth, auth.changeUsername); router.post('/api/v1/register', i18n.getUserLanguage, auth.registerUser); router.post('/api/v1/user/auth/local', i18n.getUserLanguage, auth.loginLocal); -router.post('/api/v1/user/auth/facebook', i18n.getUserLanguage, auth.loginFacebook); +router.post('/api/v1/user/auth/social', i18n.getUserLanguage, auth.loginSocial); module.exports = router; \ No newline at end of file diff --git a/views/static/login-modal.jade b/views/static/login-modal.jade index 1382914049..27e82df49e 100644 --- a/views/static/login-modal.jade +++ b/views/static/login-modal.jade @@ -3,11 +3,15 @@ script(id='modals/login.html', type='text/ng-template') button.close(type='button', ng-click='$close()') × h4.modal-title=env.t('loginAndReg') .modal-body(ng-controller='AuthCtrl') - a(href='#', ng-click='fbLogin()') - img(src='/bower_components/habitrpg-shared/img/facebook-login-register.jpeg', alt=env.t('loginFacebookAlt')) - - //can we add in google auth? I like google auth - h3=env.t('or') + a.zocial.facebook(alt=env.t('loginFacebookAlt'), ng-click='socialLogin("facebook")')=env.t('loginFacebookAlt') + //-ul.list-inline + li + a.zocial.icon.facebook(alt=env.t('loginFacebookAlt'), ng-click='socialLogin("facebook")') + li + a.zocial.icon.googleplus(alt="Google", ng-click='socialLogin("google")') Google+ + li + a.zocial.icon.twitter(alt="Twitter", ng-click='socialLogin("twitter")') Twitter + hr ul.nav.nav-tabs li.active a(data-toggle='tab',data-target='#login-tab')=env.t('login') From a8d4a7545c4a13ebb018180644357ea646eb388b Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 6 Nov 2014 16:24:48 -0700 Subject: [PATCH 13/84] bailey(costumes) --- views/shared/new-stuff.jade | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/views/shared/new-stuff.jade b/views/shared/new-stuff.jade index a661981979..d1af4cca0b 100644 --- a/views/shared/new-stuff.jade +++ b/views/shared/new-stuff.jade @@ -8,20 +8,28 @@ table h3.popover-title a(target='_blank', href='https://twitter.com/Mihakuu') Bailey .popover-content - h5 November Backgrounds And Beeminder Integration table.table.table-striped tr td - h5 November Backgrounds - p There are three new avatar backgrounds in the Background Shop! Now your avatar can enjoy a Harvest Feast, admire a Sunset Meadow, or gaze at the Starry Skies! - p.small.muted by Kiwibot, Holsety1, and Draayder - tr - td - h5 Beeminder Integration - p We've integrated with Beeminder! Now you can beemind your To-Dos automatically :) Check it out! - p If you've never heard of Beeminder or want to learn more about what we've integrated so far, check out our blog post about it. Enjoy! - p.small.muted by Alys and Alice Monday - p.small.muted 11/05/2014 + h5 Bailey: Costume Challenge Badges Awarded! + p The HabitRPG Costume Challenge Badges have been awarded! Thanks for your patience while we went through all the entries individually. You can see some of the entries on the HabitRPG blog already, and more will be added every week. + p IMPORTANT: some of the links that people provided did not work. If you entered the Challenge but even after refreshing the page you still don't have your badge, email leslie@habitrpg with the link to your costume and your avatar. (The costume and avatar must have been posted prior to November 1st to count.) + p Thanks to all our amazing participants! + p.small.muted 11/06/2014 + +h5 11/05/2014- November Backgrounds And Beeminder Integration +table.table.table-striped + tr + td + h5 November Backgrounds + p There are three new avatar backgrounds in the Background Shop! Now your avatar can enjoy a Harvest Feast, admire a Sunset Meadow, or gaze at the Starry Skies! + p.small.muted by Kiwibot, Holsety1, and Draayder + tr + td + h5 Beeminder Integration + p We've integrated with Beeminder! Now you can beemind your To-Dos automatically :) Check it out! + p If you've never heard of Beeminder or want to learn more about what we've integrated so far, check out our blog post about it. Enjoy! + p.small.muted by Alys and Alice Monday h5 11/01/2014 table.table.table-striped From 4eae55f3bab69ad9381c7158128506a204081230 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 6 Nov 2014 18:25:44 -0700 Subject: [PATCH 14/84] refactor(payments): move transactional emails to utils.js for DRY (cc @paglias) --- src/controllers/auth.js | 29 ++----------------- src/controllers/payments.js | 58 ++++++------------------------------- src/utils.js | 40 +++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 76 deletions(-) diff --git a/src/controllers/auth.js b/src/controllers/auth.js index 2b8a2c9cf6..8cf4b9a840 100644 --- a/src/controllers/auth.js +++ b/src/controllers/auth.js @@ -24,31 +24,6 @@ var accountSuspended = function(uuid){ }; } -var emailUser = function(name, email, emailType){ - request({ - url: nconf.get('EMAIL_SERVER_URL') + '/job', - method: 'POST', - auth: { - user: nconf.get('EMAIL_SERVER_AUTH_USER'), - pass: nconf.get('EMAIL_SERVER_AUTH_PASSWORD') - }, - json: { - type: 'email', - data: { - emailType: emailType, - to: { - name: name, - email: email - } - }, - options: { - attemps: 5, - backoff: {delay: 10*60*1000, type: 'fixed'} - } - } - }); -} - api.auth = function(req, res, next) { var uid = req.headers['x-api-user']; var token = req.headers['x-api-key']; @@ -128,7 +103,7 @@ api.registerUser = function(req, res, next) { } user.save(cb); - if(isProd) emailUser(username, email, 'welcome'); + if(isProd) utils.txnEmail({name:username, email:email}, 'welcome'); ga.event('register', 'Local').send() } ], function(err, saved) { @@ -201,7 +176,7 @@ api.loginSocial = function(req, res, next) { user.save(cb); if(isProd && prof.emails && prof.emails[0] && prof.emails[0].value){ - emailUser((prof.displayName || prof.username), prof.emails[0].value, 'welcome'); + utils.txnEmail({name:prof.displayName || prof.username, email:prof.emails[0].value}, 'welcome'); } ga.event('register', network).send(); } diff --git a/src/controllers/payments.js b/src/controllers/payments.js index 47aad0e85d..fd2d3ff27a 100644 --- a/src/controllers/payments.js +++ b/src/controllers/payments.js @@ -7,7 +7,7 @@ var shared = require('habitrpg-shared'); var nconf = require('nconf'); var async = require('async'); var User = require('./../models/user').model; -var ga = require('./../utils').ga; +var utils = require('./../utils'); var logging = require('./../logging'); var userAPI = require('./user'); var request = require('request'); @@ -39,46 +39,6 @@ function revealMysteryItems(user) { }); } -function getMailingInfo(user) { - var email, name; - if(user.auth.local){ - email = user.auth.local.email; - name = user.profile.name || user.auth.local.username; - }else if(user.auth.facebook && user.auth.facebook.emails && user.auth.facebook.emails[0] && user.auth.facebook.emails[0].value){ - email = user.auth.facebook.emails[0].value; - name = user.auth.facebook.displayName || user.auth.facebook.username; - } - return {'email': email, 'name': name}; -} - -function emailUser(user, emailType) { - var mailingInfo = getMailingInfo(user); - if(mailingInfo.email){ - request({ - url: nconf.get('EMAIL_SERVER_URL') + '/job', - method: 'POST', - auth: { - user: nconf.get('EMAIL_SERVER_AUTH_USER'), - pass: nconf.get('EMAIL_SERVER_AUTH_PASSWORD') - }, - json: { - type: 'email', - data: { - emailType: emailType, - to: { - name: mailingInfo.name, - email: mailingInfo.email - } - }, - options: { - attemps: 5, - backoff: {delay: (10*60*1000), type: 'fixed'} - } - } - }); - } -} - function createSubscription(user, data) { if (!user.purchased.plan) user.purchased.plan = {}; _(user.purchased.plan) @@ -95,10 +55,10 @@ function createSubscription(user, data) { mysteryItems: [] }); revealMysteryItems(user); - if(isProduction) emailUser(user, 'subscription-begins'); + if(isProduction) utils.txnEmail(user, 'subscription-begins'); user.purchased.txnCount++; - ga.event('subscribe', data.paymentMethod).send(); - ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + '-subscription', data.paymentMethod + " > Stripe").send(); + utils.ga.event('subscribe', data.paymentMethod).send(); + utils.ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + '-subscription', data.paymentMethod + " > Stripe").send(); } /** @@ -106,21 +66,21 @@ function createSubscription(user, data) { */ function cancelSubscription(user, data){ var du = user.purchased.plan.dateUpdated, now = moment(); - if(isProduction) emailUser(user, 'cancel-subscription'); + if(isProduction) utils.txnEmail(user, 'cancel-subscription'); user.purchased.plan.dateTerminated = moment( now.format('MM') + '/' + moment(du).format('DD') + '/' + now.format('YYYY') ) .add({months:1}) .toDate(); - ga.event('unsubscribe', 'Stripe').send(); + utils.ga.event('unsubscribe', 'Stripe').send(); } function buyGems(user, data) { user.balance += 5; user.purchased.txnCount++; - if(isProduction) emailUser(user, 'donation'); - ga.event('checkout', data.paymentMethod).send(); - ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + "-checkout", "Gems > " + data.paymentMethod).send(); + if(isProduction) utils.txnEmail(user, 'donation'); + utils.ga.event('checkout', data.paymentMethod).send(); + utils.ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + "-checkout", "Gems > " + data.paymentMethod).send(); } // Expose some functions for tests diff --git a/src/utils.js b/src/utils.js index b5f51ccb91..64f16a56dd 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,6 +2,7 @@ var nodemailer = require('nodemailer'); var nconf = require('nconf'); var crypto = require('crypto'); var path = require("path"); +var request = require('request'); module.exports.ga = undefined; // set Google Analytics on nconf init @@ -21,6 +22,45 @@ module.exports.sendEmail = function(mailData) { }); } +function getMailingInfo(user) { + var email, name; + if(user.auth.local && user.auth.local.email){ + email = user.auth.local.email; + name = user.profile.name || user.auth.local.username; + }else if(user.auth.facebook && user.auth.facebook.emails && user.auth.facebook.emails[0] && user.auth.facebook.emails[0].value){ + email = user.auth.facebook.emails[0].value; + name = user.auth.facebook.displayName || user.auth.facebook.username; + } + return {email: email, name: name}; +} + +module.exports.txnEmail= function(mailingInfo, emailType){ + if (mailingInfo._id) mailingInfo = getMailingInfo(mailingInfo); + if (!mailingInfo.email) return; + request({ + url: nconf.get('EMAIL_SERVER_URL') + '/job', + method: 'POST', + auth: { + user: nconf.get('EMAIL_SERVER_AUTH_USER'), + pass: nconf.get('EMAIL_SERVER_AUTH_PASSWORD') + }, + json: { + type: 'email', + data: { + emailType: emailType, + to: { + name: mailingInfo.name, + email: mailingInfo.email + } + }, + options: { + attemps: 5, + backoff: {delay: 10*60*1000, type: 'fixed'} + } + } + }); +} + // Encryption using http://dailyjs.com/2010/12/06/node-tutorial-5/ // Note: would use [password-hash](https://github.com/davidwood/node-password-hash), but we need to run // model.query().equals(), so it's a PITA to work in their verify() function From f80da293cecda060e5e6dfeea19dac261b7bf5c3 Mon Sep 17 00:00:00 2001 From: Alice Harris Date: Mon, 10 Nov 2014 06:00:01 +1000 Subject: [PATCH 15/84] fix bug that was causing empty pet paws to appear for rare mounts --- views/options/inventory/stable.jade | 2 -- 1 file changed, 2 deletions(-) diff --git a/views/options/inventory/stable.jade b/views/options/inventory/stable.jade index 0bfeaa09fe..c8c7c6fd08 100644 --- a/views/options/inventory/stable.jade +++ b/views/options/inventory/stable.jade @@ -54,8 +54,6 @@ script(type='text/ng-template', id='partials/options.inventory.mounts.html') each t,k in env.Content.specialMounts - var animal = k.split('-')[0], color = k.split('-')[1] button(ng-if='user.items.mounts["#{animal}-#{color}"]', class="pet-button Mount_Head_#{animal}-#{color}", ng-class='{active: user.items.currentMount == "#{animal}-#{color}"}', ng-click='chooseMount("#{animal}", "#{color}")', popover=env.t(t), popover-trigger='mouseenter', popover-placement='bottom') - button(class="pet-button pet-not-owned", ng-hide='user.items.mounts["#{mount}"]') - .PixelPaw script(type='text/ng-template', id='partials/options.inventory.pets.html') .container-fluid From c08ad6020003f0d780125bd4e8af8be1a35b74a7 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 7 Nov 2014 19:19:26 -0700 Subject: [PATCH 16/84] bailey: community guidelines, facebook login --- views/shared/new-stuff.jade | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/views/shared/new-stuff.jade b/views/shared/new-stuff.jade index d1af4cca0b..93ccbd591d 100644 --- a/views/shared/new-stuff.jade +++ b/views/shared/new-stuff.jade @@ -8,14 +8,27 @@ table h3.popover-title a(target='_blank', href='https://twitter.com/Mihakuu') Bailey .popover-content + h5 Facebook Login Fixed For Mobile And Community Guidelines To Chat table.table.table-striped tr td - h5 Bailey: Costume Challenge Badges Awarded! - p The HabitRPG Costume Challenge Badges have been awarded! Thanks for your patience while we went through all the entries individually. You can see some of the entries on the HabitRPG blog already, and more will be added every week. - p IMPORTANT: some of the links that people provided did not work. If you entered the Challenge but even after refreshing the page you still don't have your badge, email leslie@habitrpg with the link to your costume and your avatar. (The costume and avatar must have been posted prior to November 1st to count.) - p Thanks to all our amazing participants! - p.small.muted 11/06/2014 + h5 Facebook Login Fixed For Mobile! + p Great news! If you use Facebook to log in to the mobile app, we've released an update so you no longer have to type in your UUID/API manually, misspelling things on your tiny keyboard and bemoaning your fate. Thank goodness! The Android update is out now, and the iOS update has been submitted and should be out soon. + tr + td + h5 Community Guidelines To Chat + p Before you can use any of the public chat features, you now have to agree to our Community Guidelines. We know they're long, but they're important, so please do read them if you haven't already. Plus, we worked hard to make them entertaining, and they were illustrated by many of our excellent artisans! + + p.small.muted 11/09/2014 + +h5 11/06/2014 +table.table.table-striped + tr + td + h5 Bailey: Costume Challenge Badges Awarded! + p The HabitRPG Costume Challenge Badges have been awarded! Thanks for your patience while we went through all the entries individually. You can see some of the entries on the HabitRPG blog already, and more will be added every week. + p IMPORTANT: some of the links that people provided did not work. If you entered the Challenge but even after refreshing the page you still don't have your badge, email leslie@habitrpg.com with the link to your costume and your avatar. (The costume and avatar must have been posted prior to November 1st to count.) + p Thanks to all our amazing participants! h5 11/05/2014- November Backgrounds And Beeminder Integration table.table.table-striped From 54c913a5a80259fef838d0e3a02f1e270c27e531 Mon Sep 17 00:00:00 2001 From: ShilohT Date: Mon, 10 Nov 2014 02:21:49 -0500 Subject: [PATCH 17/84] Update menu.jade Added 'Data' menu item --- views/shared/header/menu.jade | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/views/shared/header/menu.jade b/views/shared/header/menu.jade index b768c3935e..59665eb603 100644 --- a/views/shared/header/menu.jade +++ b/views/shared/header/menu.jade @@ -43,6 +43,11 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}') a(ui-sref='options.inventory.mounts')=env.t('mounts') li a(ui-sref='options.inventory.equipment')=env.t('equipment') + ul.toolbar-submenu + li + a(target="_blank" href='http://data.habitrpg.com')=env.t('dataTool') + li + a(ui-sref='options.settings.export')=env.t('exportData') ul.toolbar-submenu li a(target="_blank" href='http://habitrpg.wikia.com/wiki/FAQ')=env.t('FAQ') @@ -112,6 +117,17 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}') a(ui-sref='options.inventory.mounts')=env.t('mounts') li a(ui-sref='options.inventory.equipment')=env.t('equipment') + li.toolbar-button-dropdown + a(target="_blank" href='http://data.habitrpg.com') + span=env.t('data') + a(ng-click='expandMenu("data")', ng-class='{active: _expandedMenu == "data"}') + span ☰ + div(ng-if='_expandedMenu == "data"') + ul.toolbar-submenu(ng-click='expandMenu(null)') + li + a(target="_blank" href='http://data.habitrpg.com')=env.t('dataTool') + li + a(ui-sref='options.settings.export')=env.t('exportData') li.toolbar-button-dropdown a(target="_blank" href='http://habitrpg.wikia.com/wiki/') span=env.t('help') From cd2d861860b605c654018cb45220f44e883ed5d0 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 11 Nov 2014 13:00:41 -0700 Subject: [PATCH 18/84] fet(analytics): track individually for cohort --- public/js/controllers/footerCtrl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/js/controllers/footerCtrl.js b/public/js/controllers/footerCtrl.js index 613ea531b0..f1e4f7f0b5 100644 --- a/public/js/controllers/footerCtrl.js +++ b/public/js/controllers/footerCtrl.js @@ -31,7 +31,7 @@ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); - ga('create', window.env.GA_ID, 'habitrpg.com'); + ga('create', window.env.GA_ID, {userId:User.user._id}); ga('require', 'displayfeatures'); ga('send', 'pageview'); } From e6a99f33a9fe423689e36280cae3a0b18a696623 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 7 Nov 2014 17:04:41 -0700 Subject: [PATCH 19/84] feat(sharing): add ability to share avatar. adds static page with avatar screen, and generates image from it --- config.json.example | 7 +++++- package.json | 6 +++-- src/routes/pages.js | 44 +++++++++++++++++++++++++++++++++++ views/avatar-static.jade | 50 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 views/avatar-static.jade diff --git a/config.json.example b/config.json.example index 821bb91fc3..dcfb3fb8d3 100644 --- a/config.json.example +++ b/config.json.example @@ -25,5 +25,10 @@ "PAYPAL_SIGNATURE": "PAYPAL_SIGNATURE", "EMAIL_SERVER_URL": "http://example.com", "EMAIL_SERVER_AUTH_USER": "user", - "EMAIL_SERVER_AUTH_PASSWORD": "password" + "EMAIL_SERVER_AUTH_PASSWORD": "password", + "S3":{ + "accessKeyId":"accessKeyId", + "secretAccessKey":"secretAccessKey" + } + } diff --git a/package.json b/package.json index 002cd497b3..a3e0fde436 100644 --- a/package.json +++ b/package.json @@ -25,17 +25,18 @@ "grunt-nodemon": "~0.3.0", "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#develop", "icalendar": "git://github.com/lefnire/node-icalendar#master", + "jade": "~1.7.0", "js2xmlparser": "~0.1.2", "lodash": "~2.4.1", - "jade": "~1.7.0", "method-override": "~2.2.0", - "mongoose": "~3.8.17", "moment": "~2.8.3", + "mongoose": "~3.8.17", "mongoose-id-autoinc": "~2013.7.14-4", "nconf": "~0.6.9", "newrelic": "~1.11.2", "nib": "~1.0.1", "nodemailer": "~0.5.2", + "pageres": "^1.0.1", "passport": "~0.2.1", "passport-facebook": "Fonger/passport-facebook#a8f98adcddad99caa9a918bc7b76462c92c5c9fd", "paypal-express-checkout": "git://github.com/HabitRPG/node-paypal-express-checkout#habitrpg", @@ -43,6 +44,7 @@ "paypal-recurring": "git://github.com/jaybryant/paypal-recurring#656b496f43440893c984700191666a5c5c535dca", "pretty-data": "git://github.com/vkiryukhin/pretty-data#master", "request": "~2.44.0", + "s3": "^4.3.1", "stripe": "*", "swagger-node-express": "git://github.com/lefnire/swagger-node-express#habitrpg", "universal-analytics": "~0.3.2", diff --git a/src/routes/pages.js b/src/routes/pages.js index 113375c16e..23fb894c18 100644 --- a/src/routes/pages.js +++ b/src/routes/pages.js @@ -28,6 +28,50 @@ _.each(pages, function(name){ }); }) +var mongoose = require('mongoose'); +router.get('/static/avatar-:uuid.html', i18n.getUserLanguage, middleware.locals, function(req, res) { + mongoose.model('User').findById(req.params.uuid).select('stats profile items achievements preferences backer contributor').exec(function(err, user){ + res.render('avatar-static', { + title: user.profile.name, + env: _.defaults({user:user},res.locals.habitrpg) + }); + }) +}); +var Pageres = require('pageres'); //https://github.com/sindresorhus/pageres +var s3 = require('s3'); //https://github.com/andrewrk/node-s3-client +var bucket = 'habitrpg-dev'; +var client = s3.createClient({ + s3Options: { + accessKeyId: nconf.get("S3").accessKeyId, + secretAccessKey: nconf.get("S3").secretAccessKey + } +}); +router.get('/static/avatar-:uuid.png', i18n.getUserLanguage, middleware.locals, function(req, res, next) { + var filename = 'avatar-'+req.params.uuid+'.png'; + new Pageres({delay: 1}) + .src(nconf.get('BASE_URL')+'/static/avatar-'+req.params.uuid+'.html', ['140x147'], {crop: true, filename: filename.replace('.png','')}) + .dest(__dirname)//TODO Delete this aftewards, or stream directly to s3 instead + .run(function (err, file) { + if (err) return next(err); + var params = { + localFile: __dirname + "/" + filename, + s3Params: { Bucket: bucket, Key: filename } + }; + var uploader = client.uploadFile(params); + uploader.on('error', function(err) { + console.error("unable to upload:", err.stack); + }); + //uploader.on('progress', function() { + // console.log("progress", uploader.progressMd5Amount, + // uploader.progressAmount, uploader.progressTotal); + //}); + uploader.on('end', function() { + res.json(200,{file: s3.getPublicUrlHttp(bucket,filename)}); + console.log("done uploading"); + }); + }); +}); + // --------- Redirects -------- router.get('/static/extensions', function(req, res) { diff --git a/views/avatar-static.jade b/views/avatar-static.jade new file mode 100644 index 0000000000..717af92fd1 --- /dev/null +++ b/views/avatar-static.jade @@ -0,0 +1,50 @@ +doctype html +html(ng-app="habitrpg") + head + title=title + link(rel='shortcut icon', href='#{env.getBuildUrl("favicon.ico")}?v=3') + + meta(charset='utf-8') + meta(name='viewport', content='width=device-width, initial-scale=1.0') + meta(name='apple-mobile-web-app-capable', content='yes') + + script(type='text/javascript'). + window.env = !{JSON.stringify(env)}; + + != env.getManifestFiles("app") + + script(type='text/javascript'). + hello.init({ + facebook : window.env.FACEBOOK_KEY + }); + + window.habitrpg + .controller('StaticAvatarCtrl', ['$scope', '$http', function($scope, $http){ + $scope.profile = window.env.user; + + $scope.postToWall = function(network, customMessage) { + hello(network).login({scope: 'publish'}).then(function (auth) { + $http.get('/static/avatar-'+$scope.profile._id+'.png').success(function(r){ + //TODO experiment with just the page as a link + hello(auth.network).api('/me/feed', 'post', { + picture: r.file, // also see link, picture, icon //https://developers.facebook.com/docs/graph-api/reference/v2.2/status#publishing + message: customMessage || "Check out my HabitRPG progress!" + }).then(function (r) { + debugger; + }); + }) + }); + } + }]) + + + + //webfonts + link(href='//fonts.googleapis.com/css?family=Lato:300,400,700,400italic,700italic', rel='stylesheet', type='text/css') + + body(ng-cloak) + include ./shared/header/avatar + div(ng-controller='StaticAvatarCtrl') + +herobox({main:true}) + button.btn.btn-primary(ng-click='postToWall("facebook", customMessage)') Post to Facebook + textarea(placeholder="Check out my HabitRPG progress!",ng-model="customMessage") From 18b350ebcce3ec7374f44af44ca2bca44e1e85d1 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 11 Nov 2014 16:05:14 -0700 Subject: [PATCH 20/84] feat(sharing): directly stream avatar screenshot to aws --- package.json | 3 ++- src/routes/pages.js | 38 +++++++++++++++----------------------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index a3e0fde436..48503271e0 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "./src/server.js", "dependencies": { "async": "~0.9.0", + "aws-sdk": "^2.0.25", "bower": "~1.3.12", "coffee-script": "1.6.x", "connect-ratelimit": "0.0.7", @@ -44,7 +45,7 @@ "paypal-recurring": "git://github.com/jaybryant/paypal-recurring#656b496f43440893c984700191666a5c5c535dca", "pretty-data": "git://github.com/vkiryukhin/pretty-data#master", "request": "~2.44.0", - "s3": "^4.3.1", + "s3-upload-stream": "^1.0.6", "stripe": "*", "swagger-node-express": "git://github.com/lefnire/swagger-node-express#habitrpg", "universal-analytics": "~0.3.2", diff --git a/src/routes/pages.js b/src/routes/pages.js index 23fb894c18..259b414237 100644 --- a/src/routes/pages.js +++ b/src/routes/pages.js @@ -38,36 +38,28 @@ router.get('/static/avatar-:uuid.html', i18n.getUserLanguage, middleware.locals, }) }); var Pageres = require('pageres'); //https://github.com/sindresorhus/pageres -var s3 = require('s3'); //https://github.com/andrewrk/node-s3-client +var AWS = require('aws-sdk'); +AWS.config.update({accessKeyId: nconf.get("S3").accessKeyId, secretAccessKey: nconf.get("S3").secretAccessKey}); +var s3Stream = require('s3-upload-stream')(new AWS.S3()); //https://github.com/nathanpeck/s3-upload-stream var bucket = 'habitrpg-dev'; -var client = s3.createClient({ - s3Options: { - accessKeyId: nconf.get("S3").accessKeyId, - secretAccessKey: nconf.get("S3").secretAccessKey - } -}); + router.get('/static/avatar-:uuid.png', i18n.getUserLanguage, middleware.locals, function(req, res, next) { var filename = 'avatar-'+req.params.uuid+'.png'; - new Pageres({delay: 1}) + new Pageres()//{delay:1} .src(nconf.get('BASE_URL')+'/static/avatar-'+req.params.uuid+'.html', ['140x147'], {crop: true, filename: filename.replace('.png','')}) - .dest(__dirname)//TODO Delete this aftewards, or stream directly to s3 instead .run(function (err, file) { if (err) return next(err); - var params = { - localFile: __dirname + "/" + filename, - s3Params: { Bucket: bucket, Key: filename } - }; - var uploader = client.uploadFile(params); - uploader.on('error', function(err) { - console.error("unable to upload:", err.stack); + var upload = s3Stream.upload({ + Bucket: bucket, + Key: filename, + ACL: "public-read", + StorageClass: "REDUCED_REDUNDANCY", + ContentType: "binary/octet-stream" }); - //uploader.on('progress', function() { - // console.log("progress", uploader.progressMd5Amount, - // uploader.progressAmount, uploader.progressTotal); - //}); - uploader.on('end', function() { - res.json(200,{file: s3.getPublicUrlHttp(bucket,filename)}); - console.log("done uploading"); + file[0].pipe(upload); + + upload.on('uploaded', function (details) { + res.redirect(details.Location); }); }); }); From 69da58503a24c177b8c0f13360466384e27ba6e2 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 11 Nov 2014 15:03:19 -0700 Subject: [PATCH 21/84] feat(sharing): move avatar-sharing to herobox, use addthis instead of custom solution. refactor from pages/* to export/* --- public/css/avatar.styl | 12 +++++++- public/js/controllers/footerCtrl.js | 5 ++-- src/controllers/dataexport.js | 45 +++++++++++++++++++++++++++++ src/routes/dataexport.js | 5 +++- src/routes/pages.js | 36 ----------------------- views/avatar-static.jade | 24 +-------------- views/index.jade | 3 ++ views/shared/header/avatar.jade | 2 ++ 8 files changed, 69 insertions(+), 63 deletions(-) diff --git a/public/css/avatar.styl b/public/css/avatar.styl index 10432e1c77..20f03c3a0f 100644 --- a/public/css/avatar.styl +++ b/public/css/avatar.styl @@ -73,7 +73,17 @@ future re: pets and whatnot, this is just temporary. &.isUser background: $color-herobox // Set a different background color for the current user &:hover, &:focus - background: lighten($color-herobox, 16.18%) + background: lighten($color-herobox, 16.18%) + + .addthis_pill_style + width: 50px !important + .addthis_native_toolbox + position: absolute + top:2px + right:2px + opacity:0 + &:hover .addthis_native_toolbox + opacity:1 //Need to find a way to indicate who is leader without using background-color as it won't work when a background image is applied //.herobox.isLeader.noBackgroundImage diff --git a/public/js/controllers/footerCtrl.js b/public/js/controllers/footerCtrl.js index f1e4f7f0b5..2a043f52f8 100644 --- a/public/js/controllers/footerCtrl.js +++ b/public/js/controllers/footerCtrl.js @@ -38,8 +38,9 @@ // Scripts only for desktop if (!window.env.IS_MOBILE) { - // Add This - $.getScript("//s7.addthis.com/js/250/addthis_widget.js#pubid=lefnire"); + // Add This - FIXME why isn't this working when here? instead it's now in + //$.getScript("//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-5016f6cc44ad68a4"); + window.addthis_options = 'facebook,twitter,googleplus,tumblr'; // Google Charts $.getScript("//www.google.com/jsapi", function() { diff --git a/src/controllers/dataexport.js b/src/controllers/dataexport.js index 48a08b1d38..0ee176bbe2 100644 --- a/src/controllers/dataexport.js +++ b/src/controllers/dataexport.js @@ -8,6 +8,12 @@ var js2xmlparser = require("js2xmlparser"); var pd = require('pretty-data').pd; var User = require('../models/user').model; +// Avatar screenshot/static-page includes +var Pageres = require('pageres'); //https://github.com/sindresorhus/pageres +var AWS = require('aws-sdk'); +AWS.config.update({accessKeyId: nconf.get("S3").accessKeyId, secretAccessKey: nconf.get("S3").secretAccessKey}); +var s3Stream = require('s3-upload-stream')(new AWS.S3()); //https://github.com/nathanpeck/s3-upload-stream +var bucket = 'habitrpg-dev'; /* ------------------------------------------------------------------------ @@ -83,3 +89,42 @@ expressres.jsonstring = function(obj, headers, status) { body = pd.json(JSON.stringify(obj)); return this.send(body, headers, status); }; + +/* + ------------------------------------------------------------------------ + Static page and image screenshot of avatar + ------------------------------------------------------------------------ + */ + + +dataexport.avatarPage = function(req, res) { + User.findById(req.params.uuid).select('stats profile items achievements preferences backer contributor').exec(function(err, user){ + res.render('avatar-static', { + title: user.profile.name, + env: _.defaults({user:user},res.locals.habitrpg) + }); + }) +}; + +dataexport.avatarImage = function(req, res, next) { + var filename = 'avatar-'+req.params.uuid+'.png'; + new Pageres()//{delay:1} + .src(nconf.get('BASE_URL')+'/export/avatar-'+req.params.uuid+'.html', ['140x147'], {crop: true, filename: filename.replace('.png','')}) + .run(function (err, file) { + if (err) return next(err); + var upload = s3Stream.upload({ + Bucket: bucket, + Key: filename, + ACL: "public-read", + StorageClass: "REDUCED_REDUNDANCY", + ContentType: "binary/octet-stream" + }); + upload.on('error', function (err) { + next(err); + }); + upload.on('uploaded', function (details) { + res.redirect(details.Location); + }); + file[0].pipe(upload); + }); +}; diff --git a/src/routes/dataexport.js b/src/routes/dataexport.js index ad2e2797d0..ba27957124 100644 --- a/src/routes/dataexport.js +++ b/src/routes/dataexport.js @@ -3,11 +3,14 @@ var router = new express.Router(); var dataexport = require('../controllers/dataexport'); var auth = require('../controllers/auth'); var nconf = require('nconf'); -var i18n = require('../i18n') +var i18n = require('../i18n'); +var middleware = require('../middleware.js'); /* Data export */ router.get('/history.csv',auth.authWithSession,i18n.getUserLanguage,dataexport.history); //[todo] encode data output options in the data controller and use these to build routes router.get('/userdata.xml',auth.authWithSession,i18n.getUserLanguage,dataexport.leanuser,dataexport.userdata.xml); router.get('/userdata.json',auth.authWithSession,i18n.getUserLanguage,dataexport.leanuser,dataexport.userdata.json); +router.get('/avatar-:uuid.html', i18n.getUserLanguage, middleware.locals, dataexport.avatarPage); +router.get('/avatar-:uuid.png', i18n.getUserLanguage, middleware.locals, dataexport.avatarImage); module.exports = router; diff --git a/src/routes/pages.js b/src/routes/pages.js index 259b414237..113375c16e 100644 --- a/src/routes/pages.js +++ b/src/routes/pages.js @@ -28,42 +28,6 @@ _.each(pages, function(name){ }); }) -var mongoose = require('mongoose'); -router.get('/static/avatar-:uuid.html', i18n.getUserLanguage, middleware.locals, function(req, res) { - mongoose.model('User').findById(req.params.uuid).select('stats profile items achievements preferences backer contributor').exec(function(err, user){ - res.render('avatar-static', { - title: user.profile.name, - env: _.defaults({user:user},res.locals.habitrpg) - }); - }) -}); -var Pageres = require('pageres'); //https://github.com/sindresorhus/pageres -var AWS = require('aws-sdk'); -AWS.config.update({accessKeyId: nconf.get("S3").accessKeyId, secretAccessKey: nconf.get("S3").secretAccessKey}); -var s3Stream = require('s3-upload-stream')(new AWS.S3()); //https://github.com/nathanpeck/s3-upload-stream -var bucket = 'habitrpg-dev'; - -router.get('/static/avatar-:uuid.png', i18n.getUserLanguage, middleware.locals, function(req, res, next) { - var filename = 'avatar-'+req.params.uuid+'.png'; - new Pageres()//{delay:1} - .src(nconf.get('BASE_URL')+'/static/avatar-'+req.params.uuid+'.html', ['140x147'], {crop: true, filename: filename.replace('.png','')}) - .run(function (err, file) { - if (err) return next(err); - var upload = s3Stream.upload({ - Bucket: bucket, - Key: filename, - ACL: "public-read", - StorageClass: "REDUCED_REDUNDANCY", - ContentType: "binary/octet-stream" - }); - file[0].pipe(upload); - - upload.on('uploaded', function (details) { - res.redirect(details.Location); - }); - }); -}); - // --------- Redirects -------- router.get('/static/extensions', function(req, res) { diff --git a/views/avatar-static.jade b/views/avatar-static.jade index 717af92fd1..9223081dba 100644 --- a/views/avatar-static.jade +++ b/views/avatar-static.jade @@ -14,31 +14,11 @@ html(ng-app="habitrpg") != env.getManifestFiles("app") script(type='text/javascript'). - hello.init({ - facebook : window.env.FACEBOOK_KEY - }); - window.habitrpg - .controller('StaticAvatarCtrl', ['$scope', '$http', function($scope, $http){ + .controller('StaticAvatarCtrl', ['$scope', function($scope){ $scope.profile = window.env.user; - - $scope.postToWall = function(network, customMessage) { - hello(network).login({scope: 'publish'}).then(function (auth) { - $http.get('/static/avatar-'+$scope.profile._id+'.png').success(function(r){ - //TODO experiment with just the page as a link - hello(auth.network).api('/me/feed', 'post', { - picture: r.file, // also see link, picture, icon //https://developers.facebook.com/docs/graph-api/reference/v2.2/status#publishing - message: customMessage || "Check out my HabitRPG progress!" - }).then(function (r) { - debugger; - }); - }) - }); - } }]) - - //webfonts link(href='//fonts.googleapis.com/css?family=Lato:300,400,700,400italic,700italic', rel='stylesheet', type='text/css') @@ -46,5 +26,3 @@ html(ng-app="habitrpg") include ./shared/header/avatar div(ng-controller='StaticAvatarCtrl') +herobox({main:true}) - button.btn.btn-primary(ng-click='postToWall("facebook", customMessage)') Post to Facebook - textarea(placeholder="Check out my HabitRPG progress!",ng-model="customMessage") diff --git a/views/index.jade b/views/index.jade index 802e3873eb..bd9f42f9f5 100644 --- a/views/index.jade +++ b/views/index.jade @@ -10,6 +10,9 @@ html(ng-app="habitrpg", ng-controller="RootCtrl", ng-class='{"applying-action":a meta(name='viewport', content='width=device-width, initial-scale=1.0') meta(name='apple-mobile-web-app-capable', content='yes') + //FIXME for some reason this won't load when in footerCtrl.js#deferredScripts() + script(type="text/javascript", src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-5016f6cc44ad68a4", async="async") + script(type='text/javascript'). window.env = !{JSON.stringify(env)}; diff --git a/views/shared/header/avatar.jade b/views/shared/header/avatar.jade index 415abe6ede..4bec44df16 100644 --- a/views/shared/header/avatar.jade +++ b/views/shared/header/avatar.jade @@ -7,6 +7,8 @@ mixin avatar(opts) .character-sprites + .addthis_native_toolbox(ng-if='profile._id==user._id', data-url="#{env.BASE_URL}/export/avatar-{{profile._id}}.html", data-title="Check out my HabitRPG progress!") + // Mount Body if !opts.minimal span(ng-if='profile.items.currentMount', class='Mount_Body_{{profile.items.currentMount}}') From 3e21e1f4483ecbe516c8ba8db835dae49ad71da9 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 11 Nov 2014 17:04:53 -0700 Subject: [PATCH 22/84] feat(sharing): implement caching of avatar img. img as image/png instead of octet/stream --- src/controllers/dataexport.js | 13 ++++++++++--- views/shared/header/avatar.jade | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/controllers/dataexport.js b/src/controllers/dataexport.js index 0ee176bbe2..2b2a5b8d0f 100644 --- a/src/controllers/dataexport.js +++ b/src/controllers/dataexport.js @@ -14,6 +14,7 @@ var AWS = require('aws-sdk'); AWS.config.update({accessKeyId: nconf.get("S3").accessKeyId, secretAccessKey: nconf.get("S3").secretAccessKey}); var s3Stream = require('s3-upload-stream')(new AWS.S3()); //https://github.com/nathanpeck/s3-upload-stream var bucket = 'habitrpg-dev'; +var request = require('request'); /* ------------------------------------------------------------------------ @@ -108,8 +109,12 @@ dataexport.avatarPage = function(req, res) { dataexport.avatarImage = function(req, res, next) { var filename = 'avatar-'+req.params.uuid+'.png'; - new Pageres()//{delay:1} - .src(nconf.get('BASE_URL')+'/export/avatar-'+req.params.uuid+'.html', ['140x147'], {crop: true, filename: filename.replace('.png','')}) + request.head('https://'+bucket+'.s3.amazonaws.com/'+filename, function(err,response,body) { + // cache images for 10 minutes on aws, else upload a new one + if (response.statusCode==200 && moment().diff(response.headers['last-modified'], 'minutes') < 10) + return res.redirect(301, 'https://' + bucket + '.s3.amazonaws.com/' + filename); + new Pageres()//{delay:1} + .src(nconf.get('BASE_URL') + '/export/avatar-' + req.params.uuid + '.html', ['140x147'], {crop: true, filename: filename.replace('.png', '')}) .run(function (err, file) { if (err) return next(err); var upload = s3Stream.upload({ @@ -117,7 +122,8 @@ dataexport.avatarImage = function(req, res, next) { Key: filename, ACL: "public-read", StorageClass: "REDUCED_REDUNDANCY", - ContentType: "binary/octet-stream" + ContentType: "image/png", + Expires: +moment().add({minutes: 3}) }); upload.on('error', function (err) { next(err); @@ -127,4 +133,5 @@ dataexport.avatarImage = function(req, res, next) { }); file[0].pipe(upload); }); + }) }; diff --git a/views/shared/header/avatar.jade b/views/shared/header/avatar.jade index 4bec44df16..3c03ac1131 100644 --- a/views/shared/header/avatar.jade +++ b/views/shared/header/avatar.jade @@ -7,7 +7,7 @@ mixin avatar(opts) .character-sprites - .addthis_native_toolbox(ng-if='profile._id==user._id', data-url="#{env.BASE_URL}/export/avatar-{{profile._id}}.html", data-title="Check out my HabitRPG progress!") + .addthis_native_toolbox(ng-if='profile._id==user._id', data-url="#{env.BASE_URL}/export/avatar-{{profile._id}}.png", data-title="Check out my HabitRPG progress!") // Mount Body if !opts.minimal From 3a415a45a96b5260ca4bc25b56da117dd0432a98 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 11 Nov 2014 17:52:03 -0700 Subject: [PATCH 23/84] feat(sharing): add download link --- public/js/controllers/footerCtrl.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/public/js/controllers/footerCtrl.js b/public/js/controllers/footerCtrl.js index 2a043f52f8..afd7ca0229 100644 --- a/public/js/controllers/footerCtrl.js +++ b/public/js/controllers/footerCtrl.js @@ -40,7 +40,16 @@ if (!window.env.IS_MOBILE) { // Add This - FIXME why isn't this working when here? instead it's now in //$.getScript("//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-5016f6cc44ad68a4"); - window.addthis_options = 'facebook,twitter,googleplus,tumblr'; + var addthisServices = 'facebook,twitter,googleplus,tumblr,'+window.env.BASE_URL.replace('https://','').replace('http://',''); + window.addthis_config = { + services_custom:{ + name: "Download", + url: window.env.BASE_URL+"/export/avatar-"+User.user._id+".png", + icon: window.env.BASE_URL+"/favicon.ico" + }, + services_expanded:addthisServices, + services_compact:addthisServices + }; // Google Charts $.getScript("//www.google.com/jsapi", function() { From 15f318ccc2b455856273d6ff33a1b0f42adc23ac Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 11 Nov 2014 19:11:04 -0700 Subject: [PATCH 24/84] feat(sharing): screw it, social links + img + text aint working, just redirect to homepage + show member modal --- public/js/static.js | 15 +++++++ public/manifest.json | 2 + views/shared/header/avatar.jade | 2 +- views/static/front.jade | 78 +++++++++++++++++---------------- 4 files changed, 59 insertions(+), 38 deletions(-) diff --git a/public/js/static.js b/public/js/static.js index 66e2b8ee72..860dbf5587 100644 --- a/public/js/static.js +++ b/public/js/static.js @@ -6,6 +6,21 @@ window.habitrpgStatic = angular.module('habitrpgStatic', ['notificationServices' .constant("STORAGE_SETTINGS_ID", 'habit-mobile-settings') .constant("MOBILE_APP", false) +.controller("RootCtrl", ['$scope', '$location', '$modal', '$http', function($scope, $location, $modal, $http){ + // must be #?memberId=xx, see https://github.com/angular/angular.js/issues/7239 + var memberId = $location.search()['memberId']; + if (memberId) { + $http.get('/api/v2/members/'+memberId).success(function(data, status, headers, config){ + $scope.profile = data; + $scope.Content = window.habitrpgShared.content; + $modal.open({ + templateUrl: 'modals/member.html', + scope: $scope + }); + }) + } + }]) + .controller("PlansCtrl", ['$rootScope', function($rootScope) { $rootScope.clickContact = function(){ diff --git a/public/manifest.json b/public/manifest.json index aaa890463c..b2a06332a8 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -99,6 +99,8 @@ "bower_components/bootstrap/dist/css/bootstrap.css", "bower_components/css-social-buttons/css/zocial.css", "bower_components/jquery-colorbox/example1/colorbox.css", + "app.css", + "bower_components/habitrpg-shared/dist/habitrpg-shared.css", "static.css" ] } diff --git a/views/shared/header/avatar.jade b/views/shared/header/avatar.jade index 3c03ac1131..41d70ba1ea 100644 --- a/views/shared/header/avatar.jade +++ b/views/shared/header/avatar.jade @@ -7,7 +7,7 @@ mixin avatar(opts) .character-sprites - .addthis_native_toolbox(ng-if='profile._id==user._id', data-url="#{env.BASE_URL}/export/avatar-{{profile._id}}.png", data-title="Check out my HabitRPG progress!") + .addthis_native_toolbox(ng-if='profile._id==user._id', data-url="#{env.BASE_URL}/static/front/#?memberId={{profile._id}}", data-title="Check out my HabitRPG progress!") // Mount Body if !opts.minimal diff --git a/views/static/front.jade b/views/static/front.jade index 2e03b1fef6..9927ef3551 100644 --- a/views/static/front.jade +++ b/views/static/front.jade @@ -8,41 +8,45 @@ block title title=env.t('titleFront') block content - .marketing - //we need to use something that's not jumbotron for this, but still keep it centered - //could someone write something else to make it pretty? - img(src='/bower_components/habitrpg-shared/img/logo/habitrpg_pixel.png', alt='HabitRPG logo') - //this image needs to be replaced by something more enticing, that shows off the features of hRPG - //while acting similarly to a logo - h1#tagline=env.t('tagline') - p.lead - button.btn.btn-primary.btn-lg#frontpage-play-button(ng-click='playButtonClick()')=env.t('playButton') - hr - img(src='/marketing/devices.png') - //we'd want the tagline centered, for sure, and a bit more pop, but without using jumbotron - //it could also be part of the image, as long as the alt text included it - //in fact, I think I really want it on the image, rather than as text, but language issues - br - p.lead=env.t('landingp1') - h2=env.t('landingp2header') - //images in these parts could be useful, too - //if there's a language workaround, image headers? people like pictures! - p.lead - =env.t('landingp2') - |  - h2=env.t('landingp3header') - //I'm not sold on "Consquences as the title here. Anyone got a better idea? - p.lead - =env.t('landingp3') - |  - h2=env.t('landingp4header') - p.lead=env.t('landingp4') - //- TODO - h2=env.t('landingend') + div(ng-controller='RootCtrl') + include ../shared/header/avatar + include ../shared/modals/members + + .marketing + //we need to use something that's not jumbotron for this, but still keep it centered + //could someone write something else to make it pretty? + img(src='/bower_components/habitrpg-shared/img/logo/habitrpg_pixel.png', alt='HabitRPG logo') + //this image needs to be replaced by something more enticing, that shows off the features of hRPG + //while acting similarly to a logo + h1#tagline=env.t('tagline') p.lead - =env.t('landingend2') - a(href="FEATURESPAGEHERE")=env.t('landingfeatureslink') - =env.t('landingend3') - a(href="ENTERPRISEPAGEHERE")=env.t('landingadminlink') - |  - =env.t('landingend4') + button.btn.btn-primary.btn-lg#frontpage-play-button(ng-click='playButtonClick()')=env.t('playButton') + hr + img(src='/marketing/devices.png') + //we'd want the tagline centered, for sure, and a bit more pop, but without using jumbotron + //it could also be part of the image, as long as the alt text included it + //in fact, I think I really want it on the image, rather than as text, but language issues + br + p.lead=env.t('landingp1') + h2=env.t('landingp2header') + //images in these parts could be useful, too + //if there's a language workaround, image headers? people like pictures! + p.lead + =env.t('landingp2') + |  + h2=env.t('landingp3header') + //I'm not sold on "Consquences as the title here. Anyone got a better idea? + p.lead + =env.t('landingp3') + |  + h2=env.t('landingp4header') + p.lead=env.t('landingp4') + //- TODO + h2=env.t('landingend') + p.lead + =env.t('landingend2') + a(href="FEATURESPAGEHERE")=env.t('landingfeatureslink') + =env.t('landingend3') + a(href="ENTERPRISEPAGEHERE")=env.t('landingadminlink') + |  + =env.t('landingend4') From cf66383a94ed8f8d1a0f6908b9604dcce61e0412 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 11 Nov 2014 21:05:06 -0700 Subject: [PATCH 25/84] bailey(goldenknight) --- views/shared/new-stuff.jade | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/views/shared/new-stuff.jade b/views/shared/new-stuff.jade index 93ccbd591d..cd1d2b1383 100644 --- a/views/shared/new-stuff.jade +++ b/views/shared/new-stuff.jade @@ -8,18 +8,24 @@ table h3.popover-title a(target='_blank', href='https://twitter.com/Mihakuu') Bailey .popover-content - h5 Facebook Login Fixed For Mobile And Community Guidelines To Chat table.table.table-striped tr td - h5 Facebook Login Fixed For Mobile! - p Great news! If you use Facebook to log in to the mobile app, we've released an update so you no longer have to type in your UUID/API manually, misspelling things on your tiny keyboard and bemoaning your fate. Thank goodness! The Android update is out now, and the iOS update has been submitted and should be out soon. - tr - td - h5 Community Guidelines To Chat - p Before you can use any of the public chat features, you now have to agree to our Community Guidelines. We know they're long, but they're important, so please do read them if you haven't already. Plus, we worked hard to make them entertaining, and they were illustrated by many of our excellent artisans! + h5 New Equipment Quest Line: The Golden Knight! + p The Golden Knight believes that she is the perfect Habitican, and that anyone who slips up in their quest for self-improvement is a lazy failure. Can you talk some sense into her - or will it come to blows? If you complete the entire quest line, you'll be rewarded with a legendary weapon... + p The first scroll in this quest line, "A Stern Talking-to," drops automatically at Level 40! If you're already over Level 40, you will automatically be awarded this quest - just check off a task and then check your inventory. + p.small.muted 11/12/2014 - p.small.muted 11/09/2014 +h5 11/09/2014 - Facebook Login Fixed For Mobile And Community Guidelines To Chat +table.table.table-striped + tr + td + h5 Facebook Login Fixed For Mobile! + p Great news! If you use Facebook to log in to the mobile app, we've released an update so you no longer have to type in your UUID/API manually, misspelling things on your tiny keyboard and bemoaning your fate. Thank goodness! The Android update is out now, and the iOS update has been submitted and should be out soon. + tr + td + h5 Community Guidelines To Chat + p Before you can use any of the public chat features, you now have to agree to our Community Guidelines. We know they're long, but they're important, so please do read them if you haven't already. Plus, we worked hard to make them entertaining, and they were illustrated by many of our excellent artisans! h5 11/06/2014 table.table.table-striped From 8f633e81db5aa7acd35f28d3cfb6a2eb798e31b8 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 12 Nov 2014 09:02:12 -0700 Subject: [PATCH 26/84] fix(sharing): add phantomjs buildpack to heroku (for pageres), proper use of nested nconf keys --- .buildpacks | 2 ++ public/js/controllers/footerCtrl.js | 4 ++-- src/controllers/dataexport.js | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 .buildpacks diff --git a/.buildpacks b/.buildpacks new file mode 100644 index 0000000000..b57b4bd3b0 --- /dev/null +++ b/.buildpacks @@ -0,0 +1,2 @@ +https://github.com/heroku/heroku-buildpack-nodejs.git +https://github.com/stomita/heroku-buildpack-phantomjs.git diff --git a/public/js/controllers/footerCtrl.js b/public/js/controllers/footerCtrl.js index afd7ca0229..9017602aec 100644 --- a/public/js/controllers/footerCtrl.js +++ b/public/js/controllers/footerCtrl.js @@ -38,8 +38,8 @@ // Scripts only for desktop if (!window.env.IS_MOBILE) { - // Add This - FIXME why isn't this working when here? instead it's now in - //$.getScript("//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-5016f6cc44ad68a4"); + // Add This + //$.getScript("//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-5016f6cc44ad68a4"); //FIXME why isn't this working when here? instead it's now in var addthisServices = 'facebook,twitter,googleplus,tumblr,'+window.env.BASE_URL.replace('https://','').replace('http://',''); window.addthis_config = { services_custom:{ diff --git a/src/controllers/dataexport.js b/src/controllers/dataexport.js index 2b2a5b8d0f..36f3e87fee 100644 --- a/src/controllers/dataexport.js +++ b/src/controllers/dataexport.js @@ -11,7 +11,7 @@ var User = require('../models/user').model; // Avatar screenshot/static-page includes var Pageres = require('pageres'); //https://github.com/sindresorhus/pageres var AWS = require('aws-sdk'); -AWS.config.update({accessKeyId: nconf.get("S3").accessKeyId, secretAccessKey: nconf.get("S3").secretAccessKey}); +AWS.config.update({accessKeyId: nconf.get("S3:accessKeyId"), secretAccessKey: nconf.get("S3:secretAccessKey")}); var s3Stream = require('s3-upload-stream')(new AWS.S3()); //https://github.com/nathanpeck/s3-upload-stream var bucket = 'habitrpg-dev'; var request = require('request'); From de509c83bec722bc514473fa19b7a68d8140558b Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 12 Nov 2014 14:40:33 -0700 Subject: [PATCH 27/84] fix(sharing): add bucket to nconf --- config.json.example | 1 + src/controllers/dataexport.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/config.json.example b/config.json.example index dcfb3fb8d3..aefb56d1b9 100644 --- a/config.json.example +++ b/config.json.example @@ -27,6 +27,7 @@ "EMAIL_SERVER_AUTH_USER": "user", "EMAIL_SERVER_AUTH_PASSWORD": "password", "S3":{ + "bucket":"bucket", "accessKeyId":"accessKeyId", "secretAccessKey":"secretAccessKey" } diff --git a/src/controllers/dataexport.js b/src/controllers/dataexport.js index 36f3e87fee..8a48ee1638 100644 --- a/src/controllers/dataexport.js +++ b/src/controllers/dataexport.js @@ -13,7 +13,7 @@ var Pageres = require('pageres'); //https://github.com/sindresorhus/pageres var AWS = require('aws-sdk'); AWS.config.update({accessKeyId: nconf.get("S3:accessKeyId"), secretAccessKey: nconf.get("S3:secretAccessKey")}); var s3Stream = require('s3-upload-stream')(new AWS.S3()); //https://github.com/nathanpeck/s3-upload-stream -var bucket = 'habitrpg-dev'; +var bucket = nconf.get("S3:bucket"); var request = require('request'); /* @@ -117,6 +117,7 @@ dataexport.avatarImage = function(req, res, next) { .src(nconf.get('BASE_URL') + '/export/avatar-' + req.params.uuid + '.html', ['140x147'], {crop: true, filename: filename.replace('.png', '')}) .run(function (err, file) { if (err) return next(err); + // see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#createMultipartUpload-property var upload = s3Stream.upload({ Bucket: bucket, Key: filename, From 216632157258363b8c53a2fae471a98ca779d6e2 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 6 Nov 2014 18:40:35 -0700 Subject: [PATCH 28/84] feat(invite-friends): add inviting friends via email. URL parsed, session holds invitation. If user joins party (new user or existing), the inviter is rewarded --- package.json | 1 + public/js/controllers/groupsCtrl.js | 15 ++++++++-- src/controllers/groups.js | 7 +++-- src/controllers/user.js | 31 +++++++++++++++++++- src/middleware.js | 8 ++++- src/routes/apiv2.coffee | 11 ++++++- views/options/settings.jade | 1 + views/options/social/group.jade | 8 +++-- views/shared/modals/index.jade | 1 + views/shared/modals/invite-friends.jade | 39 +++++++++++++++++++++++++ 10 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 views/shared/modals/invite-friends.jade diff --git a/package.json b/package.json index 48503271e0..5fe7519805 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "paypal-ipn": "~1.0.1", "paypal-recurring": "git://github.com/jaybryant/paypal-recurring#656b496f43440893c984700191666a5c5c535dca", "pretty-data": "git://github.com/vkiryukhin/pretty-data#master", + "qs": "^2.3.2", "request": "~2.44.0", "s3-upload-stream": "^1.0.6", "stripe": "*", diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index 335de42aa4..6f9559a9d7 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -1,7 +1,7 @@ "use strict"; -habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '$http', '$q', 'User', 'Members', '$state', - function($scope, $rootScope, Shared, Groups, $http, $q, User, Members, $state) { +habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '$http', '$q', 'User', 'Members', '$state', 'Notification', + function($scope, $rootScope, Shared, Groups, $http, $q, User, Members, $state, Notification) { $scope.isMemberOfPendingQuest = function(userid, group) { if (!group.quest || !group.quest.members) return false; if (group.quest.active) return false; // quest is started, not pending @@ -63,7 +63,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' } } - // ------ Invites ------ + // ------ Invites ------ $scope.invite = function(group){ Groups.Group.invite({gid: group._id, uuid: group.invitee}, undefined, function(){ @@ -72,6 +72,15 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' group.invitee = ''; }); } + + $scope.emails = [{name:"",email:""},{name:"",email:""}]; + $scope.inviteEmails = function(emails){ + $http.post('/api/v2/user/social/invite-friends', emails).success(function(){ + Notification.text("Invitations sent!"); + $scope.emails = [{name:'',email:''},{name:'',email:''}]; + }); + } + } ]) diff --git a/src/controllers/groups.js b/src/controllers/groups.js index cd7c768fad..9f789f20dd 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -300,7 +300,8 @@ api.join = function(req, res, next) { group = res.locals.group; if (group.type == 'party' && group._id == (user.invitations && user.invitations.party && user.invitations.party.id)) { - user.invitations.party = undefined; + User.update({_id:user.invitations.party.inviter}, {$inc:{'items.quests.basilist':1}}).exec(); // Reward inviter + user.invitations.party = undefined; // Clear invite user.save(); // invite new user to pending quest if (group.quest.key && !group.quest.active) { @@ -439,10 +440,10 @@ api.invite = function(req, res, next) { function sendInvite (){ if(group.type === 'guild'){ - invite.invitations.guilds.push({id: group._id, name: group.name}); + invite.invitations.guilds.push({id: group._id, name: group.name, inviter:res.locals.user._id}); }else{ //req.body.type in 'guild', 'party' - invite.invitations.party = {id: group._id, name: group.name} + invite.invitations.party = {id: group._id, name: group.name, inviter:res.locals.user._id}; } group.invites.push(invite._id); diff --git a/src/controllers/user.js b/src/controllers/user.js index e2e7ec6b38..1138791663 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -7,13 +7,15 @@ var nconf = require('nconf'); var async = require('async'); var shared = require('habitrpg-shared'); var User = require('./../models/user').model; -var ga = require('./../utils').ga; +var utils = require('./../utils'); +var ga = utils.ga; var Group = require('./../models/group').model; var Challenge = require('./../models/challenge').model; var moment = require('moment'); var logging = require('./../logging'); var acceptablePUTPaths; var api = module.exports; +var qs = require('qs'); // api.purchase // Shared.ops @@ -393,6 +395,33 @@ api.cast = function(req, res, next) { } } +/** + * POST /user/invite-friends + */ +api.inviteFriends = function(req, res, next) { + Group.findOne({type:'party', members:{'$in': [res.locals.user._id]}}).select('_id name').exec(function(err,party){ + if (err) return next(err); + var link = nconf.get('BASE_URL') + '?' + qs.stringify({partyInvite:{id:party._id, inviter:res.locals.user._id, name:party.name}}); + _.each(req.body, function(invite){ + if (invite.email) { + var variables = {link:link, inviter:res.locals.user.profile.name, invitee:invite.name}; + // TODO implement "users can only be invited once" + utils.txnEmail(invite, 'invite-friend', variables); + } + }); + res.send(200); + }) +} +api.sessionPartyInvite = function(req,res,next){ + if (req.session.partyInvite) { + res.locals.user.invitations.party = req.session.partyInvite; + Group.update({_id:req.session.partyInvite.id},{$addToSet:{invites:res.locals.user._id}}); + delete req.session.partyInvite; + return res.locals.user.save(next); + } + next(); +} + /** * All other user.ops which can easily be mapped to habitrpg-shared/index.coffee, not requiring custom API-wrapping */ diff --git a/src/middleware.js b/src/middleware.js index 4f4bece349..d7a6902ead 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -12,6 +12,7 @@ var shared = require('habitrpg-shared'); var request = require('request'); var os = require('os'); var moment = require('moment'); +var qs = require('qs'); module.exports.apiThrottle = function(app) { if (nconf.get('NODE_ENV') !== 'production') return; @@ -196,7 +197,12 @@ module.exports.locals = function(req, res, next) { tavern: tavern, // for world boss worldDmg: (tavern && tavern.quest && tavern.quest.extra && tavern.quest.extra.worldDmg) || {} - } + }; + + // Put query-string party invitations into session to be handled later + var partyInvite = qs.parse(req.query.partyInvite); + if (partyInvite && partyInvite.id) + req.session.partyInvite = partyInvite; next(); } diff --git a/src/routes/apiv2.coffee b/src/routes/apiv2.coffee index edacbdc243..d2915fe246 100644 --- a/src/routes/apiv2.coffee +++ b/src/routes/apiv2.coffee @@ -320,7 +320,7 @@ module.exports = (swagger, v2) -> parameters:[ body '','The array of batch-operations to perform','object' ] - middleware: [middleware.forceRefresh, auth.auth, i18n.getUserLanguage, cron] + middleware: [middleware.forceRefresh, auth.auth, i18n.getUserLanguage, cron, user.sessionPartyInvite] action: user.batchUpdate # Tags @@ -364,6 +364,15 @@ module.exports = (swagger, v2) -> ] action: user.deleteTag + "/user/social/invite-friends": + spec: + method: 'POST' + description: 'Invite friends via email' + parameters: [ + body 'invites','Array of [{name:"Friend\'s Name", email:"friends@email.com"}] to invite to play in your party','object' + ] + action: user.inviteFriends + # --------------------------------- # Groups # --------------------------------- diff --git a/views/options/settings.jade b/views/options/settings.jade index f8f186c1fd..28e5863e0b 100644 --- a/views/options/settings.jade +++ b/views/options/settings.jade @@ -63,6 +63,7 @@ script(type='text/ng-template', id='partials/options.settings.settings.html') button.btn.btn-default(ng-click='showTour()', popover-placement='right', popover-trigger='mouseenter', popover=env.t('restartTour'))= env.t('showTour') button.btn.btn-default(ng-click='showBailey()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('showBaileyPop'))= env.t('showBailey') button.btn.btn-default(ng-click='openRestoreModal()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('fixValPop'))= env.t('fixVal') + button.btn.btn-default(ng-click="openModal('invite-friends', {controller:'GroupsCtrl'})") Invite Friends button.btn.btn-default(ng-if='user.preferences.disableClasses==true', ng-click='user.ops.changeClass({})', popover-trigger='mouseenter', popover-placement='right', popover=env.t('enableClassPop'))= env.t('enableClass') button.btn.btn-default(ng-if='!user.preferences.disableClasses && user.flags.classSelected', ng-click='showClassesTour()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('classTourPop'))= env.t('showClass') diff --git a/views/options/social/group.jade b/views/options/social/group.jade index df056b816b..00ed6a49f5 100644 --- a/views/options/social/group.jade +++ b/views/options/social/group.jade @@ -48,9 +48,11 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter // ------ Members ------- .panel.panel-default .panel-heading - h3.panel-title=env.t('members') + h3.panel-title + =env.t('members') + button.pull-right.btn.btn-primary(ng-click="openModal('invite-friends', {controller:'GroupsCtrl'})", ng-if='::group.type=="party"') Invite Friends .panel-body.modal-fixed-height - div(ng-if='group.type=="party"') + div(ng-if='::group.type=="party"') =env.t('partyList') br select#partyOrder( @@ -86,7 +88,7 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter a.media-body span(ng-click='clickMember(invite._id, true)') | {{invite.profile.name}} - .panel-footer + .panel-footer(ng-if='::group.type!="party"') form.form-inline(ng-submit='invite(group)') //.alert.alert-danger(ng-show='_groupError') {{_groupError}} .form-group diff --git a/views/shared/modals/index.jade b/views/shared/modals/index.jade index 8d5b74e8ab..f8f5433996 100644 --- a/views/shared/modals/index.jade +++ b/views/shared/modals/index.jade @@ -10,3 +10,4 @@ include ./classes include ./quests include ./rebirth include ./limited +include ./invite-friends diff --git a/views/shared/modals/invite-friends.jade b/views/shared/modals/invite-friends.jade new file mode 100644 index 0000000000..516d425741 --- /dev/null +++ b/views/shared/modals/invite-friends.jade @@ -0,0 +1,39 @@ +script(type='text/ng-template', id='modals/invite-friends.html') + .modal-header + h4 Invite Friends + .modal-body + p.alert.alert-info Invite friends to your party who already play HabitRPG. Have them send you their User ID and enter it here. + + form.form-inline(ng-submit='invite(party)') + //-.alert.alert-danger(ng-show='_groupError') {{_groupError}} + .form-group + input.form-control(type='text', placeholder=env.t('userId'), ng-model='party.invitee') + |  + button.btn.btn-primary(type='submit') Invite Existing User + + hr + + p.alert.alert-info Invite friends to your party who don't yet play HabitRPG. This will send them an invite email, and automatically invite them to your party. + + table.table.table-striped + thead + tr + th Name + th Email + tbody + tr(ng-repeat='email in emails') + td + input.form-control(type='text', ng-model='email.name') + td + input.form-control(type='email', ng-model='email.email') + tr + td(colspan=2) + button.btn.btn-xs.pull-right(ng-click='emails = emails.concat([{name:"",email:""}])') + i.glyphicon.glyphicon-plus + tr + td(colspan=2) + button.btn.btn-primary.pull-right(ng-click='inviteEmails(emails)') Invite New User(s) + + .modal-footer + button.btn.btn-default(ng-click='$close()') Close + From 0da391df59afbda2a8c42376863a3ff522cfe321 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 13 Nov 2014 11:42:56 -0700 Subject: [PATCH 29/84] fix(intive-friends): only invite set invitation if user hasn't already been invited to a party --- src/controllers/user.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index 1138791663..848fcfdf31 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -414,10 +414,13 @@ api.inviteFriends = function(req, res, next) { } api.sessionPartyInvite = function(req,res,next){ if (req.session.partyInvite) { - res.locals.user.invitations.party = req.session.partyInvite; - Group.update({_id:req.session.partyInvite.id},{$addToSet:{invites:res.locals.user._id}}); - delete req.session.partyInvite; - return res.locals.user.save(next); + var inv = res.locals.user.invitations; + if (!(inv.party && inv.party.id)) { + inv.party = req.session.partyInvite; + Group.update({_id:req.session.partyInvite.id},{$addToSet:{invites:res.locals.user._id}}); + delete req.session.partyInvite; + return res.locals.user.save(next); + } } next(); } From beea956d778c571dfc242ad621329be36ec74bc9 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Thu, 13 Nov 2014 19:52:16 +0100 Subject: [PATCH 30/84] feat(emails): allow variables to be passed to emails --- src/utils.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/utils.js b/src/utils.js index 64f16a56dd..06ee14eb9d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -34,7 +34,7 @@ function getMailingInfo(user) { return {email: email, name: name}; } -module.exports.txnEmail= function(mailingInfo, emailType){ +module.exports.txnEmail = function(mailingInfo, emailType, variables){ if (mailingInfo._id) mailingInfo = getMailingInfo(mailingInfo); if (!mailingInfo.email) return; request({ @@ -51,7 +51,8 @@ module.exports.txnEmail= function(mailingInfo, emailType){ to: { name: mailingInfo.name, email: mailingInfo.email - } + }, + variables: variables }, options: { attemps: 5, From 02328fcf587cf5f36023a8598d92cbfba5f42460 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Thu, 13 Nov 2014 20:11:43 +0100 Subject: [PATCH 31/84] fix(invite-friend): change variables --- src/controllers/user.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/controllers/user.js b/src/controllers/user.js index 848fcfdf31..6fd278ef39 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -404,7 +404,11 @@ api.inviteFriends = function(req, res, next) { var link = nconf.get('BASE_URL') + '?' + qs.stringify({partyInvite:{id:party._id, inviter:res.locals.user._id, name:party.name}}); _.each(req.body, function(invite){ if (invite.email) { - var variables = {link:link, inviter:res.locals.user.profile.name, invitee:invite.name}; + var variables = [ + {name: 'LINK', content: link}, + {name: 'INVITER', content: res.locals.user.profile.name}, + {name: 'INVITEE', content: invite.name} + ]; // TODO implement "users can only be invited once" utils.txnEmail(invite, 'invite-friend', variables); } From 524e0514a1560feef5b1e3d33b82df8e20791c6e Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 13 Nov 2014 12:53:07 -0700 Subject: [PATCH 32/84] feat(invite-friends): update wording on invitations. Add commented-out sample code on copy-link (it's not working) --- public/js/controllers/groupsCtrl.js | 3 +++ views/shared/modals/invite-friends.jade | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index 6f9559a9d7..1b3ef7fba2 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -74,6 +74,9 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' } $scope.emails = [{name:"",email:""},{name:"",email:""}]; + //$scope.inviteLink = function(obj){ + // return window.env.BASE_URL + '?partyInvite=' + encodeURIComponent(JSON.stringify(obj)); + //} $scope.inviteEmails = function(emails){ $http.post('/api/v2/user/social/invite-friends', emails).success(function(){ Notification.text("Invitations sent!"); diff --git a/views/shared/modals/invite-friends.jade b/views/shared/modals/invite-friends.jade index 516d425741..87b7e36215 100644 --- a/views/shared/modals/invite-friends.jade +++ b/views/shared/modals/invite-friends.jade @@ -2,7 +2,7 @@ script(type='text/ng-template', id='modals/invite-friends.html') .modal-header h4 Invite Friends .modal-body - p.alert.alert-info Invite friends to your party who already play HabitRPG. Have them send you their User ID and enter it here. + p.alert.alert-info Invite friends by User ID here. form.form-inline(ng-submit='invite(party)') //-.alert.alert-danger(ng-show='_groupError') {{_groupError}} @@ -13,7 +13,7 @@ script(type='text/ng-template', id='modals/invite-friends.html') hr - p.alert.alert-info Invite friends to your party who don't yet play HabitRPG. This will send them an invite email, and automatically invite them to your party. + p.alert.alert-info Invite friends by email. If they join via your email, they'll automatically be invited to your party. table.table.table-striped thead @@ -34,6 +34,11 @@ script(type='text/ng-template', id='modals/invite-friends.html') td(colspan=2) button.btn.btn-primary.pull-right(ng-click='inviteEmails(emails)') Invite New User(s) + //- + hr + p.alert.alert-info Or share this link (copy/paste): + input.form-control(type='text', ng-value='inviteLink({id: party._id, inviter: user._id, name: party.name})') + .modal-footer button.btn.btn-default(ng-click='$close()') Close From 5fab3881a209d6cf18bcd3583c3de0d785ef6593 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 13 Nov 2014 16:33:09 -0700 Subject: [PATCH 33/84] feat(invite-friends): allow specifying inveter's name (#4238 @lemoness) --- public/js/controllers/groupsCtrl.js | 8 ++--- src/controllers/user.js | 4 +-- views/shared/modals/invite-friends.jade | 41 ++++++++++++++----------- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index 1b3ef7fba2..8ac547a874 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -72,13 +72,13 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' group.invitee = ''; }); } - - $scope.emails = [{name:"",email:""},{name:"",email:""}]; //$scope.inviteLink = function(obj){ // return window.env.BASE_URL + '?partyInvite=' + encodeURIComponent(JSON.stringify(obj)); //} - $scope.inviteEmails = function(emails){ - $http.post('/api/v2/user/social/invite-friends', emails).success(function(){ + $scope.emails = [{name:"",email:""},{name:"",email:""}]; + $scope.inviter = User.user.profile.name; + $scope.inviteEmails = function(inviter, emails){ + $http.post('/api/v2/user/social/invite-friends', {inviter:inviter, emails:emails}).success(function(){ Notification.text("Invitations sent!"); $scope.emails = [{name:'',email:''},{name:'',email:''}]; }); diff --git a/src/controllers/user.js b/src/controllers/user.js index 6fd278ef39..6162643fcf 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -402,11 +402,11 @@ api.inviteFriends = function(req, res, next) { Group.findOne({type:'party', members:{'$in': [res.locals.user._id]}}).select('_id name').exec(function(err,party){ if (err) return next(err); var link = nconf.get('BASE_URL') + '?' + qs.stringify({partyInvite:{id:party._id, inviter:res.locals.user._id, name:party.name}}); - _.each(req.body, function(invite){ + _.each(req.body.emails, function(invite){ if (invite.email) { var variables = [ {name: 'LINK', content: link}, - {name: 'INVITER', content: res.locals.user.profile.name}, + {name: 'INVITER', content: req.body.inviter || res.locals.user.profile.name}, {name: 'INVITEE', content: invite.name} ]; // TODO implement "users can only be invited once" diff --git a/views/shared/modals/invite-friends.jade b/views/shared/modals/invite-friends.jade index 87b7e36215..a658b5df0d 100644 --- a/views/shared/modals/invite-friends.jade +++ b/views/shared/modals/invite-friends.jade @@ -15,24 +15,29 @@ script(type='text/ng-template', id='modals/invite-friends.html') p.alert.alert-info Invite friends by email. If they join via your email, they'll automatically be invited to your party. - table.table.table-striped - thead - tr - th Name - th Email - tbody - tr(ng-repeat='email in emails') - td - input.form-control(type='text', ng-model='email.name') - td - input.form-control(type='email', ng-model='email.email') - tr - td(colspan=2) - button.btn.btn-xs.pull-right(ng-click='emails = emails.concat([{name:"",email:""}])') - i.glyphicon.glyphicon-plus - tr - td(colspan=2) - button.btn.btn-primary.pull-right(ng-click='inviteEmails(emails)') Invite New User(s) + form.form-horizontal(ng-submit='inviteEmails(inviter, emails)') + table.table.table-striped + thead + tr + th Name + th Email + tbody + tr(ng-repeat='email in emails') + td + input.form-control(type='text', ng-model='email.name') + td + input.form-control(type='email', ng-model='email.email') + tr + td(colspan=2) + a.btn.btn-xs.pull-right(ng-click='emails = emails.concat([{name:"",email:""}])') + i.glyphicon.glyphicon-plus + tr + td.form-group(colspan=2) + label.col-sm-1.control-label By: + .col-sm-7 + input.form-control(type='text', ng-model='inviter') + .col-sm-4 + button.btn.btn-primary(type='submit') Invite New User(s) //- hr From 1d85bcd14e5a1a4cc8b0c5f059ee985dae302c08 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 13 Nov 2014 16:38:32 -0700 Subject: [PATCH 34/84] bailey(invite-friends) --- views/shared/new-stuff.jade | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/views/shared/new-stuff.jade b/views/shared/new-stuff.jade index cd1d2b1383..da33650f34 100644 --- a/views/shared/new-stuff.jade +++ b/views/shared/new-stuff.jade @@ -8,13 +8,37 @@ table h3.popover-title a(target='_blank', href='https://twitter.com/Mihakuu') Bailey .popover-content + h5 Share Avatar To Social Media, Email Invites, First Mini Quest, And Data Tab table.table.table-striped tr td - h5 New Equipment Quest Line: The Golden Knight! - p The Golden Knight believes that she is the perfect Habitican, and that anyone who slips up in their quest for self-improvement is a lazy failure. Can you talk some sense into her - or will it come to blows? If you complete the entire quest line, you'll be rewarded with a legendary weapon... - p The first scroll in this quest line, "A Stern Talking-to," drops automatically at Level 40! If you're already over Level 40, you will automatically be awarded this quest - just check off a task and then check your inventory. - p.small.muted 11/12/2014 + h5 Share Avatar To Social Media + p You can now automatically share your avatar and public profile to social media! Jut hover over the picture and click the "Share" button in the right-hand corner. Show off your outfit, your achievements, and your profile picture! Note that your tasks, as always, remain 100% private. + p.small.muted by Lefnire + tr + td + h5 Invite Friends To Party Via Email + p Do you want to invite friends to join your party without inputting their User ID? Now you can send them an email directly from the party page - even if they don't have an account yet! + p.small.muted by Lefnire + tr + td + h5 Mini Quest: The Basi-List! + p Now when someone accepts your party invitation and joins your party, you will be given a Mini Quest: The Basi-List! Battle the Basi-List with your friends for an XP and GP reward. + p.small.muted by Arcosine and Redphoenix + tr + td + h5 Data Tab + p Now you can access the Data Display Tool and Export Data from the toolbar! + p.small.muted by ShilohT + p.small.muted 11/13/2014 + +h5 11/12/2014 +table.table.table-striped + tr + td + h5 New Equipment Quest Line: The Golden Knight! + p The Golden Knight believes that she is the perfect Habitican, and that anyone who slips up in their quest for self-improvement is a lazy failure. Can you talk some sense into her - or will it come to blows? If you complete the entire quest line, you'll be rewarded with a legendary weapon... + p The first scroll in this quest line, "A Stern Talking-to," drops automatically at Level 40! If you're already over Level 40, you will automatically be awarded this quest - just check off a task and then check your inventory. h5 11/09/2014 - Facebook Login Fixed For Mobile And Community Guidelines To Chat table.table.table-striped From 64fd6c02f772886ac3c826399075d9ba69f77986 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 13 Nov 2014 23:00:22 -0700 Subject: [PATCH 35/84] fix(mods): restricted projected fields for mods-listing. Also, static-bind for perf --- src/models/user.js | 8 ++++++-- views/options/social/tavern.jade | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/models/user.js b/src/models/user.js index 0d377efa3c..d1d4682f70 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -463,6 +463,10 @@ UserSchema.methods.unlink = function(options, cb) { module.exports.schema = UserSchema; module.exports.model = mongoose.model("User", UserSchema); -mongoose.model("User").find({$query:{'contributor.admin':true}, $orderby:{'contributor.level':-1, 'backer.npc':-1, 'profile.name':1}},function(err,mods){ - module.exports.mods = mods +mongoose.model("User") + .find({'contributor.admin':true}) + .sort('-contributor.level -backer.npc profile.name') + .select('profile contributor backer') + .exec(function(err,mods){ + module.exports.mods = mods }); diff --git a/views/options/social/tavern.jade b/views/options/social/tavern.jade index 91b5d8caa3..72826de4fc 100644 --- a/views/options/social/tavern.jade +++ b/views/options/social/tavern.jade @@ -149,10 +149,10 @@ != ' ' + env.t('tavernAlert1') + ' ' + env.t('tavernAlert2') + '.
' + env.t('moderatorIntro1') span(ng-repeat='mod in env.mods') |    - span(ng-if='mod.contributor.admin',popover=env.t('gamemaster'),popover-trigger='mouseenter',popover-placement='right') - a.label.label-default(ng-class='userLevelStyle(mod)', ng-click='clickMember(mod._id, true)') - {{mod.profile.name}}  - span(ng-class='userAdminGlyphiconStyle(mod)') + span(ng-if='::mod.contributor.admin',popover=env.t('gamemaster'),popover-trigger='mouseenter',popover-placement='right') + a.label.label-default(ng-class='::userLevelStyle(mod)', ng-click='clickMember(mod._id, true)') + {{::mod.profile.name}}  + span(ng-class='::userAdminGlyphiconStyle(mod)') p =env.t('communityGuidelinesRead1') |   From 25a5bf92a6fe7fa5e59ae319e30ab568f8027005 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 14 Nov 2014 17:47:51 -0700 Subject: [PATCH 36/84] fix(paypal): move from classic paypal APIs to paypal REST SDK --- config.json.example | 8 +- package.json | 5 +- src/controllers/payments.js | 141 ++----------------- src/controllers/payments/paypal.js | 217 +++++++++++++++++++++++++++++ test/api.mocha.coffee | 2 +- 5 files changed, 236 insertions(+), 137 deletions(-) create mode 100644 src/controllers/payments/paypal.js diff --git a/config.json.example b/config.json.example index aefb56d1b9..19b58c2a1d 100644 --- a/config.json.example +++ b/config.json.example @@ -20,9 +20,6 @@ "NEW_RELIC_APPLICATION_ID":"NEW_RELIC_APPLICATION_ID", "NEW_RELIC_API_KEY":"NEW_RELIC_API_KEY", "GA_ID": "GA_ID", - "PAYPAL_USERNAME": "PAYPAL_USERNAME", - "PAYPAL_PASSWORD": "PAYPAL_PASSWORD", - "PAYPAL_SIGNATURE": "PAYPAL_SIGNATURE", "EMAIL_SERVER_URL": "http://example.com", "EMAIL_SERVER_AUTH_USER": "user", "EMAIL_SERVER_AUTH_PASSWORD": "password", @@ -30,6 +27,11 @@ "bucket":"bucket", "accessKeyId":"accessKeyId", "secretAccessKey":"secretAccessKey" + }, + "PAYPAL":{ + "mode":"sandbox", + "client_id":"client_id", + "client_secret":"client_secret" } } diff --git a/package.json b/package.json index 5fe7519805..fc3edb043b 100644 --- a/package.json +++ b/package.json @@ -40,9 +40,8 @@ "pageres": "^1.0.1", "passport": "~0.2.1", "passport-facebook": "Fonger/passport-facebook#a8f98adcddad99caa9a918bc7b76462c92c5c9fd", - "paypal-express-checkout": "git://github.com/HabitRPG/node-paypal-express-checkout#habitrpg", - "paypal-ipn": "~1.0.1", - "paypal-recurring": "git://github.com/jaybryant/paypal-recurring#656b496f43440893c984700191666a5c5c535dca", + "paypal-ipn": "2.1.0", + "paypal-rest-sdk": "^1.2.1", "pretty-data": "git://github.com/vkiryukhin/pretty-data#master", "qs": "^2.3.2", "request": "~2.44.0", diff --git a/src/controllers/payments.js b/src/controllers/payments.js index fd2d3ff27a..c92450a38e 100644 --- a/src/controllers/payments.js +++ b/src/controllers/payments.js @@ -2,28 +2,18 @@ var _ = require('lodash'); var logger = require('../logging'); -var ipn = require('paypal-ipn'); var shared = require('habitrpg-shared'); var nconf = require('nconf'); var async = require('async'); var User = require('./../models/user').model; var utils = require('./../utils'); var logging = require('./../logging'); -var userAPI = require('./user'); var request = require('request'); var moment = require('moment'); var api = module.exports; var isProduction = nconf.get("NODE_ENV") === "production"; var stripe = require("stripe")(nconf.get('STRIPE_API_KEY')); - -var PaypalRecurring = require('paypal-recurring'); -var paypalRecurring = new PaypalRecurring({ - username: nconf.get('PAYPAL_USERNAME'), - password: nconf.get('PAYPAL_PASSWORD'), - signature: nconf.get('PAYPAL_SIGNATURE') -}, isProduction ? "production" : "sandbox"); -var paypalCheckout = require('paypal-express-checkout') - .init(nconf.get('PAYPAL_USERNAME'), nconf.get('PAYPAL_PASSWORD'), nconf.get('PAYPAL_SIGNATURE'), nconf.get('BASE_URL'), nconf.get('BASE_URL'), !isProduction); +var paypal = require('./payments/paypal'); function revealMysteryItems(user) { _.each(shared.content.gear.flat, function(item) { @@ -39,7 +29,7 @@ function revealMysteryItems(user) { }); } -function createSubscription(user, data) { +var createSubscription = api.createSubscription = function(user, data) { if (!user.purchased.plan) user.purchased.plan = {}; _(user.purchased.plan) .merge({ // override with these values @@ -64,7 +54,7 @@ function createSubscription(user, data) { /** * Sets their subscription to be cancelled later */ -function cancelSubscription(user, data){ +var cancelSubscription = api.cancelSubscription = function(user, data) { var du = user.purchased.plan.dateUpdated, now = moment(); if(isProduction) utils.txnEmail(user, 'cancel-subscription'); user.purchased.plan.dateTerminated = @@ -72,10 +62,9 @@ function cancelSubscription(user, data){ .add({months:1}) .toDate(); utils.ga.event('unsubscribe', 'Stripe').send(); - } -function buyGems(user, data) { +var buyGems = api.buyGems = function(user, data) { user.balance += 5; user.purchased.txnCount++; if(isProduction) utils.txnEmail(user, 'donation'); @@ -83,12 +72,6 @@ function buyGems(user, data) { utils.ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + "-checkout", "Gems > " + data.paymentMethod).send(); } -// Expose some functions for tests -if (nconf.get('NODE_ENV')==='testing') { - api.cancelSubscription = cancelSubscription; - api.createSubscription = createSubscription; -} - /* Setup Stripe response when posting payment */ @@ -103,7 +86,7 @@ api.stripeCheckout = function(req, res, next) { email: req.body.email, metadata: {uuid: res.locals.user._id}, card: token, - plan: req.query.plan, + plan: req.query.plan }, cb); } else { stripe.charges.create({ @@ -174,111 +157,9 @@ api.stripeSubscribeEdit = function(req, res, next) { }); }; -api.paypalSubscribe = function(req,res,next) { - // Authenticate a future subscription of ~5 USD - paypalRecurring.authenticate({ - RETURNURL: nconf.get('BASE_URL') + '/paypal/subscribe/success?uuid=' + res.locals.user._id, - CANCELURL: nconf.get("BASE_URL"), - PAYMENTREQUEST_0_AMT: 5, - L_BILLINGAGREEMENTDESCRIPTION0: "HabitRPG Subscription" - }, function(err, data, url) { - // Redirect the user if everything went well with - // a HTTP 302 according to PayPal's guidelines - if (err) return next(err); - res.redirect(302, url); - }); -}; - -api.paypalSubscribeSuccess = function(req,res,next) { - // Create a subscription of 10 USD every month - var uuid = req.query.uuid; - if (!uuid) return next("UUID required"); - paypalRecurring.createSubscription(req.query.token, req.query.PayerID,{ - AMT: 5, - DESC: "HabitRPG Subscription", - BILLINGPERIOD: "Month", - BILLINGFREQUENCY: 1, - }, function(err, data) { - if (err) return res.next(err); - User.findById(uuid, function(err,user){ - if (err) return next(err); - createSubscription(user, {customerId: data.PROFILEID, paymentMethod: 'Paypal'}); - user.save(function(err,saved){ - res.redirect('/'); - }) - }) - }); -}; - -api.paypalSubscribeCancel = function(req, res, next) { - 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) { - paypalRecurring.modifySubscription(user.purchased.plan.customerId, 'cancel', cb); - }, - function(response, cb) { - cancelSubscription(user); - user.save(cb); - } - ], function(err, saved){ - if (err) return next(err); - res.redirect('/'); - user = null; - }); -}; - -api.paypalCheckout = function(req, res, next) { - var opts = {RETURNURL:nconf.get('BASE_URL') + '/paypal/checkout/success?uuid=' + res.locals.user._id}; - paypalCheckout.pay(+new Date(), 5, 'HabitRPG Gems', 'USD', opts, function(err, url) { - if (err) return next(err); - res.redirect(url); - }); -}; - -api.paypalCheckoutSuccess = function(req,res,next) { - paypalCheckout.detail(req.query.token, req.query.PayerID, function(err, data, invoiceNumber, price) { - // see `data` vars at https://github.com/petersirka/node-paypal-express-checkout#paypal-account - //if (err) return next('PayPal Error: ' + msg); - if (err) return next(err); - if (data.ACK !== 'Success') return next('PayPal transaction failed, please try again'); - - var uuid = req.query.uuid; //, apiToken = query.apiToken; - User.findById(uuid , function(err, user) { - if (_.isEmpty(user)) err = "user not found with uuid " + uuid + " when completing paypal transaction"; - if (err) return next(err); - buyGems(user, {customerId:req.query.PayerID, paymentMethod:'Paypal'}); - user.save(function(){ - if (err) return next(err); - res.redirect('/'); - uuid = null; - }); - }); - }); -}; - -/** - * General IPN handler. We could use this for all paypal transaction handling (instead of the above functions), but I've - * found it extremely unreliable. Instead, here we'll cancel HabitRPG subscriptions for users who manually cancel their - * recurring paypal payments. - */ -api.paypalIPN = function(req, res, next) { - // Must respond to PayPal IPN request with an empty 200 first, if using Express uncomment the following: - res.send(200); - ipn.verify(req.body, function callback(err, msg) { - if (err) return logger.error(msg); - switch (req.body.txn_type) { - // TODO what's the diff b/w the two data.txn_types below? The docs recommend subscr_cancel, but I'm getting the other one instead... - case 'recurring_payment_profile_cancel': - case 'subscr_cancel': - User.findOne({'purchased.plan.customerId':req.body.recurring_payment_id},function(err, user){ - if (err) return logger.error(err); - if (_.isEmpty(user)) return; // looks like the cancellation was already handled properly above (see api.paypalSubscribeCancel) - cancelSubscription(user); - user.save(); - }); - break; - } - }); -}; +api.paypalSubscribe = paypal.createBillingAgreement; +api.paypalSubscribeSuccess = paypal.executeBillingAgreement; +api.paypalSubscribeCancel = paypal.cancelSubscription; +api.paypalCheckout = paypal.createPayment; +api.paypalCheckoutSuccess = paypal.executePayment; +api.paypalIPN = paypal.ipn; \ No newline at end of file diff --git a/src/controllers/payments/paypal.js b/src/controllers/payments/paypal.js new file mode 100644 index 0000000000..d6efdd17ea --- /dev/null +++ b/src/controllers/payments/paypal.js @@ -0,0 +1,217 @@ +var nconf = require('nconf'); +var moment = require('moment'); +var async = require('async'); +var _ = require('lodash'); +var url = require('url'); +var mongoose = require('mongoose'); +var payments = require('./../payments'); +var logger = require('../../logging'); +var ipn = require('paypal-ipn'); +var paypal = require('paypal-rest-sdk'); + +paypal.configure({ + 'mode': nconf.get("PAYPAL:mode"), //sandbox or live + 'client_id': nconf.get("PAYPAL:client_id"), + 'client_secret': nconf.get("PAYPAL:client_secret") +}); + +var parseErr = function(err){ + return (err.response && err.response.message || err.response.details[0].issue) || err; +} + +// Initialize Billing Plans +var billingPlanID; +var billingPlanTitle ="HabitRPG subscription ($5 month-to-month)"; +(function(){ + var billingPlanAttributes = { + // https://developer.paypal.com/docs/api/#billing-plans-and-agreements + "name": billingPlanTitle, + "description": billingPlanTitle, + "type": "INFINITE", + "merchant_preferences": { + "auto_bill_amount": "yes", + "cancel_url": nconf.get("BASE_URL"), + "return_url": nconf.get('BASE_URL') + '/paypal/subscribe/success' + }, + "payment_definitions": [{ + "name": billingPlanTitle, + "type": "REGULAR", + "frequency_interval": "1", + "frequency": "MONTH", + "cycles": "0", + "amount": { + "currency": "USD", + "value": "5" + } + }] + }; + + async.waterfall([ + function(cb) { + paypal.billingPlan.list({status: 'ACTIVE'}, cb); + }, + function(plans, cb){ + var plan = _.find(plans.plans, {name:billingPlanTitle}); + if (plan) return cb(null, plan); + paypal.billingPlan.create(billingPlanAttributes, cb); + }, + function(plan, cb){ + if (plan.state == "ACTIVE") return cb(null, plan); + // Super obvious this stuff, right? *sigh* + var billingPlanUpdateAttributes = [{ + "op": "replace", + "path": "/", + "value": { + "state": "ACTIVE" + } + }]; + // Activate the plan by changing status to Active + paypal.billingPlan.update(plan.id, billingPlanUpdateAttributes, function(err, response){ + if (err) return cb(err); + cb(null, plan); + }); + }, + ],function(err, plan){ + billingPlanID = plan.id; + }) +})(); + +exports.createBillingAgreement = function(req,res,next){ + var billingAgreementAttributes = { + "name": billingPlanTitle, + "description": billingPlanTitle, + "start_date": moment().add({minutes:1}).format(), + "plan": { + "id": billingPlanID + }, + "payer": { + "payment_method": "paypal" + } + }; + paypal.billingAgreement.create(billingAgreementAttributes, function (err, billingAgreement) { + if (err) return next(parseErr(err)); + // For approving subscription via Paypal, first redirect user to: approval_url + var approval_url = _.find(billingAgreement.links, {rel:'approval_url'}).href; + res.redirect(approval_url); + }); +} + +exports.executeBillingAgreement = function(req,res,next){ + async.waterfall([ + function(cb){ + paypal.billingAgreement.execute(req.query.token, {}, cb); + }, + function(billingAgreement, cb){ + mongoose.model('User').findById(req.session.userId, function(err, user){ + if (err) return cb(err); + cb(null, {billingAgreement:billingAgreement, user:user}); + }); + }, + function(data, cb){ + payments.createSubscription(data.user, {customerId: data.billingAgreement.id, paymentMethod: 'Paypal'}); + data.user.save(cb); + } + ],function(err){ + if (err) return next(parseErr(err)); + res.redirect('/'); + }) +} + +exports.createPayment = function(req, res, next) { + var create_payment = { + "intent": "sale", + "payer": { + "payment_method": "paypal" + }, + "redirect_urls": { + "return_url": nconf.get('BASE_URL') + '/paypal/checkout/success', + "cancel_url": nconf.get('BASE_URL') + }, + "transactions": [{ + "item_list": { + "items": [{ + "name": "HabitRPG Gems", + //"sku": "1", + "price": "5.00", + "currency": "USD", + "quantity": 1 + }] + }, + "amount": { + "currency": "USD", + "total": "5.00" + }, + "description": "HabitRPG Gems" + }] + }; + paypal.payment.create(create_payment, function (err, payment) { + if (err) return next(parseErr(err)); + var link = _.find(payment.links, {rel: 'approval_url'}).href; + res.redirect(link); + }); +} + +exports.executePayment = function(req, res, next) { + var paymentId = req.query.paymentId, + PayerID = req.query.PayerID; + async.waterfall([ + function(cb){ + paypal.payment.execute(paymentId, {payer_id: PayerID}, cb); + }, + function(payment, cb){ + mongoose.model('User').findById(req.session.userId, cb); + }, + function(user, cb){ + if (_.isEmpty(user)) return cb("user not found when completing paypal transaction"); + payments.buyGems(user, {customerId:PayerID, paymentMethod:'Paypal'}); + user.save(cb); + } + ],function(err, saved){ + if (err) return next(parseErr(err)); + res.redirect('/'); + }) +} + +exports.cancelSubscription = function(req, res, next){ + 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) { + paypal.billingAgreement.cancel(user.purchased.plan.customerId, {note: "Canceling the subscription"}, cb); + }, + function(response, cb) { + payments.cancelSubscription(user); + user.save(cb); + } + ], function(err, saved){ + if (err) return next(parseErr(err)); + res.redirect('/'); + user = null; + }); +} + +/** + * General IPN handler. We catch cancelled HabitRPG subscriptions for users who manually cancel their + * recurring paypal payments in their paypal dashboard. Remove this when we can move to webhooks or some other solution + */ +exports.ipn = function(req, res, next) { + console.log('IPN Called'); + res.send(200); // Must respond to PayPal IPN request with an empty 200 first + ipn.verify(req.body, function(err, msg) { + if (err) return logger.error(msg); + switch (req.body.txn_type) { + // TODO what's the diff b/w the two data.txn_types below? The docs recommend subscr_cancel, but I'm getting the other one instead... + case 'recurring_payment_profile_cancel': + case 'subscr_cancel': + mongoose.model('User').findOne({'purchased.plan.customerId':req.body.recurring_payment_id},function(err, user){ + if (err) return logger.error(err); + if (_.isEmpty(user)) return; // looks like the cancellation was already handled properly above (see api.paypalSubscribeCancel) + payments.cancelSubscription(user); + user.save(); + }); + break; + } + }); +}; + diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index a1e75ec11e..28b2a97c3d 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -606,7 +606,7 @@ describe "API", -> user = _user done() - it.skip "Handles unsubscription", (done) -> + it "Handles unsubscription", (done) -> cron = -> user.lastCron = moment().subtract("d", 1) user.fns.cron() From 727c96e5dafcdbc315f8934a7a2d434be2a2a00c Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Sun, 16 Nov 2014 10:56:18 +0100 Subject: [PATCH 37/84] fix(config): switch to new config style for emails, cc @lefnire --- config.json.example | 9 +++++---- src/utils.js | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/config.json.example b/config.json.example index 19b58c2a1d..aa70349d0a 100644 --- a/config.json.example +++ b/config.json.example @@ -20,9 +20,11 @@ "NEW_RELIC_APPLICATION_ID":"NEW_RELIC_APPLICATION_ID", "NEW_RELIC_API_KEY":"NEW_RELIC_API_KEY", "GA_ID": "GA_ID", - "EMAIL_SERVER_URL": "http://example.com", - "EMAIL_SERVER_AUTH_USER": "user", - "EMAIL_SERVER_AUTH_PASSWORD": "password", + "EMAIL_SERVER": { + "url": "http://example.com", + "authUser": "user", + "authPassword": "password" + }, "S3":{ "bucket":"bucket", "accessKeyId":"accessKeyId", @@ -33,5 +35,4 @@ "client_id":"client_id", "client_secret":"client_secret" } - } diff --git a/src/utils.js b/src/utils.js index 06ee14eb9d..0789038b5e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -38,11 +38,11 @@ module.exports.txnEmail = function(mailingInfo, emailType, variables){ if (mailingInfo._id) mailingInfo = getMailingInfo(mailingInfo); if (!mailingInfo.email) return; request({ - url: nconf.get('EMAIL_SERVER_URL') + '/job', + url: nconf.get('EMAIL_SERVER:url') + '/job', method: 'POST', auth: { - user: nconf.get('EMAIL_SERVER_AUTH_USER'), - pass: nconf.get('EMAIL_SERVER_AUTH_PASSWORD') + user: nconf.get('EMAIL_SERVER:authUser'), + pass: nconf.get('EMAIL_SERVER:authPassword') }, json: { type: 'email', From 45845228d085131d1c6f7f4b75729d35e67c18d1 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Sun, 16 Nov 2014 11:39:56 +0100 Subject: [PATCH 38/84] fix(invite-friends): generate correct link on the client --- public/js/controllers/groupsCtrl.js | 20 +++++++++++++++++--- views/shared/modals/invite-friends.jade | 7 +++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index 8ac547a874..1d4456d09e 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -72,9 +72,23 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' group.invitee = ''; }); } - //$scope.inviteLink = function(obj){ - // return window.env.BASE_URL + '?partyInvite=' + encodeURIComponent(JSON.stringify(obj)); - //} + + var serializeQs = function(obj, prefix){ + var str = []; + for(var p in obj) { + if (obj.hasOwnProperty(p)) { + var k = prefix ? prefix + "[" + p + "]" : p, v = obj[p]; + str.push(typeof v == "object" ? + serializeQs(v, k) : + encodeURIComponent(k) + "=" + encodeURIComponent(v)); + } + } + return str.join("&"); + } + + $scope.inviteLink = function(obj){ + return window.env.BASE_URL + '?' + serializeQs({partyInvite: obj}); + } $scope.emails = [{name:"",email:""},{name:"",email:""}]; $scope.inviter = User.user.profile.name; $scope.inviteEmails = function(inviter, emails){ diff --git a/views/shared/modals/invite-friends.jade b/views/shared/modals/invite-friends.jade index a658b5df0d..47c8becda4 100644 --- a/views/shared/modals/invite-friends.jade +++ b/views/shared/modals/invite-friends.jade @@ -39,10 +39,9 @@ script(type='text/ng-template', id='modals/invite-friends.html') .col-sm-4 button.btn.btn-primary(type='submit') Invite New User(s) - //- - hr - p.alert.alert-info Or share this link (copy/paste): - input.form-control(type='text', ng-value='inviteLink({id: party._id, inviter: user._id, name: party.name})') + hr + p.alert.alert-info Or share this link (copy/paste): + input.form-control(type='text', ng-value='inviteLink({id: party._id, inviter: user._id, name: party.name})') .modal-footer button.btn.btn-default(ng-click='$close()') Close From 7a121b4273969619b27bdc01ae54a6c52137727a Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 16 Nov 2014 08:03:34 -0700 Subject: [PATCH 39/84] fix(paypal): don't crash server when no paypal plan.id (see #4246) --- src/controllers/payments/paypal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/payments/paypal.js b/src/controllers/payments/paypal.js index d6efdd17ea..e46a7e4940 100644 --- a/src/controllers/payments/paypal.js +++ b/src/controllers/payments/paypal.js @@ -72,7 +72,7 @@ var billingPlanTitle ="HabitRPG subscription ($5 month-to-month)"; }); }, ],function(err, plan){ - billingPlanID = plan.id; + billingPlanID = plan && plan.id || "none"; }) })(); From 47cd11b76f4e7a2f48915a8f27a5977f910717b2 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 16 Nov 2014 09:47:15 -0700 Subject: [PATCH 40/84] refactor(paypal): move billing plan setup to a one-time file. It needn't be called every server-run (see #4246) --- config.json.example | 1 + src/controllers/payments/paypal.js | 63 ++--------------- .../payments/paypalBillingSetup.js | 68 +++++++++++++++++++ 3 files changed, 75 insertions(+), 57 deletions(-) create mode 100644 src/controllers/payments/paypalBillingSetup.js diff --git a/config.json.example b/config.json.example index aa70349d0a..a9e7d5f4ac 100644 --- a/config.json.example +++ b/config.json.example @@ -31,6 +31,7 @@ "secretAccessKey":"secretAccessKey" }, "PAYPAL":{ + "billing_plan_id":"billing_plan_id", "mode":"sandbox", "client_id":"client_id", "client_secret":"client_secret" diff --git a/src/controllers/payments/paypal.js b/src/controllers/payments/paypal.js index e46a7e4940..536f959ef9 100644 --- a/src/controllers/payments/paypal.js +++ b/src/controllers/payments/paypal.js @@ -9,6 +9,11 @@ var logger = require('../../logging'); var ipn = require('paypal-ipn'); var paypal = require('paypal-rest-sdk'); +// This is the plan.id for paypal subscriptions. You have to set up billing plans via their REST sdk (they don't have +// a web interface for billing-plan creation), see ./paypalBillingSetup.js for how. After the billing plan is created +// there, get it's plan.id and store it in config.json +var billingPlanID = nconf.get('PAYPAL:billing_plan_id'); + paypal.configure({ 'mode': nconf.get("PAYPAL:mode"), //sandbox or live 'client_id': nconf.get("PAYPAL:client_id"), @@ -19,64 +24,8 @@ var parseErr = function(err){ return (err.response && err.response.message || err.response.details[0].issue) || err; } -// Initialize Billing Plans -var billingPlanID; -var billingPlanTitle ="HabitRPG subscription ($5 month-to-month)"; -(function(){ - var billingPlanAttributes = { - // https://developer.paypal.com/docs/api/#billing-plans-and-agreements - "name": billingPlanTitle, - "description": billingPlanTitle, - "type": "INFINITE", - "merchant_preferences": { - "auto_bill_amount": "yes", - "cancel_url": nconf.get("BASE_URL"), - "return_url": nconf.get('BASE_URL') + '/paypal/subscribe/success' - }, - "payment_definitions": [{ - "name": billingPlanTitle, - "type": "REGULAR", - "frequency_interval": "1", - "frequency": "MONTH", - "cycles": "0", - "amount": { - "currency": "USD", - "value": "5" - } - }] - }; - - async.waterfall([ - function(cb) { - paypal.billingPlan.list({status: 'ACTIVE'}, cb); - }, - function(plans, cb){ - var plan = _.find(plans.plans, {name:billingPlanTitle}); - if (plan) return cb(null, plan); - paypal.billingPlan.create(billingPlanAttributes, cb); - }, - function(plan, cb){ - if (plan.state == "ACTIVE") return cb(null, plan); - // Super obvious this stuff, right? *sigh* - var billingPlanUpdateAttributes = [{ - "op": "replace", - "path": "/", - "value": { - "state": "ACTIVE" - } - }]; - // Activate the plan by changing status to Active - paypal.billingPlan.update(plan.id, billingPlanUpdateAttributes, function(err, response){ - if (err) return cb(err); - cb(null, plan); - }); - }, - ],function(err, plan){ - billingPlanID = plan && plan.id || "none"; - }) -})(); - exports.createBillingAgreement = function(req,res,next){ + var billingPlanTitle ="HabitRPG subscription ($5 month-to-month)"; var billingAgreementAttributes = { "name": billingPlanTitle, "description": billingPlanTitle, diff --git a/src/controllers/payments/paypalBillingSetup.js b/src/controllers/payments/paypalBillingSetup.js new file mode 100644 index 0000000000..d4d7f8e167 --- /dev/null +++ b/src/controllers/payments/paypalBillingSetup.js @@ -0,0 +1,68 @@ +// This file is used for creating paypal billing plans. PayPal doesn't have a web interface for setting up recurring +// payment plan definitions, instead you have to create it via their REST SDK and keep it updated the same way. So this +// file will be used once for initing your billing plan (then you get the resultant plan.id to store in config.json), +// and once for any time you need to edit the plan thereafter + +var path = require('path'); +var nconf = require('nconf'); +nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../../../config.json'))); +var paypal = require('paypal-rest-sdk'); +var OP = "list"; // list create update remove + +paypal.configure({ + 'mode': nconf.get("PAYPAL:mode"), //sandbox or live + 'client_id': nconf.get("PAYPAL:client_id"), + 'client_secret': nconf.get("PAYPAL:client_secret") +}); + +var billingPlanTitle ="HabitRPG subscription ($5 month-to-month)"; +// https://developer.paypal.com/docs/api/#billing-plans-and-agreements +var billingPlanAttributes = { + "name": billingPlanTitle, + "description": billingPlanTitle, + "type": "INFINITE", + "merchant_preferences": { + "auto_bill_amount": "yes", + "cancel_url": nconf.get("BASE_URL"), + "return_url": nconf.get('BASE_URL') + '/paypal/subscribe/success' + }, + "payment_definitions": [{ + "name": billingPlanTitle, + "type": "REGULAR", + "frequency_interval": "1", + "frequency": "MONTH", + "cycles": "0", + "amount": { + "currency": "USD", + "value": "5" + } + }] +}; + +switch(OP) { + case "list": + paypal.billingPlan.list({status: 'ACTIVE'}, function(err, plans){ + console.log({err:err, plans:plans}); + }); + break; + case "update": + break; + case "create": + paypal.billingPlan.create(billingPlanAttributes, function(err,plan){ + if (plan.state == "ACTIVE") + return console.log({err:err, plan:plan}); + var billingPlanUpdateAttributes = [{ + "op": "replace", + "path": "/", + "value": { + "state": "ACTIVE" + } + }]; + // Activate the plan by changing status to Active + paypal.billingPlan.update(plan.id, billingPlanUpdateAttributes, function(err, response){ + console.log({err:err, response:response}); + }); + }); + case "remove": + break; +} \ No newline at end of file From 45e67f0a7ac7bcacb05d3b6244f06f92f1d903b9 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 16 Nov 2014 10:02:52 -0700 Subject: [PATCH 41/84] refactor(payments): cleanup into directory, separate paypal & stripe files --- src/controllers/payments.js | 165 ----------------------------- src/controllers/payments/index.js | 77 ++++++++++++++ src/controllers/payments/paypal.js | 2 +- src/controllers/payments/stripe.js | 89 ++++++++++++++++ 4 files changed, 167 insertions(+), 166 deletions(-) delete mode 100644 src/controllers/payments.js create mode 100644 src/controllers/payments/index.js create mode 100644 src/controllers/payments/stripe.js diff --git a/src/controllers/payments.js b/src/controllers/payments.js deleted file mode 100644 index c92450a38e..0000000000 --- a/src/controllers/payments.js +++ /dev/null @@ -1,165 +0,0 @@ -/* @see ./routes.coffee for routing*/ - -var _ = require('lodash'); -var logger = require('../logging'); -var shared = require('habitrpg-shared'); -var nconf = require('nconf'); -var async = require('async'); -var User = require('./../models/user').model; -var utils = require('./../utils'); -var logging = require('./../logging'); -var request = require('request'); -var moment = require('moment'); -var api = module.exports; -var isProduction = nconf.get("NODE_ENV") === "production"; -var stripe = require("stripe")(nconf.get('STRIPE_API_KEY')); -var paypal = require('./payments/paypal'); - -function revealMysteryItems(user) { - _.each(shared.content.gear.flat, function(item) { - if ( - item.klass === 'mystery' && - moment().isAfter(item.mystery.start) && - moment().isBefore(item.mystery.end) && - !user.items.gear.owned[item.key] && - !~user.purchased.plan.mysteryItems.indexOf(item.key) - ) { - user.purchased.plan.mysteryItems.push(item.key); - } - }); -} - -var createSubscription = api.createSubscription = function(user, data) { - if (!user.purchased.plan) user.purchased.plan = {}; - _(user.purchased.plan) - .merge({ // override with these values - planId:'basic_earned', - customerId: data.customerId, - dateUpdated: new Date(), - gemsBought: 0, - paymentMethod: data.paymentMethod, - dateTerminated: null - }) - .defaults({ // allow non-override if a plan was previously used - dateCreated: new Date(), - mysteryItems: [] - }); - revealMysteryItems(user); - if(isProduction) utils.txnEmail(user, 'subscription-begins'); - user.purchased.txnCount++; - utils.ga.event('subscribe', data.paymentMethod).send(); - utils.ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + '-subscription', data.paymentMethod + " > Stripe").send(); -} - -/** - * Sets their subscription to be cancelled later - */ -var cancelSubscription = api.cancelSubscription = function(user, data) { - var du = user.purchased.plan.dateUpdated, now = moment(); - if(isProduction) utils.txnEmail(user, 'cancel-subscription'); - user.purchased.plan.dateTerminated = - moment( now.format('MM') + '/' + moment(du).format('DD') + '/' + now.format('YYYY') ) - .add({months:1}) - .toDate(); - utils.ga.event('unsubscribe', 'Stripe').send(); -} - -var buyGems = api.buyGems = function(user, data) { - user.balance += 5; - user.purchased.txnCount++; - if(isProduction) utils.txnEmail(user, 'donation'); - utils.ga.event('checkout', data.paymentMethod).send(); - utils.ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + "-checkout", "Gems > " + data.paymentMethod).send(); -} - -/* - Setup Stripe response when posting payment - */ -api.stripeCheckout = function(req, res, next) { - var token = req.body.id; - var user = res.locals.user; - - async.waterfall([ - function(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) { - if (req.query.plan) { - createSubscription(user, {customerId: response.id, paymentMethod: 'Stripe'}); - } else { - buyGems(user, {customerId: response.id, paymentMethod: 'Stripe'}); - } - 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); - user = token = null; - }); -}; - -api.stripeSubscribeCancel = function(req, res, next) { - 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) { - cancelSubscription(user); - user.save(cb); - } - ], function(err, saved){ - if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors - res.redirect('/'); - user = null; - }); -}; - -api.stripeSubscribeEdit = function(req, res, next) { - var token = req.body.id; - var user = res.locals.user; - var user_id = user.purchased.plan.customerId; - var sub_id; - - async.waterfall([ - function(cb){ - stripe.customers.listSubscriptions(user_id, cb); - }, - function(response, cb) { - sub_id = response.data[0].id; - console.warn(sub_id); - console.warn([user_id, sub_id, { card: token }]); - stripe.customers.updateSubscription(user_id, sub_id, { card: token }, cb); - }, - function(response, cb) { - 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); - token = user = user_id = sub_id; - }); -}; - -api.paypalSubscribe = paypal.createBillingAgreement; -api.paypalSubscribeSuccess = paypal.executeBillingAgreement; -api.paypalSubscribeCancel = paypal.cancelSubscription; -api.paypalCheckout = paypal.createPayment; -api.paypalCheckoutSuccess = paypal.executePayment; -api.paypalIPN = paypal.ipn; \ No newline at end of file diff --git a/src/controllers/payments/index.js b/src/controllers/payments/index.js new file mode 100644 index 0000000000..ba6359d4f9 --- /dev/null +++ b/src/controllers/payments/index.js @@ -0,0 +1,77 @@ +/* @see ./routes.coffee for routing*/ +var _ = require('lodash'); +var shared = require('habitrpg-shared'); +var nconf = require('nconf'); +var utils = require('./../../utils'); +var moment = require('moment'); +var isProduction = nconf.get("NODE_ENV") === "production"; +var stripe = require('./stripe'); +var paypal = require('./paypal'); + +function revealMysteryItems(user) { + _.each(shared.content.gear.flat, function(item) { + if ( + item.klass === 'mystery' && + moment().isAfter(item.mystery.start) && + moment().isBefore(item.mystery.end) && + !user.items.gear.owned[item.key] && + !~user.purchased.plan.mysteryItems.indexOf(item.key) + ) { + user.purchased.plan.mysteryItems.push(item.key); + } + }); +} + +exports.createSubscription = function(user, data) { + if (!user.purchased.plan) user.purchased.plan = {}; + _(user.purchased.plan) + .merge({ // override with these values + planId:'basic_earned', + customerId: data.customerId, + dateUpdated: new Date(), + gemsBought: 0, + paymentMethod: data.paymentMethod, + dateTerminated: null + }) + .defaults({ // allow non-override if a plan was previously used + dateCreated: new Date(), + mysteryItems: [] + }); + revealMysteryItems(user); + if(isProduction) utils.txnEmail(user, 'subscription-begins'); + user.purchased.txnCount++; + utils.ga.event('subscribe', data.paymentMethod).send(); + utils.ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + '-subscription', data.paymentMethod + " > Stripe").send(); +} + +/** + * Sets their subscription to be cancelled later + */ +exports.cancelSubscription = function(user, data) { + var du = user.purchased.plan.dateUpdated, now = moment(); + if(isProduction) utils.txnEmail(user, 'cancel-subscription'); + user.purchased.plan.dateTerminated = + moment( now.format('MM') + '/' + moment(du).format('DD') + '/' + now.format('YYYY') ) + .add({months:1}) + .toDate(); + utils.ga.event('unsubscribe', 'Stripe').send(); +} + +exports.buyGems = function(user, data) { + user.balance += 5; + user.purchased.txnCount++; + if(isProduction) utils.txnEmail(user, 'donation'); + utils.ga.event('checkout', data.paymentMethod).send(); + utils.ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + "-checkout", "Gems > " + data.paymentMethod).send(); +} + +exports.stripeCheckout = stripe.checkout; +exports.stripeSubscribeCancel = stripe.subscribeCancel; +exports.stripeSubscribeEdit = stripe.subscribeEdit; + +exports.paypalSubscribe = paypal.createBillingAgreement; +exports.paypalSubscribeSuccess = paypal.executeBillingAgreement; +exports.paypalSubscribeCancel = paypal.cancelSubscription; +exports.paypalCheckout = paypal.createPayment; +exports.paypalCheckoutSuccess = paypal.executePayment; +exports.paypalIPN = paypal.ipn; \ No newline at end of file diff --git a/src/controllers/payments/paypal.js b/src/controllers/payments/paypal.js index 536f959ef9..dff576a2d2 100644 --- a/src/controllers/payments/paypal.js +++ b/src/controllers/payments/paypal.js @@ -4,7 +4,7 @@ var async = require('async'); var _ = require('lodash'); var url = require('url'); var mongoose = require('mongoose'); -var payments = require('./../payments'); +var payments = require('./index'); var logger = require('../../logging'); var ipn = require('paypal-ipn'); var paypal = require('paypal-rest-sdk'); diff --git a/src/controllers/payments/stripe.js b/src/controllers/payments/stripe.js new file mode 100644 index 0000000000..4f9a52afeb --- /dev/null +++ b/src/controllers/payments/stripe.js @@ -0,0 +1,89 @@ +var nconf = require('nconf'); +var stripe = require("stripe")(nconf.get('STRIPE_API_KEY')); +var async = require('async'); +var payments = require('./index'); + +/* + Setup Stripe response when posting payment + */ +exports.checkout = function(req, res, next) { + var token = req.body.id; + var user = res.locals.user; + + async.waterfall([ + function(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) { + if (req.query.plan) { + payments.createSubscription(user, {customerId: response.id, paymentMethod: 'Stripe'}); + } else { + payments.buyGems(user, {customerId: response.id, paymentMethod: 'Stripe'}); + } + 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); + user = token = null; + }); +}; + +exports.subscribeCancel = function(req, res, next) { + 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) { + payments.cancelSubscription(user); + user.save(cb); + } + ], function(err, saved){ + if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors + res.redirect('/'); + user = null; + }); +}; + +exports.subscribeEdit = function(req, res, next) { + var token = req.body.id; + var user = res.locals.user; + var user_id = user.purchased.plan.customerId; + var sub_id; + + async.waterfall([ + function(cb){ + stripe.customers.listSubscriptions(user_id, cb); + }, + function(response, cb) { + sub_id = response.data[0].id; + console.warn(sub_id); + console.warn([user_id, sub_id, { card: token }]); + stripe.customers.updateSubscription(user_id, sub_id, { card: token }, cb); + }, + function(response, cb) { + 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); + token = user = user_id = sub_id; + }); +}; \ No newline at end of file From 4d08bb92062999206d36e9a74e0c8bb305f384ca Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 16 Nov 2014 16:05:32 -0600 Subject: [PATCH 42/84] Added uuid parameter to data link --- views/shared/header/menu.jade | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/views/shared/header/menu.jade b/views/shared/header/menu.jade index 59665eb603..6fb2909a41 100644 --- a/views/shared/header/menu.jade +++ b/views/shared/header/menu.jade @@ -45,7 +45,7 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}') a(ui-sref='options.inventory.equipment')=env.t('equipment') ul.toolbar-submenu li - a(target="_blank" href='http://data.habitrpg.com')=env.t('dataTool') + a(target="_blank" ng-href='http://data.habitrpg.com?uuid={{user._id}}')=env.t('dataTool') li a(ui-sref='options.settings.export')=env.t('exportData') ul.toolbar-submenu @@ -118,14 +118,14 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}') li a(ui-sref='options.inventory.equipment')=env.t('equipment') li.toolbar-button-dropdown - a(target="_blank" href='http://data.habitrpg.com') + a(target="_blank" ng-href='http://data.habitrpg.com?uuid={{user._id}}') span=env.t('data') a(ng-click='expandMenu("data")', ng-class='{active: _expandedMenu == "data"}') span ☰ div(ng-if='_expandedMenu == "data"') ul.toolbar-submenu(ng-click='expandMenu(null)') li - a(target="_blank" href='http://data.habitrpg.com')=env.t('dataTool') + a(target="_blank" ng-href='http://data.habitrpg.com?uuid={{user._id}}')=env.t('dataTool') li a(ui-sref='options.settings.export')=env.t('exportData') li.toolbar-button-dropdown From 6ef838cf1a7e6497446615d2674b949d6f48257f Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 16 Nov 2014 20:03:56 -0800 Subject: [PATCH 43/84] feat(webhooks): move webhooks from array to object (everything should really be stored as objects). Add to API routes (see HabitRPG/habitrpg-shared#83a22b8). Note, sorting doesn't work. Meh. --- public/js/controllers/settingsCtrl.js | 25 +++++++++++---------- src/models/user.js | 2 +- src/routes/apiv2.coffee | 31 +++++++++++++++++++++++++++ views/options/settings.jade | 15 ++++++------- 4 files changed, 53 insertions(+), 20 deletions(-) diff --git a/public/js/controllers/settingsCtrl.js b/public/js/controllers/settingsCtrl.js index 8802a09b4a..0c0123d1f3 100644 --- a/public/js/controllers/settingsCtrl.js +++ b/public/js/controllers/settingsCtrl.js @@ -2,8 +2,8 @@ // Make user and settings available for everyone through root scope. habitrpg.controller('SettingsCtrl', - ['$scope', 'User', '$rootScope', '$http', 'ApiUrlService', 'Guide', '$location', '$timeout', 'Notification', - function($scope, User, $rootScope, $http, ApiUrlService, Guide, $location, $timeout, Notification) { + ['$scope', 'User', '$rootScope', '$http', 'ApiUrlService', 'Guide', '$location', '$timeout', 'Notification', 'Shared', + function($scope, User, $rootScope, $http, ApiUrlService, Guide, $location, $timeout, Notification, Shared) { // FIXME we have this re-declared everywhere, figure which is the canonical version and delete the rest // $scope.auth = function (id, token) { @@ -163,18 +163,21 @@ habitrpg.controller('SettingsCtrl', $rootScope.$state.go('tasks'); } - //FIXME push this all to habitrpg-shared so we have API routes for this - $scope.addWebhook = function(webhook) { - User.set({'preferences.webhooks':User.user.preferences.webhooks.concat({url:webhook, enabled:true})}); - $scope._newWebhook = ''; + // ---- Webhooks ------ + $scope._newWebhook = {url:''}; + $scope.$watch('user.preferences.webhooks',function(webhooks){ + $scope.hasWebhooks = _.size(webhooks); + }) + $scope.addWebhook = function(url) { + User.user.ops.addWebhook({body:{url:url, id:Shared.uuid()}}); + $scope._newWebhook.url = ''; } - $scope.saveWebhook = function(webhook){ + $scope.saveWebhook = function(id,webhook) { delete webhook._editing; - User.set({'preferences.webhooks':User.user.preferences.webhooks}); + User.user.ops.updateWebhook({params:{id:id}, body:webhook}); } - $scope.deleteWebhook = function($index){ - User.user.preferences.webhooks.splice($index, 1); - User.set({'preferences.webhooks':User.user.preferences.webhooks}); + $scope.deleteWebhook = function(id) { + User.user.ops.deleteWebhook({params:{id:id}}); } } ]); diff --git a/src/models/user.js b/src/models/user.js index afb51e0986..207dc4c8a8 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -278,7 +278,7 @@ var UserSchema = new Schema({ advancedCollapsed: {type: Boolean, 'default': false}, toolbarCollapsed: {type:Boolean, 'default':false}, background: String, - webhooks: {type:Array, 'default': []} + webhooks: {type: Schema.Types.Mixed, 'default': {}} }, profile: { blurb: String, diff --git a/src/routes/apiv2.coffee b/src/routes/apiv2.coffee index d2915fe246..357ebcfb00 100644 --- a/src/routes/apiv2.coffee +++ b/src/routes/apiv2.coffee @@ -373,6 +373,37 @@ module.exports = (swagger, v2) -> ] action: user.inviteFriends + # Webhooks + "/user/webhooks": + spec: + method: 'POST' + description: 'Create a new webhook' + parameters: [ + body '','New Webhook {url:"webhook endpoint (required)", id:"id of webhook (shared.uuid(), optional)", enabled:"whether webhook is enabled (true by default, optional)"}','object' + ] + action: user.addWebhook + + "/user/webhooks/{id}:PUT": + spec: + path: '/user/webhooks/{id}' + method: 'PUT' + description: "Edit a webhook" + parameters: [ + path 'id','The id of the webhook to edit','string' + body '','New Webhook {url:"webhook endpoint (required)", id:"id of webhook (shared.uuid(), optional)", enabled:"whether webhook is enabled (true by default, optional)"}','object' + ] + action: user.updateWebhook + + "/user/webhooks/{id}:DELETE": + spec: + path: '/user/webhooks/{id}' + method: 'DELETE' + description: 'Delete a webhook' + parameters: [ + path 'id','Id of webhook to delete','string' + ] + action: user.deleteWebhook + # --------------------------------- # Groups # --------------------------------- diff --git a/views/options/settings.jade b/views/options/settings.jade index 1badeb390f..ca16dcda47 100644 --- a/views/options/settings.jade +++ b/views/options/settings.jade @@ -155,30 +155,29 @@ script(type='text/ng-template', id='partials/options.settings.api.html') h2 Webhooks table.table.table-striped - thead(ng-if='user.preferences.webhooks[0]') + thead(ng-if='hasWebhooks') tr th Enabled th Webhook URL th tbody - tr(ng-repeat='webhook in user.preferences.webhooks') + tr(ng-repeat="(id,webhook) in user.preferences.webhooks | orderBy:'sort'") td - input(type='checkbox', ng-model='webhook.enabled', ng-change='saveWebhook(webhook)') + input(type='checkbox', ng-model='webhook.enabled', ng-change='saveWebhook(id,webhook)') td - input.form-control(type='url', ng-model='webhook.url', ng-change='webhook._editing=true', ui-keyup="{13:'saveWebhook(webhook)'}") + input.form-control(type='url', ng-model='webhook.url', ng-change='webhook._editing=true', ui-keyup="{13:'saveWebhook(id,webhook)'}") td span.pull-left(ng-show='webhook._editing') * - a.checklist-icons(ng-click='deleteWebhook($index)') + a.checklist-icons(ng-click='deleteWebhook(id)') span.glyphicon.glyphicon-trash(tooltip=env.t('delete')) tr td(colspan=2) - form.form-horizontal(ng-submit='addWebhook(_newWebhook)') + form.form-horizontal(ng-submit='addWebhook(_newWebhook.url)') .form-group.col-sm-10 - input.form-control(type='url', ng-model='_newWebhook', placeholder='Webhook URL') + input.form-control(type='url', ng-model='_newWebhook.url', placeholder='Webhook URL') .col-sm-2 button.btn.btn-sm.btn-primary(type='submit') Add - script(id='partials/options.settings.export.html', type="text/ng-template") .container-fluid .row From e1ff9610d06144cf281a53972435efb7f43656ba Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 17 Nov 2014 12:38:35 -0800 Subject: [PATCH 44/84] fix(paypal): charge "immediately" for on subscription setup --- src/controllers/payments/paypal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/payments/paypal.js b/src/controllers/payments/paypal.js index dff576a2d2..cfc0b8d4d2 100644 --- a/src/controllers/payments/paypal.js +++ b/src/controllers/payments/paypal.js @@ -29,7 +29,7 @@ exports.createBillingAgreement = function(req,res,next){ var billingAgreementAttributes = { "name": billingPlanTitle, "description": billingPlanTitle, - "start_date": moment().add({minutes:1}).format(), + "start_date": moment().add({seconds:5}).format(), "plan": { "id": billingPlanID }, From 10fb931bf253eec96441da97109c46bef12841b7 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 17 Nov 2014 16:08:10 -0800 Subject: [PATCH 45/84] fix(hall): remove global vars --- src/controllers/hall.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/controllers/hall.js b/src/controllers/hall.js index 6fc104b0a1..cd8eba7744 100644 --- a/src/controllers/hall.js +++ b/src/controllers/hall.js @@ -55,12 +55,12 @@ api.updateHero = function(req,res,next) { function(member, cb){ if (!member) return res.json(404, {err: "User not found"}); member.balance = req.body.balance || 0; - newTier = req.body.contributor.level; // tier = level in this context - oldTier = member.contributor && member.contributor.level || 0; + var newTier = req.body.contributor.level; // tier = level in this context + var oldTier = member.contributor && member.contributor.level || 0; if (newTier > oldTier) { member.flags.contributor = true; - gemsPerTier = {1:3, 2:3, 3:3, 4:4, 5:4, 6:4, 7:4, 8:0, 9:0}; // e.g., tier 5 gives 4 gems. Tier 8 = moderator. Tier 9 = staff - tierDiff = newTier - oldTier; // can be 2+ tier increases at once + var gemsPerTier = {1:3, 2:3, 3:3, 4:4, 5:4, 6:4, 7:4, 8:0, 9:0}; // e.g., tier 5 gives 4 gems. Tier 8 = moderator. Tier 9 = staff + var tierDiff = newTier - oldTier; // can be 2+ tier increases at once while (tierDiff) { member.balance += gemsPerTier[newTier] / 4; // balance is in $ tierDiff--; @@ -71,7 +71,7 @@ api.updateHero = function(req,res,next) { member.purchased.ads = req.body.purchased.ads; if (member.contributor.level >= 6) member.items.pets['Dragon-Hydra'] = 5; if (req.body.itemPath && req.body.itemVal - && req.body.itemPath.indexOf('items.')===0 + && req.body.itemPath.indexOf('items.') === 0 && User.schema.paths[req.body.itemPath]) { shared.dotSet(member, req.body.itemPath, req.body.itemVal); // Sanitization at 5c30944 (deemed unnecessary) } From f7f44735900c20b35b383e5e8b27429485969397 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 17 Nov 2014 19:51:03 -0800 Subject: [PATCH 46/84] fix(payments): allow resubscription after unsubscribe --- views/options/settings.jade | 40 +++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/views/options/settings.jade b/views/options/settings.jade index ca16dcda47..53d689d0f3 100644 --- a/views/options/settings.jade +++ b/views/options/settings.jade @@ -222,25 +222,21 @@ script(id='partials/feature-matrix-check.html',type='text/ng-template') label script(id='partials/options.settings.subscription.html',type='text/ng-template') - .well - h2=env.t('individualSub') - div(ng-if='!user.purchased.plan.customerId') - div(ng-include="'partials/options.settings.subscription.perks.html'") - p - small.muted Payment Methods: - .btn.btn-primary(ng-click='showStripe(true)') Card - //a.btn.btn-warning(ng-click='paypalSubscribe()') PayPal - a.btn.btn-warning(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}') PayPal - div(ng-if='user.purchased.plan.customerId') - p.alert.alert-warning(ng-if='user.purchased.plan.dateTerminated') - i.glyphicon.glyphicon-time - |   - =env.t('subCanceled') - | {{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}} - p.lead - =env.t('subscribed') - |   - span.glyphicon.glyphicon-ok - div(ng-include="'partials/options.settings.subscription.perks.html'") - .btn.btn-primary(ng-if=':: !user.purchased.plan.dateTerminated && user.purchased.plan.paymentMethod=="Stripe"', ng-click='showStripeEdit()') Update Card - .btn.btn-sm.btn-danger(ng-if=':: !user.purchased.plan.dateTerminated', ng-click='cancelSubscription()')=env.t('cancelSub') + .well(ng-init="p = user.purchased.plan") + div(ng-if='p.customerId') + p.alert.alert-warning(ng-if='p.dateTerminated') + i.glyphicon.glyphicon-time + | #{env.t('subCanceled')} {{moment(p.dateTerminated).format('MM/DD/YYYY')}} + p.alert.alert-success(ng-if='!p.dateTerminated')=env.t('subscribed') + + h2=env.t('individualSub') + div(ng-include="'partials/options.settings.subscription.perks.html'") + + div(ng-if='!p.customerId || (p.customerId && p.dateTerminated)') + h3(ng-if='(p.customerId && p.dateTerminated)') Resubscribe + a.btn.btn-primary(ng-click='showStripe(true)') Card + //a.btn.btn-warning(ng-click='paypalSubscribe()') PayPal + a.btn.btn-warning(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}') PayPal + div(ng-if='p.customerId') + .btn.btn-primary(ng-if='!p.dateTerminated && p.paymentMethod=="Stripe"', ng-click='showStripeEdit()') Update Card + .btn.btn-sm.btn-danger(ng-if='!p.dateTerminated', ng-click='cancelSubscription()')=env.t('cancelSub') From 487f8afe30ec3f95e29203e9553e2c8cfc57c21c Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 18 Nov 2014 12:00:33 -0800 Subject: [PATCH 47/84] feat(help): add tour to help menu --- views/shared/header/menu.jade | 2 ++ 1 file changed, 2 insertions(+) diff --git a/views/shared/header/menu.jade b/views/shared/header/menu.jade index 59665eb603..8c1994a1b4 100644 --- a/views/shared/header/menu.jade +++ b/views/shared/header/menu.jade @@ -147,6 +147,8 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}') a(target="_blank" href='http://habitrpg.wikia.com/wiki/Contributing_to_HabitRPG')=env.t('contributeToHRPG') li a(target="_blank" href='http://habitrpg.wikia.com/wiki/')=env.t('overview') + li(ng-controller='SettingsCtrl') + a(ng-click='showTour()', popover-placement='right', popover-trigger='mouseenter', popover=env.t('restartTour'))= env.t('showTour') ul.toolbar-subscribe(ng-if='!user.purchased.plan.customerId') li.toolbar-subscribe-button button(ui-sref='options.settings.subscription',popover-trigger='mouseenter',popover-placement='bottom',popover-title=env.t('subscriptions'),popover=env.t('subDescription'),popover-append-to-body='true')=env.t('subscribe') From cb3ebccbbfce492933510992b4e2394e3e57c731 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 18 Nov 2014 15:50:37 -0800 Subject: [PATCH 48/84] feat(invite-friends): obfuscated invite --- public/js/controllers/groupsCtrl.js | 32 ++++++++++++------------- src/controllers/user.js | 27 ++++++++++++++------- src/middleware.js | 8 +++---- src/utils.js | 17 ++++++++++++- views/shared/modals/invite-friends.jade | 8 +++---- 5 files changed, 59 insertions(+), 33 deletions(-) diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index 1d4456d09e..3322087c58 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -73,22 +73,22 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' }); } - var serializeQs = function(obj, prefix){ - var str = []; - for(var p in obj) { - if (obj.hasOwnProperty(p)) { - var k = prefix ? prefix + "[" + p + "]" : p, v = obj[p]; - str.push(typeof v == "object" ? - serializeQs(v, k) : - encodeURIComponent(k) + "=" + encodeURIComponent(v)); - } - } - return str.join("&"); - } - - $scope.inviteLink = function(obj){ - return window.env.BASE_URL + '?' + serializeQs({partyInvite: obj}); - } + //var serializeQs = function(obj, prefix){ + // var str = []; + // for(var p in obj) { + // if (obj.hasOwnProperty(p)) { + // var k = prefix ? prefix + "[" + p + "]" : p, v = obj[p]; + // str.push(typeof v == "object" ? + // serializeQs(v, k) : + // encodeURIComponent(k) + "=" + encodeURIComponent(v)); + // } + // } + // return str.join("&"); + //} + // + //$scope.inviteLink = function(obj){ + // return window.env.BASE_URL + '?' + serializeQs({partyInvite: obj}); + //} $scope.emails = [{name:"",email:""},{name:"",email:""}]; $scope.inviter = User.user.profile.name; $scope.inviteEmails = function(inviter, emails){ diff --git a/src/controllers/user.js b/src/controllers/user.js index 6580971b7a..5b732dabdc 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -411,7 +411,7 @@ api.cast = function(req, res, next) { api.inviteFriends = function(req, res, next) { Group.findOne({type:'party', members:{'$in': [res.locals.user._id]}}).select('_id name').exec(function(err,party){ if (err) return next(err); - var link = nconf.get('BASE_URL') + '?' + qs.stringify({partyInvite:{id:party._id, inviter:res.locals.user._id, name:party.name}}); + var link = nconf.get('BASE_URL')+'?partyInvite='+ utils.encrypt(JSON.stringify({id:party._id, inviter:res.locals.user._id, name:party.name})); _.each(req.body.emails, function(invite){ if (invite.email) { var variables = [ @@ -426,17 +426,28 @@ api.inviteFriends = function(req, res, next) { res.send(200); }) } + api.sessionPartyInvite = function(req,res,next){ - if (req.session.partyInvite) { - var inv = res.locals.user.invitations; - if (!(inv.party && inv.party.id)) { + if (!req.session.partyInvite) return next(); + var inv = res.locals.user.invitations; + if (inv.party && inv.party.id) return next(); // already invited to a party + async.waterfall([ + function(cb){ + Group.findOne({_id:req.session.partyInvite.id, type:'party', members:{$in:[req.session.partyInvite.inviter]}}) + .select('invites members').exec(cb); + }, + function(group, cb){ + if (!group) return cb("Inviter not in party"); inv.party = req.session.partyInvite; - Group.update({_id:req.session.partyInvite.id},{$addToSet:{invites:res.locals.user._id}}); delete req.session.partyInvite; - return res.locals.user.save(next); + if (!~group.invites.indexOf(res.locals.user._id)) + group.invites.push(res.locals.user._id); //$addToSt + group.save(cb); + }, + function(saved, cb){ + res.locals.user.save(cb); } - } - next(); + ], next); } /** diff --git a/src/middleware.js b/src/middleware.js index d7a6902ead..ba5ed01f07 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -12,7 +12,7 @@ var shared = require('habitrpg-shared'); var request = require('request'); var os = require('os'); var moment = require('moment'); -var qs = require('qs'); +var utils = require('./utils'); module.exports.apiThrottle = function(app) { if (nconf.get('NODE_ENV') !== 'production') return; @@ -200,9 +200,9 @@ module.exports.locals = function(req, res, next) { }; // Put query-string party invitations into session to be handled later - var partyInvite = qs.parse(req.query.partyInvite); - if (partyInvite && partyInvite.id) - req.session.partyInvite = partyInvite; + try{ + req.session.partyInvite = JSON.parse(utils.decrypt(req.query.partyInvite)) + } catch(e){} next(); } diff --git a/src/utils.js b/src/utils.js index 0789038b5e..ba8dd47f95 100644 --- a/src/utils.js +++ b/src/utils.js @@ -90,4 +90,19 @@ module.exports.setupConfig = function(){ require('newrelic'); module.exports.ga = require('universal-analytics')(nconf.get('GA_ID')); -}; \ No newline at end of file +}; + +var algorithm = 'aes-256-ctr'; +module.exports.encrypt = function(text){ + var cipher = crypto.createCipher(algorithm,nconf.get('SESSION_SECRET')) + var crypted = cipher.update(text,'utf8','hex') + crypted += cipher.final('hex'); + return crypted; +} + +module.exports.decrypt = function(text){ + var decipher = crypto.createDecipher(algorithm,nconf.get('SESSION_SECRET')) + var dec = decipher.update(text,'hex','utf8') + dec += decipher.final('utf8'); + return dec; +} \ No newline at end of file diff --git a/views/shared/modals/invite-friends.jade b/views/shared/modals/invite-friends.jade index 47c8becda4..02e4ce0c8d 100644 --- a/views/shared/modals/invite-friends.jade +++ b/views/shared/modals/invite-friends.jade @@ -38,10 +38,10 @@ script(type='text/ng-template', id='modals/invite-friends.html') input.form-control(type='text', ng-model='inviter') .col-sm-4 button.btn.btn-primary(type='submit') Invite New User(s) - - hr - p.alert.alert-info Or share this link (copy/paste): - input.form-control(type='text', ng-value='inviteLink({id: party._id, inviter: user._id, name: party.name})') + //- + hr + p.alert.alert-info Or share this link (copy/paste): + input.form-control(type='text', ng-value='inviteLink({id: party._id, inviter: user._id, name: party.name})') .modal-footer button.btn.btn-default(ng-click='$close()') Close From bf8882e8f5e25ece295679edc6b0c1e308cb1bb7 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Fri, 14 Nov 2014 08:14:47 -0600 Subject: [PATCH 49/84] feat(quest): Add owl to purchasable quest eggs --- views/options/inventory/inventory.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/options/inventory/inventory.jade b/views/options/inventory/inventory.jade index 9f5f912e22..8303698cb1 100644 --- a/views/options/inventory/inventory.jade +++ b/views/options/inventory/inventory.jade @@ -106,7 +106,7 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html') | {{::egg.value}} span.Pet_Currency_Gem1x.inline-gems //- buyable quest eggs - each egg,quest in {gryphon:'Gryphon',hedgehog:'Hedgehog',ghost_stag:'Deer',rat:'Rat',octopus:'Octopus',dilatory_derby:'Seahorse',harpy:'Parrot',rooster:'Rooster',spider:'Spider'} + each egg,quest in {gryphon:'Gryphon',hedgehog:'Hedgehog',ghost_stag:'Deer',rat:'Rat',octopus:'Octopus',dilatory_derby:'Seahorse',harpy:'Parrot',rooster:'Rooster',spider:'Spider',owl:'Owl'} div(ng-show='user.achievements.quests.#{quest} > 1') button.customize-option(popover='{{::Content.eggs.#{egg}.notes()}}', popover-title!=env.t("egg", {eggType: "{{::Content.eggs.#{egg}.text()}}"}), popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("eggs", Content.eggs.#{egg})', class='Pet_Egg_#{egg}') p From f922a0337f0f8f94f856d8c8e88d7c880f9cc6d3 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 18 Nov 2014 23:18:53 -0800 Subject: [PATCH 50/84] bailey(owl) --- views/shared/new-stuff.jade | 48 +++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/views/shared/new-stuff.jade b/views/shared/new-stuff.jade index da33650f34..a95361a7bc 100644 --- a/views/shared/new-stuff.jade +++ b/views/shared/new-stuff.jade @@ -8,29 +8,37 @@ table h3.popover-title a(target='_blank', href='https://twitter.com/Mihakuu') Bailey .popover-content - h5 Share Avatar To Social Media, Email Invites, First Mini Quest, And Data Tab table.table.table-striped tr td - h5 Share Avatar To Social Media - p You can now automatically share your avatar and public profile to social media! Jut hover over the picture and click the "Share" button in the right-hand corner. Show off your outfit, your achievements, and your profile picture! Note that your tasks, as always, remain 100% private. - p.small.muted by Lefnire - tr - td - h5 Invite Friends To Party Via Email - p Do you want to invite friends to join your party without inputting their User ID? Now you can send them an email directly from the party page - even if they don't have an account yet! - p.small.muted by Lefnire - tr - td - h5 Mini Quest: The Basi-List! - p Now when someone accepts your party invitation and joins your party, you will be given a Mini Quest: The Basi-List! Battle the Basi-List with your friends for an XP and GP reward. - p.small.muted by Arcosine and Redphoenix - tr - td - h5 Data Tab - p Now you can access the Data Display Tool and Export Data from the toolbar! - p.small.muted by ShilohT - p.small.muted 11/13/2014 + h5 New Pet Quest: The Night-Owl! + p Habiticans are in the dark when a giant Night-Owl blots out the Tavern light! Can you drive it away in time to finish your all-nighter? If so, you may find some cute pet owls in the morning... + p.small.muted by Twitching, Lemoness, and Arcosine + p.small.muted 11/18/2014 + +h5 Share Avatar To Social Media, Email Invites, First Mini Quest, And Data Tab +table.table.table-striped + tr + td + h5 Share Avatar To Social Media + p You can now automatically share your avatar and public profile to social media! Jut hover over the picture and click the "Share" button in the right-hand corner. Show off your outfit, your achievements, and your profile picture! Note that your tasks, as always, remain 100% private. + p.small.muted by Lefnire + tr + td + h5 Invite Friends To Party Via Email + p Do you want to invite friends to join your party without inputting their User ID? Now you can send them an email directly from the party page - even if they don't have an account yet! + p.small.muted by Lefnire + tr + td + h5 Mini Quest: The Basi-List! + p Now when someone accepts your party invitation and joins your party, you will be given a Mini Quest: The Basi-List! Battle the Basi-List with your friends for an XP and GP reward. + p.small.muted by Arcosine and Redphoenix + tr + td + h5 Data Tab + p Now you can access the Data Display Tool and Export Data from the toolbar! + p.small.muted by ShilohT +p.small.muted 11/13/2014 h5 11/12/2014 table.table.table-striped From 2023cf852ed1b11af641607d156d3cb9466d9aaa Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 19 Nov 2014 14:16:41 -0800 Subject: [PATCH 51/84] chore(bower): upgrade angular to 1.3.3. Remove bindonce (incompatible). Note, bindonce="..." attrs still need to be converted to their {{::}} equiv for better performance, but this is functional for now --- bower.json | 11 +++++------ public/js/app.js | 2 +- public/manifest.json | 1 - views/shared/tasks/lists.jade | 18 +++++++++--------- views/shared/tasks/task.jade | 24 ++++++++++++------------ 5 files changed, 27 insertions(+), 29 deletions(-) diff --git a/bower.json b/bower.json index 30a785c817..b86ef48756 100644 --- a/bower.json +++ b/bower.json @@ -16,16 +16,15 @@ "dependencies": { "jquery": "~2.1.0", "jquery.cookie": "~1.4.0", - "angular": "1.3.0-beta.11", + "angular": "1.3.3", "angular-ui": "~0.4.0", - "angular-sanitize": "1.3.0-beta.11", - "angular-resource": "1.3.0-beta.11", + "angular-sanitize": "1.3.3", + "angular-resource": "1.3.3", "angular-ui-utils": "~0.1.0", "angular-ui-select2": "git://github.com/angular-ui/ui-select2.git", "angular-bootstrap": "~0.10.0", "angular-ui-router": "git://github.com/HabitRPG/ui-router.git#habitrpg", "angular-loading-bar": "~0.3.0", - "angular-bindonce": "~0.2.1", "bootstrap": "~3.1.0", "bootstrap-growl": "git://github.com/ifightcrime/bootstrap-growl.git#master", "bootstrap-tour": "~0.8.1", @@ -45,10 +44,10 @@ "css-social-buttons": "https://github.com/samcollins/css-social-buttons.git" }, "devDependencies": { - "angular-mocks": "1.3.0-beta.11" + "angular-mocks": "1.3.3" }, "resolutions": { - "angular": "1.3.0-beta.11", + "angular": "1.3.3", "jquery": ">=1.9.0" } } diff --git a/public/js/app.js b/public/js/app.js index a5c04bdf80..cf04bd8373 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -3,7 +3,7 @@ window.habitrpg = angular.module('habitrpg', ['ngResource', 'ngSanitize', 'userServices', 'groupServices', 'memberServices', 'challengeServices', 'authServices', 'notificationServices', 'guideServices', 'authCtrl', - 'ui.bootstrap', 'ui.keypress', 'ui.router', 'chieffancypants.loadingBar', 'At', 'pasvaz.bindonce', 'infinite-scroll', 'ui.select2']) + 'ui.bootstrap', 'ui.keypress', 'ui.router', 'chieffancypants.loadingBar', 'At', 'infinite-scroll', 'ui.select2']) // @see https://github.com/angular-ui/ui-router/issues/110 and https://github.com/HabitRPG/habitrpg/issues/1705 // temporary hack until they have a better solution diff --git a/public/manifest.json b/public/manifest.json index b2a06332a8..ad86285390 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -15,7 +15,6 @@ "bower_components/angular-loading-bar/build/loading-bar.js", "bower_components/Angular-At-Directive/src/at.js", "bower_components/Angular-At-Directive/src/caret.js", - "bower_components/angular-bindonce/bindonce.js", "bower_components/js-emoji/emoji.js", "bower_components/sticky/jquery.sticky.js", "bower_components/ngInfiniteScroll/build/ng-infinite-scroll.min.js", diff --git a/views/shared/tasks/lists.jade b/views/shared/tasks/lists.jade index ebac10c83c..ba1a10bbdc 100644 --- a/views/shared/tasks/lists.jade +++ b/views/shared/tasks/lists.jade @@ -4,11 +4,11 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template") .tasks-lists.container-fluid .row - .col-md-3.col-sm-6(bindonce='lists', ng-repeat='list in lists', bo-class='{"rewards-module": list.type==="reward"}') + .col-md-3.col-sm-6(bindonce='lists', ng-repeat='list in lists', ng-class='::{"rewards-module": list.type==="reward"}') .task-column(class='{{list.type}}s') // Todos export/graph options - span.option-box.pull-right(bo-if='main && list.type=="todo"') + span.option-box.pull-right(ng-if='::main && list.type=="todo"') a.option-action(ng-show='obj.history.todos', ng-click='toggleChart("todos")', tooltip=env.t('progress')) span.glyphicon.glyphicon-signal //a.option-action(ng-href='/v1/users/{{user.id}}/calendar.ics?apiToken={{user.apiToken}}', tooltip='iCal') @@ -20,7 +20,7 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template") h2.task-column_title {{list.header}} // Todo Chart - .todos-chart(bo-if='list.type == "todo"', ng-show='charts.todos') + .todos-chart(ng-if='::list.type == "todo"', ng-show='charts.todos') // Add New form.addtask-form.form-inline.new-task-form(name='new{{list.type}}form', ng-hide='obj._locked || (list.showCompleted && list.type=="todo")', ng-submit='addTask(obj[list.type+"s"],list)') @@ -31,7 +31,7 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template") mixin taskColumnTabs(position) // Habits Tabs - div(bo-if='main && list.type=="habit"', class='tabbable tabs-below') + div(ng-if='::main && list.type=="habit"', class='tabbable tabs-below') ul.nav.nav-tabs li(ng-class='{active: list.view == "all"}') a(ng-click='list.view = "all"')=env.t('all') @@ -40,7 +40,7 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template") li(ng-class='{active: list.view == "greenblue"}') a(ng-click='list.view = "greenblue"')=env.t('greenblue') // Daily Tabs - div(bo-if='main && list.type=="daily"', class='tabbable tabs-below') + div(ng-if='::main && list.type=="daily"', class='tabbable tabs-below') // remaining/completed tabs ul.nav.nav-tabs li(ng-class='{active: list.view == "all"}') @@ -50,7 +50,7 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template") li(ng-class='{active: list.view == "complete"}') a(ng-click='list.view = "complete"')=env.t('grey') // Todo Tabs - div(bo-if='main && list.type=="todo"', bo-class='{"tabbable tabs-below": list.type=="todo"}') + div(ng-if='::main && list.type=="todo"', ng-class='::{"tabbable tabs-below": list.type=="todo"}') // div(ng-show='list.view == "complete" || list.view == "all"') // li.task.reward-item(ng-if='#{canceler ? "user.stats.buffs."+canceler : "user.items.special."+k+">0"}',popover-trigger='mouseenter', popover-placement='top', popover='{{Content.spells.special.#{k}.notes()}}') if position=="bottom" @@ -66,7 +66,7 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template") li(ng-class='{active: list.showCompleted}') a(ng-click='list.showCompleted= true')=env.t('complete') // Rewards Tabs - div(bo-if='main && list.type=="reward"', class='tabbable tabs-below') + div(ng-if='::main && list.type=="reward"', class='tabbable tabs-below') ul.nav.nav-tabs li(ng-class='{active: list.view == "all"}') a(ng-click='list.view = "all"')=env.t('all') @@ -92,7 +92,7 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template") span.reward-cost {{item.value}} span.shop_gold // main content - span(bo-class='{"shop_{{item.key}} shop-sprite item-img": true}').reward-img + span(ng-class='::{"shop_{{item.key}} shop-sprite item-img": true}').reward-img p.task-text {{item.text()}} // Events @@ -140,7 +140,7 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template") br // Ads - div(bo-if='main && !user.purchased.ads && !user.purchased.plan.customerId && list.type!="reward"') + div(ng-if='::main && !user.purchased.ads && !user.purchased.plan.customerId && list.type!="reward"') span.pull-right a(ui-sref='options.settings.subscription', popover=env.t('removeAds'), popover-trigger='mouseenter') span.glyphicon.glyphicon-remove diff --git a/views/shared/tasks/task.jade b/views/shared/tasks/task.jade index 4b00804cc1..c4ac19e7db 100644 --- a/views/shared/tasks/task.jade +++ b/views/shared/tasks/task.jade @@ -1,4 +1,4 @@ -li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s"]', class='task {{Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)', ng-class='{"cast-target":spell && (list.type != "reward")}', popover-trigger='mouseenter', data-popover-html="{{task.notes | markdown}}", data-popover-placement="top", ng-show='shouldShow(task, list, user.preferences)') +li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s"]', class='task {{Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)', ng-class='{"cast-target":spell && (list.type != "reward")}', popover-trigger='mouseenter', data-popover-html="{{task.notes | markdown}}", data-popover-placement="top", ng-show='shouldShow(task, list, user.preferences)') // right-hand side control buttons .task-meta-controls @@ -60,7 +60,7 @@ li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s .task-controls.task-primary(ng-if='!task._editing') // Habits - span(bo-if='task.type=="habit"') + span(ng-if='::task.type=="habit"') // score() is overridden in challengesCtrl to do nothing a.task-action-btn(ng-if='task.up', ng-click='score(task,"up")') + a.task-action-btn(ng-if='task.down', ng-click='score(task,"down")') - @@ -72,7 +72,7 @@ li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s span.shop_gold // Daily & Todos - span.task-checker.action-yesno(bo-if='task.type=="daily" || task.type=="todo"') + span.task-checker.action-yesno(ng-if='::task.type=="daily" || task.type=="todo"') input.visuallyhidden.focusable(ng-if='$state.includes("tasks")', id='box-{{obj._id}}_{{task.id}}', type='checkbox', ng-model='task.completed', ng-change='task.type=="todo" && pushTask(task,$index,"bottom"); changeCheck(task)') input.visuallyhidden.focusable(ng-if='!$state.includes("tasks")', id='box-{{obj._id}}_{{task.id}}', type='checkbox') label(for='box-{{obj._id}}_{{task.id}}') @@ -163,17 +163,17 @@ li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s label(for='{{obj._id}}_{{task.id}}-option-minus') // if Daily, calendar - fieldset(bo-if='task.type=="daily"', class="option-group") + fieldset(ng-if='::task.type=="daily"', class="option-group") legend.option-title=env.t('repeat') .task-controls.tile-group.repeat-days(bindonce) // note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding - button.task-action-btn.tile(ng-class='{active: task.repeat.su}', type='button', ng-click='task.challenge.id || (task.repeat.su = !task.repeat.su)', bo-text='moment.weekdaysMin(0)') - button.task-action-btn.tile(ng-class='{active: task.repeat.m}', type='button', ng-click='task.challenge.id || (task.repeat.m = !task.repeat.m)', bo-text='moment.weekdaysMin(1)') - button.task-action-btn.tile(ng-class='{active: task.repeat.t}', type='button', ng-click='task.challenge.id || (task.repeat.t = !task.repeat.t)', bo-text='moment.weekdaysMin(2)') - button.task-action-btn.tile(ng-class='{active: task.repeat.w}', type='button', ng-click='task.challenge.id || (task.repeat.w = !task.repeat.w)', bo-text='moment.weekdaysMin(3)') - button.task-action-btn.tile(ng-class='{active: task.repeat.th}', type='button', ng-click='task.challenge.id || (task.repeat.th = !task.repeat.th)', bo-text='moment.weekdaysMin(4)') - button.task-action-btn.tile(ng-class='{active: task.repeat.f}', type='button', ng-click='task.challenge.id || (task.repeat.f= !task.repeat.f)', bo-text='moment.weekdaysMin(5)') - button.task-action-btn.tile(ng-class='{active: task.repeat.s}', type='button', ng-click='task.challenge.id || (task.repeat.s = !task.repeat.s)', bo-text='moment.weekdaysMin(6)') + button.task-action-btn.tile(ng-class='{active: task.repeat.su}', type='button', ng-click='task.challenge.id || (task.repeat.su = !task.repeat.su)') {{::moment.weekdaysMin(0)}} + button.task-action-btn.tile(ng-class='{active: task.repeat.m}', type='button', ng-click='task.challenge.id || (task.repeat.m = !task.repeat.m)') {{::moment.weekdaysMin(1)}} + button.task-action-btn.tile(ng-class='{active: task.repeat.t}', type='button', ng-click='task.challenge.id || (task.repeat.t = !task.repeat.t)') {{::moment.weekdaysMin(2)}} + button.task-action-btn.tile(ng-class='{active: task.repeat.w}', type='button', ng-click='task.challenge.id || (task.repeat.w = !task.repeat.w)') {{::moment.weekdaysMin(3)}} + button.task-action-btn.tile(ng-class='{active: task.repeat.th}', type='button', ng-click='task.challenge.id || (task.repeat.th = !task.repeat.th)') {{::moment.weekdaysMin(4)}} + button.task-action-btn.tile(ng-class='{active: task.repeat.f}', type='button', ng-click='task.challenge.id || (task.repeat.f= !task.repeat.f)') {{::moment.weekdaysMin(5)}} + button.task-action-btn.tile(ng-class='{active: task.repeat.s}', type='button', ng-click='task.challenge.id || (task.repeat.s = !task.repeat.s)') {{::moment.weekdaysMin(6)}} // if Reward, pricing fieldset.option-group.option-short(ng-if='task.type=="reward" && !task.challenge.id') @@ -195,7 +195,7 @@ li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s markdown(ng-model='tag.name') // Advanced Options - span(bo-if='task.type!="reward"') + span(ng-if='::task.type!="reward"') p.option-title.mega(ng-click='task._advanced = !task._advanced', tooltip=env.t('expandCollapse'))=env.t('advancedOptions') fieldset.option-group.advanced-option(ng-class="{visuallyhidden: task._advanced}") legend.option-title From 5f3eef770fea0e8e963f3b7448cd505d700318b4 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 19 Nov 2014 14:29:54 -0800 Subject: [PATCH 52/84] chore(modules): add angular-filter --- bower.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bower.json b/bower.json index b86ef48756..63dc3d3eb2 100644 --- a/bower.json +++ b/bower.json @@ -41,7 +41,8 @@ "pnotify": "~1.3.1", "jquery-ui": "~1.10.3", "hello": "~1.3.1", - "css-social-buttons": "https://github.com/samcollins/css-social-buttons.git" + "css-social-buttons": "https://github.com/samcollins/css-social-buttons.git", + "angular-filter": "~0.5.1" }, "devDependencies": { "angular-mocks": "1.3.3" From ebebcea34b9948fa211fccaa112485b99334721b Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 19 Nov 2014 15:25:58 -0800 Subject: [PATCH 53/84] fix(webhooks): sort webhooks by created, use angular-filter for sorting object --- public/js/app.js | 2 +- public/manifest.json | 1 + views/options/settings.jade | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index cf04bd8373..9f30a29483 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -3,7 +3,7 @@ window.habitrpg = angular.module('habitrpg', ['ngResource', 'ngSanitize', 'userServices', 'groupServices', 'memberServices', 'challengeServices', 'authServices', 'notificationServices', 'guideServices', 'authCtrl', - 'ui.bootstrap', 'ui.keypress', 'ui.router', 'chieffancypants.loadingBar', 'At', 'infinite-scroll', 'ui.select2']) + 'ui.bootstrap', 'ui.keypress', 'ui.router', 'chieffancypants.loadingBar', 'At', 'infinite-scroll', 'ui.select2', 'angular.filter']) // @see https://github.com/angular-ui/ui-router/issues/110 and https://github.com/HabitRPG/habitrpg/issues/1705 // temporary hack until they have a better solution diff --git a/public/manifest.json b/public/manifest.json index ad86285390..2e8a5b4ee1 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -21,6 +21,7 @@ "bower_components/select2/select2.js", "bower_components/angular-ui-select2/src/select2.js", "bower_components/hello/dist/hello.all.min.js", + "bower_components/angular-filter/dist/angular-filter.min.js", "bower_components/angular-bootstrap/ui-bootstrap.js", "bower_components/angular-bootstrap/ui-bootstrap-tpls.js", diff --git a/views/options/settings.jade b/views/options/settings.jade index 53d689d0f3..99c94f4dc2 100644 --- a/views/options/settings.jade +++ b/views/options/settings.jade @@ -161,14 +161,14 @@ script(type='text/ng-template', id='partials/options.settings.api.html') th Webhook URL th tbody - tr(ng-repeat="(id,webhook) in user.preferences.webhooks | orderBy:'sort'") + tr(ng-repeat="webhook in user.preferences.webhooks | toArray:true | orderBy:'sort'") td - input(type='checkbox', ng-model='webhook.enabled', ng-change='saveWebhook(id,webhook)') + input(type='checkbox', ng-model='webhook.enabled', ng-change='saveWebhook(webhook.$key,webhook)') td - input.form-control(type='url', ng-model='webhook.url', ng-change='webhook._editing=true', ui-keyup="{13:'saveWebhook(id,webhook)'}") + input.form-control(type='url', ng-model='webhook.url', ng-change='webhook._editing=true', ui-keyup="{13:'saveWebhook(webhook.$key,webhook)'}") td span.pull-left(ng-show='webhook._editing') * - a.checklist-icons(ng-click='deleteWebhook(id)') + a.checklist-icons(ng-click='deleteWebhook(webhook.$key)') span.glyphicon.glyphicon-trash(tooltip=env.t('delete')) tr td(colspan=2) From b36c9c0fb0515af78b67913de71c02cecd0c29ea Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 20 Nov 2014 19:02:46 -0700 Subject: [PATCH 54/84] fix(bootstrap): upgrade angular-ui-bootstrap --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 63dc3d3eb2..7d51137de2 100644 --- a/bower.json +++ b/bower.json @@ -22,7 +22,7 @@ "angular-resource": "1.3.3", "angular-ui-utils": "~0.1.0", "angular-ui-select2": "git://github.com/angular-ui/ui-select2.git", - "angular-bootstrap": "~0.10.0", + "angular-bootstrap": "~0.12.0", "angular-ui-router": "git://github.com/HabitRPG/ui-router.git#habitrpg", "angular-loading-bar": "~0.3.0", "bootstrap": "~3.1.0", From f5415ac6c529f529572e824f2e9ab6515444b484 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 19 Nov 2014 13:53:47 -0800 Subject: [PATCH 55/84] feat(private-messages): start implementing private messaging (includes blocking). Start experimenting with reflists for PMs, instead of arrays --- public/js/app.js | 10 +++++ public/js/controllers/groupsCtrl.js | 11 ++++- src/controllers/groups.js | 11 +---- src/controllers/members.js | 57 ++++++++++++++++++++++++++ src/controllers/user.js | 2 +- src/models/group.js | 2 +- src/models/user.js | 7 ++++ src/routes/apiv2.coffee | 24 ++++++++++- views/options/social/chat-message.jade | 45 +++++++++++--------- views/options/social/group.jade | 3 +- views/options/social/index.jade | 21 ++++++++-- views/options/social/tavern.jade | 3 +- views/shared/header/menu.jade | 9 ++++ views/shared/modals/members.jade | 39 ++++++++++++------ 14 files changed, 189 insertions(+), 55 deletions(-) create mode 100644 src/controllers/members.js diff --git a/public/js/app.js b/public/js/app.js index 9f30a29483..32e03deb57 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -75,6 +75,16 @@ window.habitrpg = angular.module('habitrpg', templateUrl: "partials/options.social.html" }) + .state('options.social.inbox', { + url: "/inbox", + templateUrl: "partials/options.social.inbox.html", + controller: ['$rootScope', function($rootScope){ + // clear "new messages" + //$rootScope.$on('userSynced',function(){ + $rootScope.User.user.ops.update && $rootScope.set({'inbox.newMessages':0}); + }] + }) + .state('options.social.tavern', { url: "/tavern", templateUrl: "partials/options.social.tavern.html", diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index 3322087c58..a57198dafb 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -101,8 +101,8 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' } ]) - .controller("MemberModalCtrl", ['$scope', '$rootScope', 'Members', 'Shared', - function($scope, $rootScope, Members, Shared) { + .controller("MemberModalCtrl", ['$scope', '$rootScope', 'Members', 'Shared', '$http', 'Notification', + function($scope, $rootScope, Members, Shared, $http, Notification) { $scope.timestamp = function(timestamp){ return moment(timestamp).format('MM/DD/YYYY'); } @@ -112,6 +112,13 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' member.petCount = Shared.countPets(null, member.items.pets); $scope.profile = member; }); + $scope.sendPrivateMessage = function(uuid, message){ + $http.post('/api/v2/members/'+uuid+'/message',{message:message}).success(function(){ + Notification.text('Message sent.'); + $rootScope.User.sync(); + $scope.$close(); + }); + } } ]) diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 9f789f20dd..50272cdab2 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -19,7 +19,7 @@ var api = module.exports; ------------------------------------------------------------------------ */ -var partyFields = 'profile preferences stats achievements party backer contributor auth.timestamps items'; +var partyFields = api.partyFields = 'profile preferences stats achievements party backer contributor auth.timestamps items'; var nameFields = 'profile.name'; var challengeFields = '_id name'; var guildPopulate = {path: 'members', select: nameFields, options: {limit: 15} }; @@ -44,15 +44,6 @@ var populateQuery = function(type, q){ return q; } - -api.getMember = function(req, res, next) { - User.findById(req.params.uid).select(partyFields).exec(function(err, user){ - if (err) return next(err); - if (!user) return res.json(400,{err:'User not found'}); - res.json(user); - }) -} - /** * Fetch groups list. This no longer returns party or tavern, as those can be requested indivdually * as /groups/party or /groups/tavern diff --git a/src/controllers/members.js b/src/controllers/members.js new file mode 100644 index 0000000000..96bd66b421 --- /dev/null +++ b/src/controllers/members.js @@ -0,0 +1,57 @@ +var User = require('mongoose').model('User'); +var groups = require('../models/group'); +var partyFields = require('./groups').partyFields +var api = module.exports; +var async = require('async'); +var _ = require('lodash'); +var shared = require('habitrpg-shared'); + +api.getMember = function(req, res, next) { + User.findById(req.params.uuid).select(partyFields).exec(function(err, user){ + if (err) return next(err); + if (!user) return res.json(400,{err:'User not found'}); + res.json(user); + }) +} + +api.sendPrivateMessage = function(req,res,next){ + async.waterfall([ + function(cb){ + User.findById(req.params.uuid, cb); + }, + function(member, cb){ + if (!member) return cb({code:404, err: 'User not found'}); + if (~member.inbox.blocks.indexOf(res.locals.user._id) || member.inbox.optOut) { + return cb({code:401, err: "Can't send message to this user."}); + } + + var message = groups.chatDefaults(req.body.message, res.locals.user); + shared.refPush(member.inbox.messages, message); + member.inbox.newMessages++; + member._v++; + member.markModified('inbox.messages'); + + var message = groups.chatDefaults(req.body.message, member); + shared.refPush(res.locals.user.inbox.messages, _.defaults({sent:true},message)); + res.locals.user.markModified('inbox.messages'); + + member.save(cb); + }, + function(a,b,cb){ + res.locals.user.save(cb); + } + ], function(err){ + if (err) return err.code ? res.json(err.code,{err:err.err}) : err; + res.send(200); + }) + +} + +api.block = function(req,res,next){ + var b = res.locals.user.inbox; + if (~b.blocks.indexOf(req.params.id)){ + b.blocks.push(req.params.id) + res.locals.user.save(); + } + res.send(200); +} diff --git a/src/controllers/user.js b/src/controllers/user.js index 5b732dabdc..a1c71908a0 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -216,7 +216,7 @@ api.getUser = function(req, res, next) { * FIXME - one-by-one we want to widdle down this list, instead replacing each needed set path with API operations */ acceptablePUTPaths = _.reduce(require('./../models/user').schema.paths, function(m,v,leaf){ - var found= _.find('achievements filters flags invitations lastCron party preferences profile stats'.split(' '), function(root){ + var found= _.find('achievements filters flags invitations lastCron party preferences profile stats inbox'.split(' '), function(root){ return leaf.indexOf(root) == 0; }); if (found) m[leaf]=true; diff --git a/src/models/group.js b/src/models/group.js index 2f8f6674b9..fd2518b8ed 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -93,7 +93,7 @@ GroupSchema.methods.toJSON = function(){ return doc; } -var chatDefaults = function(message,user){ +var chatDefaults = module.exports.chatDefaults = function(message,user){ var message = { id: shared.uuid(), text: message, diff --git a/src/models/user.js b/src/models/user.js index 207dc4c8a8..2be3e9fd95 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -326,6 +326,13 @@ var UserSchema = new Schema({ challenges: [{type: 'String', ref:'Challenge'}], + inbox: { + newMessages: {type:Number, 'default':0}, + blocks: {type:Array, 'default':[]}, + messages: {type:Schema.Types.Mixed, 'default':{}}, //reflist + optOut: {type:Boolean, 'default':false} + }, + habits: {type:[TaskSchemas.HabitSchema]}, dailys: {type:[TaskSchemas.DailySchema]}, todos: {type:[TaskSchemas.TodoSchema]}, diff --git a/src/routes/apiv2.coffee b/src/routes/apiv2.coffee index 357ebcfb00..fe7db360cc 100644 --- a/src/routes/apiv2.coffee +++ b/src/routes/apiv2.coffee @@ -10,6 +10,7 @@ $ mocha test/user.mocha.coffee user = require("../controllers/user") groups = require("../controllers/groups") +members = require("../controllers/members") auth = require("../controllers/auth") hall = require("../controllers/hall") challenges = require("../controllers/challenges") @@ -579,9 +580,28 @@ module.exports = (swagger, v2) -> # --------------------------------- # Members # --------------------------------- - "/members/{uid}": + "/members/{uuid}": spec:{} - action: groups.getMember + action: members.getMember + "/members/{uuid}/message": + spec: + method: 'POST' + description: 'Send a private message to a member' + parameters: [ + path 'uuid', 'The UUID of the member to message', 'string' + body '', '{message: "The private message to send"}', 'object' + ] + middleware: [auth.auth] + action: members.sendPrivateMessage + "/members/{uuid}/block": + spec: + method: 'POST' + description: 'Block a member from sending private messages' + parameters: [ + path 'uuid', 'The UUID of the member to message', 'string' + ] + middleware: [auth.auth] + action: members.block # --------------------------------- # Hall of Heroes / Patrons diff --git a/views/options/social/chat-message.jade b/views/options/social/chat-message.jade index d7507908a5..7264525901 100644 --- a/views/options/social/chat-message.jade +++ b/views/options/social/chat-message.jade @@ -1,19 +1,26 @@ -li.chat-message(ng-repeat='message in group.chat track by message.id', ng-class=':: {highlight: isUserMentioned(user,message) || message.uuid=="system", "own-message": user._id == message.uuid}') - .scrollable-message - span(ng-if='::message.user') - a.label.label-default.chat-message.hidden-label - span {{::message.user}}  - span(ng-class='userAdminGlyphiconStyleFromLevel(message.contributor.level)') - // this invisible username label is here to push the message text far enough right that the visible label can be floated to this point without covering up any of the text - markdown(ng-model='::message.text') - | - - span.muted.time(from-now='::message.timestamp') - span - a.label.label-default(ng-show='countExists(message.likes)', ng-class='{"label-success":message.likes[user._id]}', ng-click='likeChatMessage(group,message)') +{{countExists(message.likes)}} - a.chat-plus-one.muted(ng-show='!countExists(message.likes)', ng-click='likeChatMessage(group,message)') +1 - |   - a(ng-if=':: user.contributor.admin || message.uuid == user.id', ng-click='deleteChatMessage(group, message)') - span.glyphicon.glyphicon-trash(tooltip=env.t('delete')) - a.label.label-default.chat-message(ng-if=':: message.user', class='float-label', ng-class='userLevelStyleFromLevel(message.contributor.level, message.backer.npc, style)', ng-click='clickMember(message.uuid, true)') - span(tooltip='{{::contribText(message.contributor, message.backer)}}') {{::message.user}}  - span(ng-class='userAdminGlyphiconStyleFromLevel(message.contributor.level)') +mixin chatMessages(inbox) + ul.list-unstyled.tavern-chat + - var ngRepeat = inbox ? 'message in user.inbox.messages | toArray:true | orderBy:"sort":true' : 'message in group.chat track by message.id' + li.chat-message(ng-repeat=ngRepeat, ng-class=':: {highlight: isUserMentioned(user,message) || message.uuid=="system", "own-message": user._id == message.uuid}', style='{{::message.sent ? "opacity:0.5" : ""}}') + .scrollable-message + span(ng-if='::message.user') + a.label.label-default.chat-message.hidden-label + span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent') + span {{::message.user}}  + span(ng-class='userAdminGlyphiconStyleFromLevel(message.contributor.level)') + // this invisible username label is here to push the message text far enough right that the visible label can be floated to this point without covering up any of the text + markdown(ng-model='::message.text') + | - + span.muted.time(from-now='::message.timestamp') + unless type='inbox' + span + a.label.label-default(ng-show='countExists(message.likes)', ng-class='{"label-success":message.likes[user._id]}', ng-click='likeChatMessage(group,message)') +{{countExists(message.likes)}} + a.chat-plus-one.muted(ng-show='!countExists(message.likes)', ng-click='likeChatMessage(group, message)') +1 + |   + a(ng-click='#{inbox? "user.ops.deletePM({params:{id:message.$key}})" : "deleteChatMessage(group, message)"}', ng-if='#{inbox ? "true" : ":: user.contributor.admin || message.uuid == user.id"}') + span.glyphicon.glyphicon-trash(tooltip=env.t('delete')) + span.float-label + a.label.label-default.chat-message(ng-if=':: message.user', ng-class='::userLevelStyleFromLevel(message.contributor.level, message.backer.npc, style)', ng-click='clickMember(message.uuid, true)') + span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent') + span(tooltip='{{::contribText(message.contributor, message.backer)}}') {{::message.user}}  + span(ng-class='::userAdminGlyphiconStyleFromLevel(message.contributor.level)') diff --git a/views/options/social/group.jade b/views/options/social/group.jade index 00ed6a49f5..ec36aea8b4 100644 --- a/views/options/social/group.jade +++ b/views/options/social/group.jade @@ -114,5 +114,4 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter h3=env.t('chat') include ./chat-box - ul.list-unstyled.tavern-chat - include ./chat-message + +chatMessages() diff --git a/views/options/social/index.jade b/views/options/social/index.jade index b632b1c6d9..2582e62c11 100644 --- a/views/options/social/index.jade +++ b/views/options/social/index.jade @@ -1,9 +1,20 @@ // FIXME note, due to https://github.com/angular-ui/bootstrap/issues/783 we can't use nested angular-bootstrap tabs // Subscribe to that ticket & change this when they fix -include ./challenges.jade -include ./hall.jade -include ./boss.jade +include ./challenges +include ./hall +include ./boss +include ./chat-message + +script(type='text/ng-template', id='partials/options.social.inbox.html') + .container-fluid + .row + .col-md-12 + +chatMessages('inbox') + .checkbox + label + input(type='checkbox', ng-model='user.inbox.optOut', ng-change='set({"inbox.optOut": user.inbox.optOut?true: false})') + span.hint(popover-trigger='mouseenter', popover-placement='right', popover="Don't like private messages? Click to completely opt out") Opt Out script(type='text/ng-template', id='partials/options.social.tavern.html') include ./tavern @@ -78,6 +89,10 @@ script(type='text/ng-template', id='partials/options.social.guilds.html') script(type='text/ng-template', id='partials/options.social.html') ul.options-menu + li(ng-class="{ active: $state.includes('options.social.inbox') }") + a(ui-sref='options.social.inbox') + | Inbox  + span.badge.badge-danger(ng-if='user.inbox.newMessages') {{user.inbox.newMessages}} li(ng-class="{ active: $state.includes('options.social.tavern') }") a(ui-sref='options.social.tavern') =env.t('tavern') diff --git a/views/options/social/tavern.jade b/views/options/social/tavern.jade index 72826de4fc..753dd02c45 100644 --- a/views/options/social/tavern.jade +++ b/views/options/social/tavern.jade @@ -160,5 +160,4 @@ |   =env.t('communityGuidelinesRead2') - ul.list-unstyled.tavern-chat - include ./chat-message + +chatMessages() diff --git a/views/shared/header/menu.jade b/views/shared/header/menu.jade index 8c1994a1b4..df5386be07 100644 --- a/views/shared/header/menu.jade +++ b/views/shared/header/menu.jade @@ -24,6 +24,10 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}') li a(ui-sref='options.profile.profile')=env.t('profile') ul.toolbar-submenu + li + a(ui-sref='options.social.inbox') + | Inbox  + span.badge.badge-danger(ng-if='user.inbox.newMessages') {{user.inbox.newMessages}} li a(ui-sref='options.social.tavern')=env.t('tavern') li @@ -88,10 +92,15 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}') li.toolbar-button-dropdown a(ui-sref='options.social.tavern') span=env.t('social') + span.badge.badge-danger(ng-if='user.inbox.newMessages') {{user.inbox.newMessages}} a(ng-click='expandMenu("social")', ng-class='{active: _expandedMenu == "social"}') span ☰ div(ng-if='_expandedMenu == "social"') ul.toolbar-submenu(ng-click='expandMenu(null)') + li + a(ui-sref='options.social.inbox') + | Inbox  + span.badge.badge-danger(ng-if='user.inbox.newMessages') {{user.inbox.newMessages}} li a(ui-sref='options.social.tavern')=env.t('tavern') li diff --git a/views/shared/modals/members.jade b/views/shared/modals/members.jade index eb7e843bb9..40c190c45f 100644 --- a/views/shared/modals/members.jade +++ b/views/shared/modals/members.jade @@ -1,28 +1,32 @@ -script(type='text/ng-template', id='modals/member.html') - .modal-header(bindonce='profile') +script(type='text/ng-template', id='modals/member.html') + .modal-header h4 - span {{profile.profile.name}} - span(ng-if='profile.contributor.level') - {{contribText(profile.contributor, profile.backer)}} - .modal-body(bindonce='profile') + span {{::profile.profile.name}} + span(ng-if='profile.contributor.level') - {{::contribText(profile.contributor, profile.backer)}} + ul.pull-right.list-inline(ng-if='::user') + li.glyphicon.glyphicon-plus(ng-show='user.inbox.blocks | contains:profile._id', tooltip='Un-block', ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='bottom', style='cursor:pointer') + li.glyphicon.glyphicon-remove(ng-hide='user.inbox.blocks | contains:profile._id', tooltip='Block', ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='bottom', style='cursor:pointer') + li.glyphicon.glyphicon-envelope(tooltip='Send private message', ng-click="openModal('private-message',{controller:'MemberModalCtrl'})", tooltip-placement='bottom', style='cursor:pointer') + .modal-body .container-fluid .row .col-md-6 - img(ng-show='profile.profile.imageUrl', ng-src='{{profile.profile.imageUrl}}') - markdown(ng-show='profile.profile.blurb', ng-model='profile.profile.blurb') - ul.muted.list-unstyled(ng-if='profile.auth.timestamps') + img(ng-show='::profile.profile.imageUrl', ng-src='{{::profile.profile.imageUrl}}') + markdown(ng-show='::profile.profile.blurb', ng-model='::profile.profile.blurb') + ul.muted.list-unstyled(ng-if='::profile.auth.timestamps') li {{profile._id}} - li(ng-show='profile.auth.timestamps.created') + li(ng-show='::profile.auth.timestamps.created') |  =env.t('memberSince') |  - | {{timestamp(profile.auth.timestamps.created)}} - - li(ng-show='profile.auth.timestamps.loggedin') + | {{::timestamp(profile.auth.timestamps.created)}} - + li(ng-show='::profile.auth.timestamps.loggedin') |  =env.t('lastLoggedIn') |  - | {{timestamp(profile.auth.timestamps.loggedin)}} - + | {{::timestamp(profile.auth.timestamps.loggedin)}} - h3=env.t('stats') - .label.label-info {{ {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer")}[profile.stats.class] }} + .label.label-info {{:: {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer")}[profile.stats.class] }} include ../profiles/stats .col-md-6 +herobox() @@ -30,3 +34,12 @@ script(type='text/ng-template', id='modals/member.html') include ../profiles/achievements .modal-footer button.btn.btn-default(ng-click='$close()')=env.t('ok') + +script(type='text/ng-template', id='modals/private-message.html') + .modal-header + h4 Private message to {{profile.profile.name}} + .modal-body + textarea.form-control(type='text',ng-model='_message') + .modal-footer + button.btn.btn-primary(ng-click='sendPrivateMessage(profile._id, _message)') Send + button.btn.btn-default(ng-click='$close()')=env.t('cancel') \ No newline at end of file From 697694c69de4f53557ebc98603f91bedcb6ac5d0 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 20 Nov 2014 20:14:52 -0700 Subject: [PATCH 56/84] refactor(private-messages): cleanup block-user --- src/controllers/members.js | 11 +---------- src/routes/apiv2.coffee | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/controllers/members.js b/src/controllers/members.js index 96bd66b421..6789868530 100644 --- a/src/controllers/members.js +++ b/src/controllers/members.js @@ -45,13 +45,4 @@ api.sendPrivateMessage = function(req,res,next){ res.send(200); }) -} - -api.block = function(req,res,next){ - var b = res.locals.user.inbox; - if (~b.blocks.indexOf(req.params.id)){ - b.blocks.push(req.params.id) - res.locals.user.save(); - } - res.send(200); -} +} \ No newline at end of file diff --git a/src/routes/apiv2.coffee b/src/routes/apiv2.coffee index fe7db360cc..00191a52b1 100644 --- a/src/routes/apiv2.coffee +++ b/src/routes/apiv2.coffee @@ -601,7 +601,7 @@ module.exports = (swagger, v2) -> path 'uuid', 'The UUID of the member to message', 'string' ] middleware: [auth.auth] - action: members.block + action: user.blockUser # --------------------------------- # Hall of Heroes / Patrons From 31fdc866c3682308447ff4ee1a4fb6d18e75f806 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 21 Nov 2014 08:15:19 -0700 Subject: [PATCH 57/84] fix(members): add a callback so members modal doesn't popup until after member is fetched --- public/js/controllers/groupsCtrl.js | 5 +++-- public/js/services/memberServices.js | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index a57198dafb..f77f08b017 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -45,8 +45,9 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' } else { // We need the member information up top here, but then we pass it down to the modal controller // down below. Better way of handling this? - Members.selectMember(uid); - $rootScope.openModal('member', {controller:'MemberModalCtrl'}); + Members.selectMember(uid, function(){ + $rootScope.openModal('member', {controller:'MemberModalCtrl'}); + }); } } diff --git a/public/js/services/memberServices.js b/public/js/services/memberServices.js index c24eaa5981..bb3a4c5ac5 100644 --- a/public/js/services/memberServices.js +++ b/public/js/services/memberServices.js @@ -57,7 +57,7 @@ angular.module('memberServices', ['ngResource', 'sharedServices']). * either gets them or fetches if not available * @param uid */ - selectMember: function(uid) { + selectMember: function(uid, cb) { var self = this; // Fetch from cache if we can. For guild members, only their uname will have been fetched on initial load, // check if they have full fields (eg, check profile.items and an item inside @@ -67,11 +67,13 @@ angular.module('memberServices', ['ngResource', 'sharedServices']). if (members[uid] && members[uid].items && members[uid].items.weapon) { Shared.wrap(members[uid],false); self.selectedMember = members[uid]; + cb(); } else { Member.get({uid: uid}, function(member){ self.populate(member); // lazy load for later Shared.wrap(member,false); self.selectedMember = members[member._id]; + cb(); }); } } From a429dce5dd962b0d88526e42fbd6f22b2d824e7b Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 21 Nov 2014 08:25:35 -0700 Subject: [PATCH 58/84] fix(private-messages): blocker can't message blockee --- src/controllers/members.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controllers/members.js b/src/controllers/members.js index 6789868530..9d3017960f 100644 --- a/src/controllers/members.js +++ b/src/controllers/members.js @@ -21,7 +21,9 @@ api.sendPrivateMessage = function(req,res,next){ }, function(member, cb){ if (!member) return cb({code:404, err: 'User not found'}); - if (~member.inbox.blocks.indexOf(res.locals.user._id) || member.inbox.optOut) { + if (~member.inbox.blocks.indexOf(res.locals.user._id) // can't send message if that user blocked me + || ~res.locals.user.inbox.blocks.indexOf(member._id) // or if I blocked them + || member.inbox.optOut) { // or if they've opted out of messaging return cb({code:401, err: "Can't send message to this user."}); } From 1539c35d296d2f94f675d374b53e9bd0155b30fc Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 21 Nov 2014 08:34:08 -0700 Subject: [PATCH 59/84] fix(private-messages): clear newMessages both when leaving and entering inbox --- public/js/app.js | 7 +------ public/js/controllers/rootCtrl.js | 4 ++++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index 32e03deb57..adbcd0f6f8 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -77,12 +77,7 @@ window.habitrpg = angular.module('habitrpg', .state('options.social.inbox', { url: "/inbox", - templateUrl: "partials/options.social.inbox.html", - controller: ['$rootScope', function($rootScope){ - // clear "new messages" - //$rootScope.$on('userSynced',function(){ - $rootScope.User.user.ops.update && $rootScope.set({'inbox.newMessages':0}); - }] + templateUrl: "partials/options.social.inbox.html" }) .state('options.social.tavern', { diff --git a/public/js/controllers/rootCtrl.js b/public/js/controllers/rootCtrl.js index 1ef4aaeebc..942c976fa4 100644 --- a/public/js/controllers/rootCtrl.js +++ b/public/js/controllers/rootCtrl.js @@ -16,6 +16,10 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams){ if (!!fromState.name) window.ga && ga('send', 'pageview', {page: '/#/'+toState.name}); + // clear inbox when entering or exiting inbox tab + if (fromState.name=='options.social.inbox' || toState.name=='options.social.inbox') { + User.user.ops.update && User.set({'inbox.newMessages':0}); + } }); $rootScope.User = User; From 743df194c7b7d93d0fd9dcda37942020f1fcce0f Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 21 Nov 2014 08:42:24 -0700 Subject: [PATCH 60/84] feat(private-messages): add a "clear all" button --- views/options/social/index.jade | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/views/options/social/index.jade b/views/options/social/index.jade index 2582e62c11..5866743dd1 100644 --- a/views/options/social/index.jade +++ b/views/options/social/index.jade @@ -11,10 +11,12 @@ script(type='text/ng-template', id='partials/options.social.inbox.html') .row .col-md-12 +chatMessages('inbox') - .checkbox - label - input(type='checkbox', ng-model='user.inbox.optOut', ng-change='set({"inbox.optOut": user.inbox.optOut?true: false})') - span.hint(popover-trigger='mouseenter', popover-placement='right', popover="Don't like private messages? Click to completely opt out") Opt Out + .form-inline + a.btn.btn-xs.btn-danger(popover='Delete all messages', popover-trigger='mouseenter', ng-click='user.ops.clearPMs({})', popover-placement='right') Clear All + .checkbox + label + input(type='checkbox', ng-model='user.inbox.optOut', ng-change='set({"inbox.optOut": user.inbox.optOut?true: false})') + span.hint(popover-trigger='mouseenter', popover-placement='right', popover="Don't like private messages? Click to completely opt out") Opt Out script(type='text/ng-template', id='partials/options.social.tavern.html') include ./tavern From ecef457d2b9c092e3eee9576449a177eeda47c6d Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 21 Nov 2014 10:40:57 -0700 Subject: [PATCH 61/84] fix(private-messages): glyphicon-ban-circle (@Alys @crookedneighbor) --- views/shared/modals/members.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/shared/modals/members.jade b/views/shared/modals/members.jade index 40c190c45f..33c1052dc0 100644 --- a/views/shared/modals/members.jade +++ b/views/shared/modals/members.jade @@ -5,7 +5,7 @@ script(type='text/ng-template', id='modals/member.html') span(ng-if='profile.contributor.level') - {{::contribText(profile.contributor, profile.backer)}} ul.pull-right.list-inline(ng-if='::user') li.glyphicon.glyphicon-plus(ng-show='user.inbox.blocks | contains:profile._id', tooltip='Un-block', ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='bottom', style='cursor:pointer') - li.glyphicon.glyphicon-remove(ng-hide='user.inbox.blocks | contains:profile._id', tooltip='Block', ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='bottom', style='cursor:pointer') + li.glyphicon.glyphicon-ban-circle(ng-hide='user.inbox.blocks | contains:profile._id', tooltip='Block', ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='bottom', style='cursor:pointer') li.glyphicon.glyphicon-envelope(tooltip='Send private message', ng-click="openModal('private-message',{controller:'MemberModalCtrl'})", tooltip-placement='bottom', style='cursor:pointer') .modal-body .container-fluid From f16d9d609e9380e2883fda2ba4d23b3bd215f5fa Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 23 Nov 2014 20:28:36 -0600 Subject: [PATCH 62/84] Added quick reply button (doesn't load in the correct parameters for sending a msg) --- public/js/controllers/groupsCtrl.js | 3 +++ views/options/social/chat-message.jade | 2 ++ 2 files changed, 5 insertions(+) diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index f77f08b017..99b6c3e248 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -99,6 +99,9 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' }); } + $scope.quickReply = function(uuid) { + $rootScope.openModal('private-message',{controller:'MemberModalCtrl', scope:$scope}) + } } ]) diff --git a/views/options/social/chat-message.jade b/views/options/social/chat-message.jade index 7264525901..6e86485d92 100644 --- a/views/options/social/chat-message.jade +++ b/views/options/social/chat-message.jade @@ -19,6 +19,8 @@ mixin chatMessages(inbox) |   a(ng-click='#{inbox? "user.ops.deletePM({params:{id:message.$key}})" : "deleteChatMessage(group, message)"}', ng-if='#{inbox ? "true" : ":: user.contributor.admin || message.uuid == user.id"}') span.glyphicon.glyphicon-trash(tooltip=env.t('delete')) + a(ng-click="quickReply(message.uuid)") + span.glyphicon.glyphicon-share-alt(tooltip=env.t('pm-reply')) span.float-label a.label.label-default.chat-message(ng-if=':: message.user', ng-class='::userLevelStyleFromLevel(message.contributor.level, message.backer.npc, style)', ng-click='clickMember(message.uuid, true)') span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent') From 8c1adb7fa93d0dfe9241b039b18fadd2dc41434c Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 23 Nov 2014 20:36:00 -0600 Subject: [PATCH 63/84] Adjusted quick reply function so it actually passes in the uuid correctly --- public/js/controllers/groupsCtrl.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index 99b6c3e248..8c24071ff3 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -99,8 +99,10 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' }); } - $scope.quickReply = function(uuid) { - $rootScope.openModal('private-message',{controller:'MemberModalCtrl', scope:$scope}) + $scope.quickReply = function(uid) { + Members.selectMember(uid, function(){ + $rootScope.openModal('private-message',{controller:'MemberModalCtrl'}) + }); } } ]) From bf586cbc17fbed1e4bba86cc2ece988fa692bb32 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 23 Nov 2014 21:16:32 -0600 Subject: [PATCH 64/84] Added env.t declarations for translations --- views/options/social/index.jade | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/views/options/social/index.jade b/views/options/social/index.jade index 5866743dd1..b0fb4053c8 100644 --- a/views/options/social/index.jade +++ b/views/options/social/index.jade @@ -12,11 +12,11 @@ script(type='text/ng-template', id='partials/options.social.inbox.html') .col-md-12 +chatMessages('inbox') .form-inline - a.btn.btn-xs.btn-danger(popover='Delete all messages', popover-trigger='mouseenter', ng-click='user.ops.clearPMs({})', popover-placement='right') Clear All + a.btn.btn-xs.btn-danger(popover=env.t('clearAllPopover'), popover-trigger='mouseenter', ng-click='user.ops.clearPMs({})', popover-placement='right')=env.t('clearAll') .checkbox label input(type='checkbox', ng-model='user.inbox.optOut', ng-change='set({"inbox.optOut": user.inbox.optOut?true: false})') - span.hint(popover-trigger='mouseenter', popover-placement='right', popover="Don't like private messages? Click to completely opt out") Opt Out + span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('optOutPopover'))=env.t('optOut') script(type='text/ng-template', id='partials/options.social.tavern.html') include ./tavern From d8c30fc8851bea7c1293af514bfdda24d82569a9 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 23 Nov 2014 21:19:40 -0600 Subject: [PATCH 65/84] Added env.t for member modal to allow for translations --- views/shared/modals/members.jade | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/views/shared/modals/members.jade b/views/shared/modals/members.jade index 33c1052dc0..7ccdefa80f 100644 --- a/views/shared/modals/members.jade +++ b/views/shared/modals/members.jade @@ -4,9 +4,9 @@ script(type='text/ng-template', id='modals/member.html') span {{::profile.profile.name}} span(ng-if='profile.contributor.level') - {{::contribText(profile.contributor, profile.backer)}} ul.pull-right.list-inline(ng-if='::user') - li.glyphicon.glyphicon-plus(ng-show='user.inbox.blocks | contains:profile._id', tooltip='Un-block', ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='bottom', style='cursor:pointer') - li.glyphicon.glyphicon-ban-circle(ng-hide='user.inbox.blocks | contains:profile._id', tooltip='Block', ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='bottom', style='cursor:pointer') - li.glyphicon.glyphicon-envelope(tooltip='Send private message', ng-click="openModal('private-message',{controller:'MemberModalCtrl'})", tooltip-placement='bottom', style='cursor:pointer') + li.glyphicon.glyphicon-plus(ng-show='user.inbox.blocks | contains:profile._id', tooltip=env.t('unblock'), ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='bottom', style='cursor:pointer') + li.glyphicon.glyphicon-ban-circle(ng-hide='user.inbox.blocks | contains:profile._id', tooltip=env.t('block'), ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='bottom', style='cursor:pointer') + li.glyphicon.glyphicon-envelope(tooltip=env.t('sendPM'), ng-click="openModal('private-message',{controller:'MemberModalCtrl'})", tooltip-placement='bottom', style='cursor:pointer') .modal-body .container-fluid .row @@ -42,4 +42,4 @@ script(type='text/ng-template', id='modals/private-message.html') textarea.form-control(type='text',ng-model='_message') .modal-footer button.btn.btn-primary(ng-click='sendPrivateMessage(profile._id, _message)') Send - button.btn.btn-default(ng-click='$close()')=env.t('cancel') \ No newline at end of file + button.btn.btn-default(ng-click='$close()')=env.t('cancel') From 7ea8ff70ae96ed7c6f19d7630d115d91167cc0e1 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 23 Nov 2014 21:33:14 -0600 Subject: [PATCH 66/84] Added additional env.t declarations for pm modal --- views/shared/modals/members.jade | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/views/shared/modals/members.jade b/views/shared/modals/members.jade index 7ccdefa80f..2bfb09d15c 100644 --- a/views/shared/modals/members.jade +++ b/views/shared/modals/members.jade @@ -37,9 +37,9 @@ script(type='text/ng-template', id='modals/member.html') script(type='text/ng-template', id='modals/private-message.html') .modal-header - h4 Private message to {{profile.profile.name}} + h4=env.t('pmHeading', {name: "{{profile.profile.name}}"}) .modal-body textarea.form-control(type='text',ng-model='_message') .modal-footer - button.btn.btn-primary(ng-click='sendPrivateMessage(profile._id, _message)') Send + button.btn.btn-primary(ng-click='sendPrivateMessage(profile._id, _message)')=env.t("send") button.btn.btn-default(ng-click='$close()')=env.t('cancel') From aafc1131ad31001c4b9ebee436ed4c50fee680a7 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 23 Nov 2014 21:39:14 -0600 Subject: [PATCH 67/84] Made message sent notification a translatable string --- public/js/controllers/groupsCtrl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index f77f08b017..f5c0e9e846 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -115,7 +115,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' }); $scope.sendPrivateMessage = function(uuid, message){ $http.post('/api/v2/members/'+uuid+'/message',{message:message}).success(function(){ - Notification.text('Message sent.'); + Notification.text(window.env.t('messageSentAlert')); $rootScope.User.sync(); $scope.$close(); }); From 36ffd8a8b2ab02436c04f9b90742ef7ae2fdd7b2 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 24 Nov 2014 18:14:04 -0700 Subject: [PATCH 68/84] fix(quick-reply): only show quick-reply in inbox (cc @crookedneighbor) --- views/options/social/chat-message.jade | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/views/options/social/chat-message.jade b/views/options/social/chat-message.jade index 6e86485d92..57b8488a72 100644 --- a/views/options/social/chat-message.jade +++ b/views/options/social/chat-message.jade @@ -19,8 +19,9 @@ mixin chatMessages(inbox) |   a(ng-click='#{inbox? "user.ops.deletePM({params:{id:message.$key}})" : "deleteChatMessage(group, message)"}', ng-if='#{inbox ? "true" : ":: user.contributor.admin || message.uuid == user.id"}') span.glyphicon.glyphicon-trash(tooltip=env.t('delete')) - a(ng-click="quickReply(message.uuid)") - span.glyphicon.glyphicon-share-alt(tooltip=env.t('pm-reply')) + if inbox + a(ng-click="quickReply(message.uuid)") + span.glyphicon.glyphicon-share-alt(tooltip=env.t('pm-reply')) span.float-label a.label.label-default.chat-message(ng-if=':: message.user', ng-class='::userLevelStyleFromLevel(message.contributor.level, message.backer.npc, style)', ng-click='clickMember(message.uuid, true)') span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent') From 478cba9a5a3d578a1a3f0f19d765a08dd1314b37 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 24 Nov 2014 18:59:27 -0700 Subject: [PATCH 69/84] feat(members): add options: $modal.open({size, windowClass}). Use those to make avatar modal wider, and "100% window height" (see code, it's kinda hacky) --- public/css/avatar.styl | 10 +++++++--- public/js/controllers/groupsCtrl.js | 6 +++--- public/js/controllers/rootCtrl.js | 5 +++-- views/shared/modals/members.jade | 8 +++++--- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/public/css/avatar.styl b/public/css/avatar.styl index 20f03c3a0f..f5132b070f 100644 --- a/public/css/avatar.styl +++ b/public/css/avatar.styl @@ -9,9 +9,13 @@ Seeing as the sprites might change drastically in the future re: pets and whatnot, this is just temporary. ---------------------------------------------------- */ -.profile-modal-header - width:560px - margin: 0px auto +// see http://stackoverflow.com/questions/24166568/set-bootstrap-modal-body-height-by-percentage +.profile-modal + .modal-dialog,.modal-content + height: 96% + .modal-body + max-height: calc(100% - 150px) //100% = dialog height, 150px = header + footer + overflow-y: scroll .herobox // Base styles diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index 3969fd5d78..42ec3a421e 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -46,7 +46,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' // We need the member information up top here, but then we pass it down to the modal controller // down below. Better way of handling this? Members.selectMember(uid, function(){ - $rootScope.openModal('member', {controller:'MemberModalCtrl'}); + $rootScope.openModal('member', {controller:'MemberModalCtrl', windowClass:'profile-modal', size:'lg'}); }); } } @@ -101,8 +101,8 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' $scope.quickReply = function(uid) { Members.selectMember(uid, function(){ - $rootScope.openModal('private-message',{controller:'MemberModalCtrl'}) - }); + $rootScope.openModal('private-message',{controller:'MemberModalCtrl'}); + }); } } ]) diff --git a/public/js/controllers/rootCtrl.js b/public/js/controllers/rootCtrl.js index 942c976fa4..4e27c81b52 100644 --- a/public/js/controllers/rootCtrl.js +++ b/public/js/controllers/rootCtrl.js @@ -117,8 +117,9 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ controller: options.controller, // optional scope: options.scope, // optional keyboard: (options.keyboard === undefined ? true : options.keyboard), // optional - backdrop: (options.backdrop === undefined ? true : options.backdrop) // optional - + backdrop: (options.backdrop === undefined ? true : options.backdrop), // optional + size: options.size, // optional, 'sm' or 'lg' + windowClass: options.windowClass // optional }); } diff --git a/views/shared/modals/members.jade b/views/shared/modals/members.jade index 2bfb09d15c..d8a8d349b0 100644 --- a/views/shared/modals/members.jade +++ b/views/shared/modals/members.jade @@ -29,9 +29,11 @@ script(type='text/ng-template', id='modals/member.html') .label.label-info {{:: {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer")}[profile.stats.class] }} include ../profiles/stats .col-md-6 - +herobox() - h3=env.t('achievements') - include ../profiles/achievements + .row + +herobox() + .row + h3=env.t('achievements') + include ../profiles/achievements .modal-footer button.btn.btn-default(ng-click='$close()')=env.t('ok') From 5f654c4dad658199ab02e06637a8df7b6c2d68f8 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 24 Nov 2014 19:11:49 -0700 Subject: [PATCH 70/84] fix(private-messages): move PM buttons to bottom-left of modal --- views/shared/modals/members.jade | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/views/shared/modals/members.jade b/views/shared/modals/members.jade index d8a8d349b0..55181df160 100644 --- a/views/shared/modals/members.jade +++ b/views/shared/modals/members.jade @@ -3,10 +3,6 @@ script(type='text/ng-template', id='modals/member.html') h4 span {{::profile.profile.name}} span(ng-if='profile.contributor.level') - {{::contribText(profile.contributor, profile.backer)}} - ul.pull-right.list-inline(ng-if='::user') - li.glyphicon.glyphicon-plus(ng-show='user.inbox.blocks | contains:profile._id', tooltip=env.t('unblock'), ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='bottom', style='cursor:pointer') - li.glyphicon.glyphicon-ban-circle(ng-hide='user.inbox.blocks | contains:profile._id', tooltip=env.t('block'), ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='bottom', style='cursor:pointer') - li.glyphicon.glyphicon-envelope(tooltip=env.t('sendPM'), ng-click="openModal('private-message',{controller:'MemberModalCtrl'})", tooltip-placement='bottom', style='cursor:pointer') .modal-body .container-fluid .row @@ -35,7 +31,14 @@ script(type='text/ng-template', id='modals/member.html') h3=env.t('achievements') include ../profiles/achievements .modal-footer - button.btn.btn-default(ng-click='$close()')=env.t('ok') + .btn-group.pull-left(ng-if='::user') + button.btn.btn-md.btn-default(ng-show='user.inbox.blocks | contains:profile._id', tooltip=env.t('unblock'), ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='right') + span.glyphicon.glyphicon-plus + button.btn.btn-md.btn-default(ng-hide='user.inbox.blocks | contains:profile._id', tooltip=env.t('block'), ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='right') + span.glyphicon.glyphicon-ban-circle + button.btn.btn-md.btn-default(tooltip=env.t('sendPM'), ng-click="openModal('private-message',{controller:'MemberModalCtrl'})", tooltip-placement='right') + span.glyphicon.glyphicon-envelope + button.btn.btn-default(ng-click='$close()')=env.t('close') script(type='text/ng-template', id='modals/private-message.html') .modal-header From 0f7f30aa121076b669331a1b117495d73402168e Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 24 Nov 2014 19:25:31 -0700 Subject: [PATCH 71/84] bailey: private messages & november mysteries --- migrations/mysteryitems.js | 2 +- views/shared/new-stuff.jade | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/migrations/mysteryitems.js b/migrations/mysteryitems.js index 1531f8fb0f..b222047bda 100644 --- a/migrations/mysteryitems.js +++ b/migrations/mysteryitems.js @@ -2,7 +2,7 @@ var _id = ''; var update = { $push: { 'purchased.plan.mysteryItems':{ - $each:['back_mystery_201410','armor_mystery_201410'] + $each:['head_mystery_201411','weapon_mystery_201411'] } } }; diff --git a/views/shared/new-stuff.jade b/views/shared/new-stuff.jade index a95361a7bc..c4979df357 100644 --- a/views/shared/new-stuff.jade +++ b/views/shared/new-stuff.jade @@ -8,13 +8,27 @@ table h3.popover-title a(target='_blank', href='https://twitter.com/Mihakuu') Bailey .popover-content + h5 November Item Set Revealed, Private Messaging Version 1.0 table.table.table-striped tr td - h5 New Pet Quest: The Night-Owl! - p Habiticans are in the dark when a giant Night-Owl blots out the Tavern light! Can you drive it away in time to finish your all-nighter? If so, you may find some cute pet owls in the morning... - p.small.muted by Twitching, Lemoness, and Arcosine - p.small.muted 11/18/2014 + h5 November Item Set Revealed + p The November Subscriber Item has been revealed: the Feast and Fun Set! All November subscribers will receive the Pitchfork of Feasting and the Steel Helm of Sporting. You still have five days to subscribe and receive the item set! Thank you so much for your support - we really do rely on you to keep HabitRPG free to use and running smoothly. + p.small.muted by Lemoness + tr + td + h5 Private Messaging Version 1.0 + p We're excited to announce a new feature: Private Messaging! Now you can send someone a PM by clicking the envelope icon in the bottom-left of their profile window . You can check your messages under Social > Inbox! This is a very rudimentary feature so far, only containing the ability to send messages, block people, and opt out. To read about some of the planned features for the future and make suggestions, check out this Trello card! + p.small.muted by Lefnire + p.small.muted 11/25/2014 + +h5 11/18/2014 +table.table.table-striped + tr + td + h5 New Pet Quest: The Night-Owl! + p Habiticans are in the dark when a giant Night-Owl blots out the Tavern light! Can you drive it away in time to finish your all-nighter? If so, you may find some cute pet owls in the morning... + p.small.muted by Twitching, Lemoness, and Arcosine h5 Share Avatar To Social Media, Email Invites, First Mini Quest, And Data Tab table.table.table-striped From c5c096013e79f94879742177d2ee55c6c13bf114 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 24 Nov 2014 19:47:19 -0700 Subject: [PATCH 72/84] refactor(private): use cpus-1 on prod, max(2) cpus on beta (and incidentally localhost) --- src/server.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/server.js b/src/server.js index 98e9fc0040..aed0b524b6 100644 --- a/src/server.js +++ b/src/server.js @@ -10,9 +10,8 @@ var isDev = nconf.get('NODE_ENV') === 'development'; if (cluster.isMaster && (isDev || isProd)) { // Fork workers. - _.times(_.min([require('os').cpus().length,2]), function(){ - cluster.fork(); - }); + var cpus = require('os').cpus(); + _.times(nconf.get("HEROKU_PROD") ? (cpus.length-1 || 1) : (_.min([cpus.length,2])), cluster.fork); cluster.on('disconnect', function(worker, code, signal) { var w = cluster.fork(); // replace the dead worker From 7de70fb5b1782da1a46b9105ba30110674659b61 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 24 Nov 2014 21:28:47 -0700 Subject: [PATCH 73/84] fix(chat): fix missing like button --- views/options/social/chat-message.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/options/social/chat-message.jade b/views/options/social/chat-message.jade index 57b8488a72..84d4105425 100644 --- a/views/options/social/chat-message.jade +++ b/views/options/social/chat-message.jade @@ -12,7 +12,7 @@ mixin chatMessages(inbox) markdown(ng-model='::message.text') | - span.muted.time(from-now='::message.timestamp') - unless type='inbox' + unless inbox span a.label.label-default(ng-show='countExists(message.likes)', ng-class='{"label-success":message.likes[user._id]}', ng-click='likeChatMessage(group,message)') +{{countExists(message.likes)}} a.chat-plus-one.muted(ng-show='!countExists(message.likes)', ng-click='likeChatMessage(group, message)') +1 From f0b3da295cfdf6f7d1d74c51f955e6f1163d5390 Mon Sep 17 00:00:00 2001 From: Alice Harris Date: Tue, 25 Nov 2014 14:50:01 +1000 Subject: [PATCH 74/84] re-enable +1 button, add spaces before delete button, add "quick reply" to Tavern/guilds/party chat --- views/options/social/chat-message.jade | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/views/options/social/chat-message.jade b/views/options/social/chat-message.jade index 57b8488a72..27ecf66f70 100644 --- a/views/options/social/chat-message.jade +++ b/views/options/social/chat-message.jade @@ -12,16 +12,19 @@ mixin chatMessages(inbox) markdown(ng-model='::message.text') | - span.muted.time(from-now='::message.timestamp') - unless type='inbox' + unless inbox span a.label.label-default(ng-show='countExists(message.likes)', ng-class='{"label-success":message.likes[user._id]}', ng-click='likeChatMessage(group,message)') +{{countExists(message.likes)}} a.chat-plus-one.muted(ng-show='!countExists(message.likes)', ng-click='likeChatMessage(group, message)') +1 - |   - a(ng-click='#{inbox? "user.ops.deletePM({params:{id:message.$key}})" : "deleteChatMessage(group, message)"}', ng-if='#{inbox ? "true" : ":: user.contributor.admin || message.uuid == user.id"}') - span.glyphicon.glyphicon-trash(tooltip=env.t('delete')) + |     + a(ng-click="quickReply(message.uuid)") + span.glyphicon.glyphicon-envelope(tooltip=env.t('sendPM')) if inbox a(ng-click="quickReply(message.uuid)") span.glyphicon.glyphicon-share-alt(tooltip=env.t('pm-reply')) + |         + a(ng-click='#{inbox? "user.ops.deletePM({params:{id:message.$key}})" : "deleteChatMessage(group, message)"}', ng-if='#{inbox ? "true" : ":: user.contributor.admin || message.uuid == user.id"}') + span.glyphicon.glyphicon-trash(tooltip=env.t('delete')) span.float-label a.label.label-default.chat-message(ng-if=':: message.user', ng-class='::userLevelStyleFromLevel(message.contributor.level, message.backer.npc, style)', ng-click='clickMember(message.uuid, true)') span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent') From 89b1242e5310ff24fd975cf72a19c5da07922486 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 25 Nov 2014 10:22:36 -0700 Subject: [PATCH 75/84] fix(emoji): move back to main jsemoji repo, fixes #4265 --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 7d51137de2..69c91e3766 100644 --- a/bower.json +++ b/bower.json @@ -33,7 +33,7 @@ "github-buttons": "git://github.com/mdo/github-buttons.git", "marked": "~0.2.9", "Angular-At-Directive": "git://github.com/snicker/Angular-At-Directive#master", - "js-emoji": "git://github.com/snicker/js-emoji#master", + "js-emoji": "https://github.com/iamcal/js-emoji.git", "sticky": "*", "swagger-ui": "git://github.com/wordnik/swagger-ui.git", "ngInfiniteScroll": "1.0.0", From d24ece973bf23161a9c21be14b41c4c6132c0147 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 25 Nov 2014 15:26:42 -0700 Subject: [PATCH 76/84] fix(food): popover-placement='top' in foodtray, fixes #4278 --- views/options/inventory/stable.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/options/inventory/stable.jade b/views/options/inventory/stable.jade index c8c7c6fd08..6fa5b75aa8 100644 --- a/views/options/inventory/stable.jade +++ b/views/options/inventory/stable.jade @@ -97,7 +97,7 @@ script(type='text/ng-template', id='partials/options.inventory.pets.html') li.customize-menu menu.pets-menu(label=env.t('food')) div(ng-repeat='(food,points) in ownedItems(user.items.food)') - button.customize-option(popover-append-to-body='true', popover='{{:: Content.food[food].notes()}}', popover-title='{{:: Content.food[food].text()}}', popover-trigger='mouseenter', popover-placement='left', ng-click='chooseFood(food)', class='Pet_Food_{{::food}}') + button.customize-option(popover-append-to-body='true', popover='{{:: Content.food[food].notes()}}', popover-title='{{:: Content.food[food].text()}}', popover-trigger='mouseenter', popover-placement='top', ng-click='chooseFood(food)', class='Pet_Food_{{::food}}') .badge.badge-info.stack-count {{points}} // Remove this once we have images in p {{:: Content.food[food].text()}} From afbd2199e8608458d746f3a564cc231ce126b7e3 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Wed, 26 Nov 2014 11:51:44 +0100 Subject: [PATCH 77/84] fix(datepicker): fixes #4273 --- views/shared/tasks/task.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/shared/tasks/task.jade b/views/shared/tasks/task.jade index c4ac19e7db..aacd405278 100644 --- a/views/shared/tasks/task.jade +++ b/views/shared/tasks/task.jade @@ -185,7 +185,7 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s // if Todos, the due date fieldset.option-group(ng-if='task.type=="todo" && !task.challenge.id') legend.option-title=env.t('dueDate') - input.option-content.datepicker(type='text', datepicker-popup='MM/dd/yyyy', ng-model='task.date') + input.option-content.datepicker(type='text', datepicker-popup='MM/dd/yyyy', ng-model='task.date', is-open='datepickerOpened', ng-click='datepickerOpened = true') // Tags fieldset.option-group(ng-if='!$state.includes("options.social.challenges")') From 43fa461a67ddea2125def4a0e517af081eab90ac Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Wed, 26 Nov 2014 12:19:33 +0100 Subject: [PATCH 78/84] fix(costume): fixes #4274 --- views/options/inventory/inventory.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/options/inventory/inventory.jade b/views/options/inventory/inventory.jade index 8303698cb1..46d1cf7f9a 100644 --- a/views/options/inventory/inventory.jade +++ b/views/options/inventory/inventory.jade @@ -11,7 +11,7 @@ script(type='text/ng-template', id='partials/options.inventory.equipment.html') h3.equipment-title.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('costumeText'))=env.t('costume') .checkbox.equipment-title label - input(type="checkbox", ng-model="user.preferences.costume", ng-click='set({"preferences.costume":!user.preferences.costume})') + input(type="checkbox", ng-model="user.preferences.costume", ng-click='set({"preferences.costume":user.preferences.costume})') |  =env.t('useCostume') li.customize-menu(ng-if='user.preferences.costume') From c0a3fc887a125341ebaea8085152a101cc5c895b Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Wed, 26 Nov 2014 12:26:41 +0100 Subject: [PATCH 79/84] fix(costume): better fix for #4274 --- views/options/inventory/inventory.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/options/inventory/inventory.jade b/views/options/inventory/inventory.jade index 46d1cf7f9a..e4e8574b89 100644 --- a/views/options/inventory/inventory.jade +++ b/views/options/inventory/inventory.jade @@ -11,7 +11,7 @@ script(type='text/ng-template', id='partials/options.inventory.equipment.html') h3.equipment-title.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('costumeText'))=env.t('costume') .checkbox.equipment-title label - input(type="checkbox", ng-model="user.preferences.costume", ng-click='set({"preferences.costume":user.preferences.costume})') + input(type="checkbox", ng-model="user.preferences.costume", ng-change='set({"preferences.costume":user.preferences.costume ? true : false})') |  =env.t('useCostume') li.customize-menu(ng-if='user.preferences.costume') From f5d0b7957306370f18c42c1b37c296a7d03423de Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Wed, 26 Nov 2014 12:38:02 +0100 Subject: [PATCH 80/84] fix(loading bar): possible fix --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 69c91e3766..5819739e29 100644 --- a/bower.json +++ b/bower.json @@ -24,7 +24,7 @@ "angular-ui-select2": "git://github.com/angular-ui/ui-select2.git", "angular-bootstrap": "~0.12.0", "angular-ui-router": "git://github.com/HabitRPG/ui-router.git#habitrpg", - "angular-loading-bar": "~0.3.0", + "angular-loading-bar": "~0.6.0", "bootstrap": "~3.1.0", "bootstrap-growl": "git://github.com/ifightcrime/bootstrap-growl.git#master", "bootstrap-tour": "~0.8.1", From 0278caca76dce9d2891d732a8eb3fe8fb50b1e11 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 26 Nov 2014 10:14:01 -0700 Subject: [PATCH 81/84] fix(#4282): fix $parse errors in avatar customize screens --- views/options/profile.jade | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/views/options/profile.jade b/views/options/profile.jade index 5920b9df78..18c2044aef 100644 --- a/views/options/profile.jade +++ b/views/options/profile.jade @@ -9,14 +9,14 @@ mixin gemCost(cost) mixin customizeProfile(mobile) mixin buyPref(path,colors,title,status) - li.customize-menu(ng-if='#{status=="disabled" ? "user.purchased."+path+"."+colors.join(" || user.purchased."+path+".") : true}', class=~["limited","seasonal"].indexOf(status) ? "well limited-edition" : "") + li.customize-menu(ng-if='#{status=="disabled" ? "user.purchased."+path+"[\""+colors.join("\"] || user.purchased."+path+"[\"")+"\"]" : true}', class=~["limited","seasonal"].indexOf(status) ? "well limited-edition" : "") if ~['limited','seasonal'].indexOf(status) .label.label-info.pull-right.hint(popover=limited, popover-title=env.t(status+'Edition'), popover-placement='right', popover-trigger='mouseenter')=env.t(status+'Edition') menu(label=env.t(title)) +gemCost(2) - button.btn.btn-xs(ng-hide='#{status=="disabled"} || user.purchased.#{path}.#{colors.join(" && user.purchased."+path+".")}', ng-click='unlock("#{path}.#{colors.join(","+path+".")}")')!= env.t('unlockSet',{cost:5}) + ' ' + button.btn.btn-xs(ng-hide='#{status=="disabled"} || #{"user.purchased."+path+"[\""+colors.join("\"] || user.purchased."+path+"[\"")+"\"]"}', ng-click='unlock("#{path}.#{colors.join(","+path+".")}")')!= env.t('unlockSet',{cost:5}) + ' ' each color in colors - button.customize-option(type='button', class='#{path=="skin" ? "skin_"+color : "customize-option hair hair_bangs_1_"+color}', ng-class='{locked: !user.purchased.#{path}.#{color}}', ng-if='#{status!="disabled"} || user.purchased.#{path}.#{color}', ng-click='unlock("#{path}.#{color}")') + button.customize-option(type='button', class='#{path=="skin" ? "skin_"+color : "customize-option hair hair_bangs_1_"+color}', ng-class='{locked: !user.purchased.#{path}["#{color}"]}', ng-if='#{status!="disabled"} || user.purchased.#{path}["#{color}"]', ng-click='unlock("#{path}.#{color}")') div(class=mobile ? 'padding' : 'container-fluid row') .col-md-4 @@ -77,9 +77,9 @@ mixin customizeProfile(mobile) // Purchasable hairstyles menu(label=env.t('hairSet1')) +gemCost(2) - button.btn.btn-xs(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('unlockSet',{cost:5}) + ' ' + button.btn.btn-xs(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('unlockSet',{cost:5}) + ' ' each num in [2,4,5,6,7,8] - button(class='hair_base_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.base.#{num}}', ng-click='unlock("hair.base.#{num}")') + button(class='hair_base_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.base["#{num}"]}', ng-click='unlock("hair.base.#{num}")') // Flower li.customize-menu @@ -92,19 +92,19 @@ mixin customizeProfile(mobile) h5=env.t('bodyFacialHair') +gemCost(2) - button.btn.btn-xs(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('unlockSet',{cost:5}) + ' ' + button.btn.btn-xs(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('unlockSet',{cost:5}) + ' ' // Beard menu(label=env.t('beard')) button(class='head_0 customize-option', type='button', ng-click='set({"preferences.hair.beard":0})') each num in [1,2,3] - button(class='hair_beard_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.beard.#{num}}', ng-click='unlock("hair.beard.#{num}")') + button(class='hair_beard_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.beard["#{num}"]}', ng-click='unlock("hair.beard.#{num}")') // Mustache menu(label=env.t('mustache')) button(class='head_0 customize-option', type='button', ng-click='set({"preferences.hair.mustache":0})') 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(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}")') .col-md-4 h3(class=mobile?'item item-divider':'')=env.t('bodySkin') From 1e5b7e6f2d0026bf0849a33de2f5eb701605a274 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 26 Nov 2014 10:47:18 -0700 Subject: [PATCH 82/84] fix(#4282): refactor for DRY unlock/hide paths --- views/options/profile.jade | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/views/options/profile.jade b/views/options/profile.jade index 18c2044aef..ec620e135a 100644 --- a/views/options/profile.jade +++ b/views/options/profile.jade @@ -4,17 +4,19 @@ mixin gemCost(cost) = ' ' + env.t('locked') block +-var showPath = function(path, items, joiner) { return path+'["'+items.join('"] '+joiner+' '+path+'["')+'"]'; } +-var unlockPath = function(path, items) { return 'unlock("'+path+'.'+items.join(','+path+'.')+'")'; } // Make it a mixin so we can call it from mobile mixin customizeProfile(mobile) mixin buyPref(path,colors,title,status) - li.customize-menu(ng-if='#{status=="disabled" ? "user.purchased."+path+"[\""+colors.join("\"] || user.purchased."+path+"[\"")+"\"]" : true}', class=~["limited","seasonal"].indexOf(status) ? "well limited-edition" : "") + li.customize-menu(ng-if='#{status=="disabled" ? showPath("user.purchased."+path, colors, "||") : true}', class=~["limited","seasonal"].indexOf(status) ? "well limited-edition" : "") if ~['limited','seasonal'].indexOf(status) .label.label-info.pull-right.hint(popover=limited, popover-title=env.t(status+'Edition'), popover-placement='right', popover-trigger='mouseenter')=env.t(status+'Edition') menu(label=env.t(title)) +gemCost(2) - button.btn.btn-xs(ng-hide='#{status=="disabled"} || #{"user.purchased."+path+"[\""+colors.join("\"] || user.purchased."+path+"[\"")+"\"]"}', ng-click='unlock("#{path}.#{colors.join(","+path+".")}")')!= env.t('unlockSet',{cost:5}) + ' ' + button.btn.btn-xs(ng-hide='#{status=="disabled"} || #{showPath("user.purchased."+path, colors, "||")}', ng-click='#{unlockPath(path, colors)}')!= env.t('unlockSet',{cost:5}) + ' ' each color in colors button.customize-option(type='button', class='#{path=="skin" ? "skin_"+color : "customize-option hair hair_bangs_1_"+color}', ng-class='{locked: !user.purchased.#{path}["#{color}"]}', ng-if='#{status!="disabled"} || user.purchased.#{path}["#{color}"]', ng-click='unlock("#{path}.#{color}")') @@ -35,9 +37,10 @@ mixin customizeProfile(mobile) button.customize-option(class='{{user.preferences.size}}_shirt_'+shirt, type='button', ng-click='set({"preferences.shirt":"'+shirt+'"})') menu(label=env.t('specialShirts')) + - var specialShirts = ['convict', 'cross', 'fire', 'horizon', 'ocean', 'purple', 'rainbow', 'redblue', 'thunder', 'tropical', 'zombie'] +gemCost(2) - button.btn.btn-xs(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('unlockSet',{cost:5}) + ' ' - each shirt in ['convict', 'cross', 'fire', 'horizon', 'ocean', 'purple', 'rainbow', 'redblue', 'thunder', 'tropical', 'zombie'] + button.btn.btn-xs(ng-hide='#{showPath("user.purchased.shirt", specialShirts, "&&")}', ng-click='#{unlockPath("shirt",specialShirts)}')!= env.t('unlockSet',{cost:5}) + ' ' + each shirt in specialShirts button.customize-option(type='button', class='{{user.preferences.size}}_shirt_'+shirt, ng-class='{locked: !user.purchased.shirt.'+shirt+'}', ng-click='unlock("shirt.'+shirt+'")') @@ -76,9 +79,10 @@ mixin customizeProfile(mobile) // Purchasable hairstyles menu(label=env.t('hairSet1')) + - var colors = [2,4,5,6,7,8] +gemCost(2) - button.btn.btn-xs(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('unlockSet',{cost:5}) + ' ' - each num in [2,4,5,6,7,8] + button.btn.btn-xs(ng-hide='#{showPath("user.purchased.hair", colors, "&&")}', ng-click='#{unlockPath("hair.base",colors)}')!= env.t('unlockSet',{cost:5}) + ' ' + each num in colors button(class='hair_base_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.base["#{num}"]}', ng-click='unlock("hair.base.#{num}")') // Flower @@ -90,7 +94,6 @@ mixin customizeProfile(mobile) li.customize-menu h5=env.t('bodyFacialHair') - +gemCost(2) button.btn.btn-xs(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('unlockSet',{cost:5}) + ' ' From be3342cb261dc70cfb9ffe7ee644ed2c38523202 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Wed, 26 Nov 2014 19:34:16 -0600 Subject: [PATCH 83/84] feat(event): Thanksgiving Bailey --- views/shared/new-stuff.jade | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/views/shared/new-stuff.jade b/views/shared/new-stuff.jade index c4979df357..f1b254710b 100644 --- a/views/shared/new-stuff.jade +++ b/views/shared/new-stuff.jade @@ -8,19 +8,32 @@ table h3.popover-title a(target='_blank', href='https://twitter.com/Mihakuu') Bailey .popover-content - h5 November Item Set Revealed, Private Messaging Version 1.0 + h5 HAPPY THANKSGIVING! table.table.table-striped tr td - h5 November Item Set Revealed - p The November Subscriber Item has been revealed: the Feast and Fun Set! All November subscribers will receive the Pitchfork of Feasting and the Steel Helm of Sporting. You still have five days to subscribe and receive the item set! Thank you so much for your support - we really do rely on you to keep HabitRPG free to use and running smoothly. + h5 Happy Thanksgiving! + p It's Thanksgiving in Habitica! On this day Habiticans celebrate by spending time with loved ones, giving thanks, and riding their glorious turkeys into the magnificent sunset. Some of the NPCs are celebrating the occasion! p.small.muted by Lemoness tr td - h5 Private Messaging Version 1.0 - p We're excited to announce a new feature: Private Messaging! Now you can send someone a PM by clicking the envelope icon in the bottom-left of their profile window . You can check your messages under Social > Inbox! This is a very rudimentary feature so far, only containing the ability to send messages, block people, and opt out. To read about some of the planned features for the future and make suggestions, check out this Trello card! - p.small.muted by Lefnire - p.small.muted 11/25/2014 + h5 Turkey Pet and Mount! + p Those of you who weren't around last Thanksgiving have received an adorable Turkey Pet, and those of you who got a Turkey Pet last year have received a handsome Turkey Mount! Thank you for using HabitRPG - we really love you guys <3 + p.small.muted by Lemoness + p.small.muted 11/26/2014 + +h5 11/25/2014 +table.table.table-striped + tr + td + h5 November Item Set Revealed + p The November Subscriber Item has been revealed: the Feast and Fun Set! All November subscribers will receive the Pitchfork of Feasting and the Steel Helm of Sporting. You still have five days to subscribe and receive the item set! Thank you so much for your support - we really do rely on you to keep HabitRPG free to use and running smoothly. + p.small.muted by Lemoness + tr + td + h5 Private Messaging Version 1.0 + p We're excited to announce a new feature: Private Messaging! Now you can send someone a PM by clicking the envelope icon in the bottom-left of their profile window . You can check your messages under Social > Inbox! This is a very rudimentary feature so far, only containing the ability to send messages, block people, and opt out. To read about some of the planned features for the future and make suggestions, check out this Trello card! + p.small.muted by Lefnire h5 11/18/2014 table.table.table-striped @@ -30,12 +43,12 @@ table.table.table-striped p Habiticans are in the dark when a giant Night-Owl blots out the Tavern light! Can you drive it away in time to finish your all-nighter? If so, you may find some cute pet owls in the morning... p.small.muted by Twitching, Lemoness, and Arcosine -h5 Share Avatar To Social Media, Email Invites, First Mini Quest, And Data Tab +h5 11/13/2014 - Share Avatar To Social Media, Email Invites, First Mini Quest, And Data Tab table.table.table-striped tr td h5 Share Avatar To Social Media - p You can now automatically share your avatar and public profile to social media! Jut hover over the picture and click the "Share" button in the right-hand corner. Show off your outfit, your achievements, and your profile picture! Note that your tasks, as always, remain 100% private. + p You can now automatically share your avatar and public profile to social media! Just hover over the picture and click the "Share" button in the right-hand corner. Show off your outfit, your achievements, and your profile picture! Note that your tasks, as always, remain 100% private. p.small.muted by Lefnire tr td @@ -52,7 +65,6 @@ table.table.table-striped h5 Data Tab p Now you can access the Data Display Tool and Export Data from the toolbar! p.small.muted by ShilohT -p.small.muted 11/13/2014 h5 11/12/2014 table.table.table-striped From a8cf10261042a6925f4b4949313ba891a1ca5dd0 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Wed, 26 Nov 2014 19:12:59 -0700 Subject: [PATCH 84/84] feat(thanksgiving): add migration for turkey mounts / pets --- migrations/20141126_turkey_mounts.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 migrations/20141126_turkey_mounts.js diff --git a/migrations/20141126_turkey_mounts.js b/migrations/20141126_turkey_mounts.js new file mode 100644 index 0000000000..1270390c06 --- /dev/null +++ b/migrations/20141126_turkey_mounts.js @@ -0,0 +1,11 @@ +db.users.update( + {'items.pets.Turkey-Base':{$ne:null}}, + {$set:{'items.mounts.Turkey-Base':true}}, + {multi:1} +) + +db.users.update( + {'items.pets.Turkey-Base':null}, + {$set:{'items.pets.Turkey-Base':5}}, + {multi:1} +) \ No newline at end of file