Merge branch 'develop' into Ozzystrasza-tavern_default_prize

This commit is contained in:
Blade Barringer
2016-08-05 12:22:41 -05:00
97 changed files with 12873 additions and 12664 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+37 -31
View File
@@ -1,6 +1,6 @@
.2014_Fall_HealerPROMO2 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -637px -1378px;
background-position: -1001px -1378px;
width: 90px;
height: 90px;
}
@@ -12,19 +12,19 @@
}
.2014_Fall_RoguePROMO3 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -452px -865px;
background-position: -306px -220px;
width: 105px;
height: 90px;
}
.2014_Fall_Warrior_PROMO {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1013px -1275px;
background-position: -1295px -1275px;
width: 90px;
height: 90px;
}
.promo_android {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1278px -199px;
background-position: -813px -151px;
width: 175px;
height: 175px;
}
@@ -64,6 +64,12 @@
width: 139px;
height: 588px;
}
.promo_backgrounds_armoire_201608 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -452px -442px;
width: 140px;
height: 439px;
}
.promo_backtoschool {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1493px -151px;
@@ -78,7 +84,7 @@
}
.promo_chairs_glasses {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -452px -442px;
background-position: -452px -882px;
width: 51px;
height: 210px;
}
@@ -96,7 +102,7 @@
}
.promo_dilatoryDistress {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -831px -1275px;
background-position: -1113px -1275px;
width: 90px;
height: 90px;
}
@@ -126,7 +132,7 @@
}
.promo_enchanted_armoire_201509 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1286px -1275px;
background-position: -182px -1378px;
width: 90px;
height: 90px;
}
@@ -138,7 +144,7 @@
}
.promo_enchanted_armoire_201601 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: 0px -1378px;
background-position: -364px -1378px;
width: 90px;
height: 90px;
}
@@ -150,7 +156,7 @@
}
.promo_habitica {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -813px -151px;
background-position: -1278px -199px;
width: 175px;
height: 175px;
}
@@ -174,13 +180,13 @@
}
.promo_mystery_201405 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -273px -1378px;
background-position: -637px -1378px;
width: 90px;
height: 90px;
}
.promo_mystery_201406 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -452px -956px;
background-position: 0px -1378px;
width: 90px;
height: 96px;
}
@@ -198,7 +204,7 @@
}
.promo_mystery_201409 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -455px -1378px;
background-position: -819px -1378px;
width: 90px;
height: 90px;
}
@@ -210,7 +216,7 @@
}
.promo_mystery_201411 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -546px -1378px;
background-position: -910px -1378px;
width: 90px;
height: 90px;
}
@@ -228,13 +234,13 @@
}
.promo_mystery_201502 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -649px -1275px;
background-position: -931px -1275px;
width: 90px;
height: 90px;
}
.promo_mystery_201503 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -91px -1378px;
background-position: -455px -1378px;
width: 90px;
height: 90px;
}
@@ -246,7 +252,7 @@
}
.promo_mystery_201505 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1377px -1275px;
background-position: -273px -1378px;
width: 90px;
height: 90px;
}
@@ -258,31 +264,31 @@
}
.promo_mystery_201507 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -452px -653px;
background-position: -306px -417px;
width: 90px;
height: 105px;
}
.promo_mystery_201508 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -306px -402px;
background-position: -837px -1275px;
width: 93px;
height: 90px;
}
.promo_mystery_201509 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1104px -1275px;
background-position: -1386px -1275px;
width: 90px;
height: 90px;
}
.promo_mystery_201510 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -306px -311px;
background-position: -743px -1275px;
width: 93px;
height: 90px;
}
.promo_mystery_201511 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -922px -1275px;
background-position: -1204px -1275px;
width: 90px;
height: 90px;
}
@@ -300,37 +306,37 @@
}
.promo_mystery_201602 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -740px -1275px;
background-position: -1022px -1275px;
width: 90px;
height: 90px;
}
.promo_mystery_201603 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -364px -1378px;
background-position: -728px -1378px;
width: 90px;
height: 90px;
}
.promo_mystery_201604 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -306px -220px;
background-position: -649px -1275px;
width: 93px;
height: 90px;
}
.promo_mystery_201605 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -182px -1378px;
background-position: -546px -1378px;
width: 90px;
height: 90px;
}
.promo_mystery_201606 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -452px -759px;
background-position: -306px -311px;
width: 90px;
height: 105px;
}
.promo_mystery_201607 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1195px -1275px;
background-position: -91px -1378px;
width: 90px;
height: 90px;
}
@@ -408,13 +414,13 @@
}
.promo_springclasses2014 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -699px -785px;
background-position: -699px -876px;
width: 288px;
height: 90px;
}
.promo_springclasses2015 {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -699px -876px;
background-position: -699px -785px;
width: 288px;
height: 90px;
}
@@ -522,7 +528,7 @@
}
.welcome_basic_avatars {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -994px -642px;
background-position: -1241px -642px;
width: 246px;
height: 165px;
}
@@ -534,7 +540,7 @@
}
.welcome_sample_tasks {
background-image: url(spritesmith-largeSprites-0.png);
background-position: -1241px -642px;
background-position: -994px -642px;
width: 246px;
height: 165px;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 385 KiB

After

Width:  |  Height:  |  Size: 397 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 342 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 146 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 148 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 154 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 60 KiB

+3328 -3328
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

+3274 -3220
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 126 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 134 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 148 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 377 KiB

After

Width:  |  Height:  |  Size: 400 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 154 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 130 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+9 -1
View File
@@ -207,5 +207,13 @@
"backgroundDeepSeaText": "Deep Sea",
"backgroundDeepSeaNotes": "Dive to the Deep Sea.",
"backgroundDilatoryCastleText": "Castle of Dilatory",
"backgroundDilatoryCastleNotes": "Swim past the Castle of Dilatory."
"backgroundDilatoryCastleNotes": "Swim past the Castle of Dilatory.",
"backgrounds082016": "SET 27: Released August 2016",
"backgroundIdyllicCabinText": "Idyllic Cabin",
"backgroundIdyllicCabinNotes": "Retreat to an Idyllic Cabin.",
"backgroundMountainPyramidText": "Mountain Pyramid",
"backgroundMountainPyramidNotes": "Climb the many steps of a Mountain Pyramid.",
"backgroundStormyShipText": "Stormy Ship",
"backgroundStormyShipNotes": "Hold steady against wind and wave aboard a Stormy Ship."
}
+6
View File
@@ -481,6 +481,8 @@
"armorArmoireStripedSwimsuitNotes": "What could be more fun than battling sea monsters on the beach? Increases Constitution by <%= con %>. Enchanted Armoire: Seaside Set (Item 2 of 3).",
"armorArmoireCannoneerRagsText": "Cannoneer Rags",
"armorArmoireCannoneerRagsNotes": "These rags be tougher than they look. Increases Constitution by <%= con %>. Enchanted Armoire: Cannoneer Set (Item 2 of 3).",
"armorArmoireFalconerArmorText": "Falconer Armor",
"armorArmoireFalconerArmorNotes": "Keep away talon attacks with this sturdy armor! Increases Constitution by <%= con %>. Enchanted Armoire: Falconer Set (Item 1 of 3).",
"headgear": "headgear",
"headgearCapitalized": "Headgear",
@@ -746,6 +748,8 @@
"headArmoireGreenFloppyHatNotes": "Many spells have been sewn into this simple hat, giving it a gorgeous green color. Increases Constitution, Intelligence, and Perception by <%= attrs %> each. Enchanted Armoire: Independent Item.",
"headArmoireCannoneerBandannaText": "Cannoneer Bandanna",
"headArmoireCannoneerBandannaNotes": "'Tis a cannoneer's life for me! Increases Intelligence and Perception by <%= attrs %> each. Enchanted Armoire: Cannoneer Set (Item 3 of 3).",
"headArmoireFalconerCapText": "Falconer Cap",
"headArmoireFalconerCapNotes": "This jaunty cap helps you better understand birds of prey. Increases Intelligence by <%= int %>. Enchanted Armoire: Falconer Set (Item 2 of 3).",
"offhand": "shield-hand item",
"offhandCapitalized": "Shield-Hand Item",
@@ -881,6 +885,8 @@
"shieldArmoireFloralBouquetNotes": "Not much help in battle, but aren't they beautiful? Increases Constitution by <%= con %>. Enchanted Armoire: Independent Item.",
"shieldArmoireSandyBucketText": "Sandy Bucket",
"shieldArmoireSandyBucketNotes": "Good for storing all that Gold that you'll earn from completing tasks! Increases Perception by <%= per %>. Enchanted Armoire: Seaside Set (Item 3 of 3).",
"shieldArmoirePerchingFalconText": "Perching Falcon",
"shieldArmoirePerchingFalconNotes": "A falcon friend perches on your arm, prepared to swoop at your enemies. Increases Strength by <%= str %>. Enchanted Armoire: Falconer Set (Item 3 of 3).",
"back": "Back Accessory",
"backBase0Text": "No Back Accessory",
+1
View File
@@ -117,6 +117,7 @@
"audioTheme_gokulTheme": "Gokul Theme",
"audioTheme_luneFoxTheme": "LuneFox's Theme",
"audioTheme_rosstavoTheme": "Rosstavo's Theme",
"audioTheme_dewinTheme": "Dewin's Theme",
"askQuestion": "Ask a Question",
"reportBug": "Report a Bug",
"HabiticaWiki": "The Habitica Wiki",
@@ -367,6 +367,20 @@ let backgrounds = {
notes: t('backgroundDeepSeaNotes'),
},
},
backgrounds082016: {
idyllic_cabin: {
text: t('backgroundIdyllicCabinText'),
notes: t('backgroundIdyllicCabinNotes'),
},
mountain_pyramid: {
text: t('backgroundMountainPyramidText'),
notes: t('backgroundMountainPyramidNotes'),
},
stormy_ship: {
text: t('backgroundStormyShipText'),
notes: t('backgroundStormyShipNotes'),
},
},
};
/* eslint-enable quote-props */
@@ -150,6 +150,14 @@ let armor = {
set: 'cannoneer',
canOwn: ownsItem('armor_armoire_cannoneerRags'),
},
falconerArmor: {
text: t('armorArmoireFalconerArmorText'),
notes: t('armorArmoireFalconerArmorNotes', { con: 10 }),
value: 100,
con: 10,
set: 'falconer',
canOwn: ownsItem('armor_armoire_falconerArmor'),
},
};
let eyewear = {
@@ -380,6 +388,14 @@ let head = {
set: 'cannoneer',
canOwn: ownsItem('head_armoire_cannoneerBandanna'),
},
falconerCap: {
text: t('headArmoireFalconerCapText'),
notes: t('headArmoireFalconerCapNotes', { int: 10 }),
value: 100,
int: 10,
set: 'falconer',
canOwn: ownsItem('head_armoire_falconerCap'),
},
};
let shield = {
@@ -440,6 +456,14 @@ let shield = {
set: 'seaside',
canOwn: ownsItem('shield_armoire_sandyBucket'),
},
perchingFalcon: {
text: t('shieldArmoirePerchingFalconText'),
notes: t('shieldArmoirePerchingFalconNotes', { str: 16 }),
value: 100,
str: 16,
set: 'falconer',
canOwn: ownsItem('shield_armoire_perchingFalcon'),
},
};
let headAccessory = {
+1 -9
View File
@@ -53,7 +53,7 @@ shops.getMarketCategories = function getMarket (user, language) {
value: hatchingPotion.value,
locked: false,
currency: 'gems',
purchaseType: 'hatchingpotions',
purchaseType: 'hatchingPotions',
};
}).sortBy('key').value();
categories.push(hatchingPotionsCategory);
@@ -192,14 +192,6 @@ shops.getTimeTravelersCategories = function getTimeTravelersCategories (user, la
// };
shops.getSeasonalShopCategories = function getSeasonalShopCategories (user, language) {
let availableSets = {
summerWarrior: i18n.t('daringSwashbucklerSet', language),
summerMage: i18n.t('emeraldMermageSet', language),
summerHealer: i18n.t('reefSeahealerSet', language),
summerRogue: i18n.t('roguishPirateSet', language),
summer2015Warrior: i18n.t('sunfishWarriorSet', language),
summer2015Mage: i18n.t('shipSoothsayerSet', language),
summer2015Healer: i18n.t('strappingSailorSet', language),
summer2015Rogue: i18n.t('reefRenegadeSet', language),
};
let categories = [];
+2 -1
View File
@@ -65,7 +65,8 @@
"LOGGLY_ACCOUNT": "account",
"PUSH_CONFIGS": {
"GCM_SERVER_API_KEY": "",
"APN_ENABLED": "true"
"APN_ENABLED": "true",
"FCM_SERVER_API_KEY": ""
},
"PUSHER": {
"ENABLED": "false",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "habitica",
"version": "3.27.0",
"version": "3.28.0",
"dependencies": {
"abbrev": {
"version": "1.0.9",
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "3.27.0",
"version": "3.28.0",
"main": "./website/server/index.js",
"dependencies": {
"accepts": "^1.3.2",
@@ -68,6 +68,7 @@
"nconf": "~0.8.2",
"newrelic": "^1.27.2",
"nib": "^1.1.0",
"node-gcm": "^0.14.4",
"nodemailer": "^2.3.2",
"object-path": "^0.9.2",
"pageres": "^4.1.1",
@@ -4,12 +4,21 @@ import {
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
import common from '../../../../../common';
describe('GET /groups/:groupId/members', () => {
let user;
beforeEach(async () => {
user = await generateUser();
user = await generateUser({
balance: 10,
contributor: {level: 1},
backer: {tier: 3},
preferences: {
costume: false,
background: 'volcano',
},
});
});
it('validates optional req.query.lastId to be an UUID', async () => {
@@ -57,6 +66,30 @@ describe('GET /groups/:groupId/members', () => {
expect(res[0].profile).to.have.all.keys(['name']);
});
it('req.query.includeAllPublicFields === true only works with parties', async () => {
let group = await generateGroup(user, {type: 'guild', name: generateUUID()});
let res = await user.get(`/groups/${group._id}/members?includeAllPublicFields=true`);
expect(res[0]).to.have.all.keys(['_id', 'id', 'profile']);
expect(res[0].profile).to.have.all.keys(['name']);
});
it('populates all public fields if req.query.includeAllPublicFields === true and it is a party', async () => {
await generateGroup(user, {type: 'party', name: generateUUID()});
let [memberRes] = await user.get('/groups/party/members?includeAllPublicFields=true');
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
'backer', 'contributor', 'auth', 'items',
]);
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql(['size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background'].sort());
expect(memberRes.stats.maxMP).to.exists;
expect(memberRes.stats.maxHealth).to.equal(common.maxHealth);
expect(memberRes.stats.toNextLevel).to.equal(common.tnl(memberRes.stats.lvl));
});
it('returns only first 30 members', async () => {
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
@@ -1,9 +1,12 @@
import {
generateUser,
resetHabiticaDB,
} from '../../../../helpers/api-v3-integration.helper';
describe('GET /hall/heroes', () => {
it('returns all heroes sorted by -contributor.level and with correct fields', async () => {
await resetHabiticaDB();
let nonHero = await generateUser();
let hero1 = await generateUser({
contributor: {level: 1},
@@ -3,6 +3,7 @@ import {
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
import common from '../../../../../common';
describe('GET /members/:memberId', () => {
let user;
@@ -36,6 +37,10 @@ describe('GET /members/:memberId', () => {
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
expect(Object.keys(memberRes.preferences).sort()).to.eql(['size', 'hair', 'skin', 'shirt',
'chair', 'costume', 'sleep', 'background'].sort());
expect(memberRes.stats.maxMP).to.exists;
expect(memberRes.stats.maxHealth).to.equal(common.maxHealth);
expect(memberRes.stats.toNextLevel).to.equal(common.tnl(memberRes.stats.lvl));
});
it('handles non-existing members', async () => {
@@ -15,7 +15,7 @@ describe('GET /shops/seasonal', () => {
expect(shop.identifier).to.equal('seasonalShop');
expect(shop.text).to.eql(t('seasonalShop'));
expect(shop.notes).to.eql(t('seasonalShopSummerText'));
expect(shop.notes).to.eql(t('seasonalShopClosedText'));
expect(shop.imageName).to.be.a('string');
expect(shop.categories).to.be.an('array');
});
@@ -1,6 +1,7 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
import common from '../../../../../common';
describe('GET /user', () => {
let user;
@@ -9,9 +10,13 @@ describe('GET /user', () => {
user = await generateUser();
});
it('returns the authenticated user', async () => {
it('returns the authenticated user with computed stats', async () => {
let returnedUser = await user.get('/user');
expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.stats.maxMP).to.exists;
expect(returnedUser.stats.maxHealth).to.equal(common.maxHealth);
expect(returnedUser.stats.toNextLevel).to.equal(common.tnl(returnedUser.stats.lvl));
});
it('does not return private paths (and apiToken)', async () => {
@@ -13,6 +13,7 @@ describe('POST /user/auth/local/login', () => {
api = requester();
user = await generateUser();
});
it('success with username', async () => {
let response = await api.post(endpoint, {
username: user.auth.local.username,
@@ -20,6 +21,7 @@ describe('POST /user/auth/local/login', () => {
});
expect(response.apiToken).to.eql(user.apiToken);
});
it('success with email', async () => {
let response = await api.post(endpoint, {
username: user.auth.local.email,
@@ -27,6 +29,7 @@ describe('POST /user/auth/local/login', () => {
});
expect(response.apiToken).to.eql(user.apiToken);
});
it('user is blocked', async () => {
await user.update({ 'auth.blocked': 1 });
await expect(api.post(endpoint, {
@@ -38,6 +41,7 @@ describe('POST /user/auth/local/login', () => {
message: t('accountSuspended', { userId: user._id }),
});
});
it('wrong password', async () => {
await expect(api.post(endpoint, {
username: user.auth.local.username,
@@ -48,6 +52,7 @@ describe('POST /user/auth/local/login', () => {
message: t('invalidLoginCredentialsLong'),
});
});
it('missing username', async () => {
await expect(api.post(endpoint, {
password: 'wrong-password',
@@ -57,6 +62,7 @@ describe('POST /user/auth/local/login', () => {
message: t('invalidReqParams'),
});
});
it('missing password', async () => {
await expect(api.post(endpoint, {
username: user.auth.local.username,
@@ -0,0 +1,59 @@
import {
generateUser,
requester,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import passport from 'passport';
describe('POST /user/auth/social', () => {
let api;
let user;
let endpoint = '/user/auth/social';
let randomAccessToken = '123456';
let facebookId = 'facebookId';
let network = 'facebook';
before(async () => {
api = requester();
user = await generateUser();
let expectedResult = {id: facebookId};
let passportFacebookProfile = sinon.stub(passport._strategies.facebook, 'userProfile');
passportFacebookProfile.yields(null, expectedResult);
});
it('fails if network is not facebook', async () => {
await expect(api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network: 'NotFacebook',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyFbSupported'),
});
});
it('registers a new user', async () => {
let response = await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
});
it('logs an existing user in', async () => {
await user.update({ 'auth.facebook.id': facebookId });
let response = await api.post(endpoint, {
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.eql(user.apiToken);
expect(response.id).to.eql(user._id);
expect(response.newUser).to.be.false;
});
});
+12 -75
View File
@@ -2,12 +2,13 @@ import { model as User } from '../../../../../website/server/models/user';
import requireAgain from 'require-again';
import pushNotify from 'push-notify';
import nconf from 'nconf';
import gcmLib from 'node-gcm'; // works with FCM notifications too
describe('pushNotifications', () => {
let user;
let sendPushNotification;
let pathToPushNotifications = '../../../../../website/server/libs/pushNotifications';
let gcmSendSpy;
let fcmSendSpy;
let apnSendSpy;
let identifier = 'identifier';
@@ -16,15 +17,12 @@ describe('pushNotifications', () => {
beforeEach(() => {
user = new User();
gcmSendSpy = sinon.spy();
fcmSendSpy = sinon.spy();
apnSendSpy = sinon.spy();
sandbox.stub(nconf, 'get').returns('true');
sandbox.stub(nconf, 'get').returns('true-key');
sandbox.stub(pushNotify, 'gcm').returns({
on: () => null,
send: gcmSendSpy,
});
sandbox.stub(gcmLib.Sender.prototype, 'send', fcmSendSpy);
sandbox.stub(pushNotify, 'apn').returns({
on: () => null,
@@ -40,14 +38,14 @@ describe('pushNotifications', () => {
it('throws if user is not supplied', () => {
expect(sendPushNotification).to.throw;
expect(gcmSendSpy).to.not.have.been.called;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if user.preferences.pushNotifications.unsubscribeFromAll is true', () => {
user.preferences.pushNotifications.unsubscribeFromAll = true;
expect(() => sendPushNotification(user)).to.throw;
expect(gcmSendSpy).to.not.have.been.called;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
@@ -56,7 +54,7 @@ describe('pushNotifications', () => {
title,
message,
})).to.throw;
expect(gcmSendSpy).to.not.have.been.called;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
@@ -65,7 +63,7 @@ describe('pushNotifications', () => {
identifier,
message,
})).to.throw;
expect(gcmSendSpy).to.not.have.been.called;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
@@ -74,7 +72,7 @@ describe('pushNotifications', () => {
identifier,
title,
})).to.throw;
expect(gcmSendSpy).to.not.have.been.called;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
@@ -84,68 +82,7 @@ describe('pushNotifications', () => {
title,
message,
});
expect(gcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('uses GCM for Android devices', () => {
user.pushDevices.push({
type: 'android',
regId: '123',
});
let details = {
identifier,
title,
message,
payload: {
a: true,
b: true,
},
timeToLive: 23,
};
sendPushNotification(user, details);
expect(gcmSendSpy).to.have.been.calledOnce;
expect(gcmSendSpy).to.have.been.calledWithMatch({
registrationId: '123',
delayWhileIdle: true,
timeToLive: 23,
data: {
identifier,
title,
message,
a: true,
b: true,
},
});
expect(apnSendSpy).to.not.have.been.called;
});
it('defaults timeToLive to 15', () => {
user.pushDevices.push({
type: 'android',
regId: '123',
});
let details = {
identifier,
title,
message,
};
sendPushNotification(user, details);
expect(gcmSendSpy).to.have.been.calledOnce;
expect(gcmSendSpy).to.have.been.calledWithMatch({
registrationId: '123',
delayWhileIdle: true,
timeToLive: 15,
data: {
identifier,
title,
message,
},
});
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
@@ -180,6 +117,6 @@ describe('pushNotifications', () => {
b: true,
},
});
expect(gcmSendSpy).to.not.have.been.called;
expect(fcmSendSpy).to.not.have.been.called;
});
});
+16
View File
@@ -1,4 +1,5 @@
import { model as User } from '../../../../../website/server/models/user';
import common from '../../../../../common';
describe('User Model', () => {
it('keeps user._tmp when calling .toJSON', () => {
@@ -31,6 +32,21 @@ describe('User Model', () => {
expect(toJSON).to.not.have.keys('_nonTmp');
});
it('can add computed stats to a JSONified user object', () => {
let user = new User();
let userToJSON = user.toJSON();
expect(userToJSON.stats.maxMP).to.not.exists;
expect(userToJSON.stats.maxHealth).to.not.exists;
expect(userToJSON.stats.toNextLevel).to.not.exists;
user.addComputedStatsToJSONObj(userToJSON);
expect(userToJSON.stats.maxMP).to.exists;
expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth);
expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl));
});
context('notifications', () => {
it('can add notifications with data', () => {
let user = new User();
+1
View File
@@ -14,6 +14,7 @@
display: inline-block
.wide-popover
max-width: -moz-max-content
max-width: max-content
// Small alerts
+3 -1
View File
@@ -163,7 +163,7 @@ api.registerLocal = {
function _loginRes (user, req, res) {
if (user.auth.blocked) throw new NotAuthorized(res.t('accountSuspended', {userId: user._id}));
return res.respond(200, {id: user._id, apiToken: user.apiToken});
return res.respond(200, {id: user._id, apiToken: user.apiToken, newUser: user.newUser || false});
}
/**
@@ -178,6 +178,7 @@ function _loginRes (user, req, res) {
*
* @apiSuccess {String} data._id The user's unique identifier
* @apiSuccess {String} data.apiToken The user's api token that must be used to authenticate requests.
* @apiSuccess {Boolean} data.newUser Returns true if the user was just created (always false for local login).
*/
api.loginLocal = {
method: 'POST',
@@ -260,6 +261,7 @@ api.loginSocial = {
let savedUser = await user.save();
user.newUser = true;
_loginRes(user, ...arguments);
// Clean previous email preferences
+13 -2
View File
@@ -50,7 +50,10 @@ api.getMember = {
if (!member) throw new NotFound(res.t('userWithIDNotFound', {userId: memberId}));
// manually call toJSON with minimize: true so empty paths aren't returned
res.respond(200, member.toJSON({minimize: true}));
let memberToJSON = member.toJSON({minimize: true});
member.addComputedStatsToJSONObj(memberToJSON);
res.respond(200, memberToJSON);
},
};
@@ -100,6 +103,7 @@ function _getMembersForItem (type) {
let query = {};
let fields = nameFields;
let addComputedStats = false; // add computes stats to the member info when items and stats are available
if (type === 'challenge-members') {
query.challenges = challenge._id;
@@ -111,6 +115,7 @@ function _getMembersForItem (type) {
if (req.query.includeAllPublicFields === 'true') {
fields = memberFields;
addComputedStats = true;
}
}
} else if (type === 'group-invites') {
@@ -138,7 +143,13 @@ function _getMembersForItem (type) {
.exec();
// manually call toJSON with minimize: true so empty paths aren't returned
res.respond(200, members.map(member => member.toJSON({minimize: true})));
let membersToJSON = members.map(member => {
let memberToJSON = member.toJSON({minimize: true});
if (addComputedStats) member.addComputedStatsToJSONObj(memberToJSON);
return memberToJSON;
});
res.respond(200, membersToJSON);
};
}
+2 -2
View File
@@ -111,8 +111,8 @@ api.getSeasonalShopItems = {
let resObject = {
identifier: 'seasonalShop',
text: res.t('seasonalShop'),
notes: res.t('seasonalShopSummerText'),
imageName: 'seasonalshop_open',
notes: res.t('seasonalShopClosedText'),
imageName: 'seasonalshop_closed',
categories: shops.getSeasonalShopCategories(user, req.language),
};
+5 -10
View File
@@ -30,19 +30,14 @@ api.getUser = {
middlewares: [authWithHeaders()],
url: '/user',
async handler (req, res) {
let user = res.locals.user.toJSON();
let user = res.locals.user;
let userToJSON = user.toJSON();
// Remove apiToken from response TODO make it private at the user level? returned in signup/login
delete user.apiToken;
delete userToJSON.apiToken;
// TODO move to model? (maybe virtuals, maybe in toJSON)
// NOTE: if an item is manually added to user.stats common/fns/predictableRandom must be tweaked
// so it's not considered. Otherwise the client will have it while the server won't and the results will be different.
user.stats.toNextLevel = common.tnl(user.stats.lvl);
user.stats.maxHealth = common.maxHealth;
user.stats.maxMP = common.statsComputed(user).maxMP;
return res.respond(200, user);
user.addComputedStatsToJSONObj(userToJSON);
return res.respond(200, userToJSON);
},
};
@@ -8,7 +8,7 @@ const md = markdownIt({
let api = {};
const TOTAL_USER_COUNT = '1,100,000';
const TOTAL_USER_COUNT = '1,500,000';
const LOADING_SCREEN_TIPS = 32;
api.getFrontPage = {
+14 -18
View File
@@ -1,5 +1,6 @@
import _ from 'lodash';
import nconf from 'nconf';
// TODO remove this lib and use directly the apn module
import pushNotify from 'push-notify';
import apnLib from 'apn';
import logger from './logger';
@@ -7,19 +8,11 @@ import Bluebird from 'bluebird';
import {
S3,
} from './aws';
import gcmLib from 'node-gcm'; // works with FCM notifications too
const GCM_API_KEY = nconf.get('PUSH_CONFIGS:GCM_SERVER_API_KEY');
const FCM_API_KEY = nconf.get('PUSH_CONFIGS:FCM_SERVER_API_KEY');
let gcm = GCM_API_KEY ? pushNotify.gcm({
apiKey: GCM_API_KEY,
retries: 3,
}) : undefined;
if (gcm) {
gcm.on('transmissionError', (err, message, registrationId) => {
logger.error('GCM Error', err, message, registrationId);
});
}
const fcmSender = FCM_API_KEY ? new gcmLib.Sender(FCM_API_KEY) : undefined;
let apn;
@@ -82,15 +75,18 @@ module.exports = function sendNotification (user, details = {}) {
_.each(pushDevices, pushDevice => {
switch (pushDevice.type) {
case 'android':
if (gcm) {
payload.title = details.title;
payload.message = details.message;
gcm.send({
registrationId: pushDevice.regId,
delayWhileIdle: true,
timeToLive: details.timeToLive ? details.timeToLive : 15,
// Required for fcm to be received in background
payload.title = details.title;
payload.body = details.message;
if (fcmSender) {
let message = new gcmLib.Message({
data: payload,
});
fcmSender.send(message, {
registrationTokens: [pushDevice.regId],
}, 10, (err) => logger.error('FCM Error', err));
}
break;
+14 -3
View File
@@ -1,4 +1,4 @@
import shared from '../../../../common';
import common from '../../../../common';
import Bluebird from 'bluebird';
import {
chatDefaults,
@@ -23,12 +23,12 @@ schema.methods.getGroups = function getUserGroups () {
schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, message) {
let sender = this;
shared.refPush(userToReceiveMessage.inbox.messages, chatDefaults(message, sender));
common.refPush(userToReceiveMessage.inbox.messages, chatDefaults(message, sender));
userToReceiveMessage.inbox.newMessages++;
userToReceiveMessage._v++;
userToReceiveMessage.markModified('inbox.messages');
shared.refPush(sender.inbox.messages, defaults({sent: true}, chatDefaults(message, userToReceiveMessage)));
common.refPush(sender.inbox.messages, defaults({sent: true}, chatDefaults(message, userToReceiveMessage)));
sender.markModified('inbox.messages');
let promises = [userToReceiveMessage.save(), sender.save()];
@@ -40,4 +40,15 @@ schema.methods.addNotification = function addUserNotification (type, data = {})
type,
data,
});
};
// Add stats.toNextLevel, stats.maxMP and stats.maxHealth
// to a JSONified User object
schema.methods.addComputedStatsToJSONObj = function addComputedStatsToUserJSONObj (obj) {
// NOTE: if an item is manually added to user.stats then
// common/fns/predictableRandom must be tweaked so the new item is not considered.
// Otherwise the client will have it while the server won't and the results will be different.
obj.stats.toNextLevel = common.tnl(this.stats.lvl);
obj.stats.maxHealth = common.maxHealth;
obj.stats.maxMP = common.statsComputed(this).maxMP;
};
+1 -1
View File
@@ -391,7 +391,7 @@ let schema = new Schema({
skin: {type: String, default: '915533'},
shirt: {type: String, default: 'blue'},
timezoneOffset: {type: Number, default: 0},
sound: {type: String, default: 'off', enum: ['off', 'danielTheBard', 'gokulTheme', 'luneFoxTheme', 'wattsTheme', 'rosstavoTheme']},
sound: {type: String, default: 'off', enum: ['off', 'danielTheBard', 'gokulTheme', 'luneFoxTheme', 'wattsTheme', 'rosstavoTheme', 'dewinTheme']},
chair: {type: String, default: 'none'},
timezoneOffsetAtLastCron: Number,
language: String,
+2 -2
View File
@@ -223,9 +223,9 @@ nav.toolbar(ng-controller='MenuCtrl')
div
ul.toolbar-submenu
// Using [{k,v}] instead of {k:v,k:v} to maintain order ('off' at top)
for theme in ['off', 'danielTheBard', 'gokulTheme', 'luneFoxTheme', 'wattsTheme', 'rosstavoTheme']
for theme in ['off', 'danielTheBard', 'gokulTheme', 'luneFoxTheme', 'wattsTheme', 'rosstavoTheme', 'dewinTheme']
li
a(ng-class="{'bg-primary':user.preferences.sound === '# {theme}'}", ng-click="set({'preferences.sound':'#{theme}'})")=env.t('audioTheme_'+theme)
a(ng-class="{'bg-info':user.preferences.sound === '#{theme}'}", ng-click="set({'preferences.sound':'#{theme}'})")=env.t('audioTheme_'+theme)
ul.toolbar-controls
li.toolbar-controls-button
a(data-close-menu)=env.t('close')
+42 -25
View File
@@ -1,42 +1,59 @@
h2 8/1/2016 - TAKE THIS CHALLENGE AND GUILD SPOTLIGHT
h3 8/3/2016 - AUGUST BACKGROUNDS AND ARMOIRE ITEMS; NEW AUDIO THEME
.promo_backgrounds_armoire_201608.pull-right
hr
tr
td
.promo_takethis_armor.pull-right
h3 New Take This Challenge
p The next Take This Challenge has launched, <a href='/#/options/groups/challenges/ee2b3c87-13f0-422a-af3c-309102d4f7e6'>Cast of Characters</a>, with a focus on picturing your feelings as characters in a story. Be sure to check it out to earn additional pieces of the Take This armor set!
br
p <a href='http://www.takethis.org/' target='_blank'>Take This</a> is a nonprofit that seeks to inform the gamer community about mental health issues, to provide education about mental disorders and mental illness prevention, and to reduce the stigma of mental illness.
br
p Congratulations to the winners of the last Take This Challenge, "Check Your HP": grand prize winner bookguinea, and runners-up abbey, SeanXiaaa, pushkin89, fightorflight, and ProfDzul. Plus, all participants in that Challenge have received a piece of the <a href='http://habitica.wikia.com/wiki/Event_Item_Sequences#Take_This_Armor_Set' target='_blank'>Take This item set</a>. Enjoy!
p.small.muted by Lemoness, SabreCat, Doctor B, and the Take This team
h3 August Backgrounds
p Weve added three new backgrounds: Stormy Ship, Idyllic Cabin, and Mountain Pyramid. Get them now from the <a href='/#/options/profile/backgrounds'>Background Shop</a> and show off your avatars in the Tavern!
p.small.muted by JInjooHat, KusSv, and Beffymaroo
tr
td
h3 GUILD SPOTLIGHT
p There's a new <a href='https://habitica.wordpress.com/category/guild-spotlight/' target='_blank'>Guild Spotlight on the blog</a>! Check it out now to learn about some awesome Habitica Guilds that highlight fun fandoms.
p.small.muted by Lemoness
h3 August Armoire Items: Falconer Set
p Theres new Gold-purchasable equipment in the Enchanted Armoire, including the Falconer set. The Enchanted Armoire unlocks when you achieve Ultimate Gear. Better work hard on your real-life tasks to earn all the pieces!
p.small.muted by Mako413
tr
td
h3 New Audio Theme
p There's a new audio theme available: Dewin's Theme! Select this theme from the 🔈 drop-down to level up to sounds with an 8-bit flavor.
p.small.muted by Dewin
if menuItem !== 'oldNews'
hr
a(href='/static/old-news', target='_blank') Read older news
mixin oldNews
h2 7/31/2016 - HABITICA NAMING DAY AND GRYPHON PETS/MOUNTS!
h2 8/1/2016 - TAKE THIS CHALLENGE AND GUILD SPOTLIGHT
tr
td
span.Mount_Body_Gryphon-RoyalPurple.pull-right
span.Mount_Head_Gryphon-RoyalPurple.pull-right(style='margin:0')
h3 Habitica Naming Day
p Happy Habitica Naming day! In honor of the day when we changed the name of the app from HabitRPG to Habitica, we've given everyone an achievement, as well as some delicious cake for your pets and mounts.
p.small.muted by Lemoness and SabreCat
tr
td
.Pet-Gryphon-RoyalPurple.pull-right
h3 Habitica Gryphon Mount and Pet
p Speaking of pets and mounts, we've given all new users our Purple Gryphon Mount, Melior! If you already received Melior last year, we've given you his little sister Meliora, a Purple Gryphon Pet. You can find them in the Rare Pet/Rare Mount sections of the stable!
.promo_takethis_armor.pull-right
h3 New Take This Challenge
p The next Take This Challenge has launched, <a href='/#/options/groups/challenges/ee2b3c87-13f0-422a-af3c-309102d4f7e6'>Cast of Characters</a>, with a focus on picturing your feelings as characters in a story. Be sure to check it out to earn additional pieces of the Take This armor set!
br
p Thanks for being a Habitica user -- you all mean so much to us. We hope that you enjoy your presents!
p.small.muted by Lemoness and Baconsaur
p <a href='http://www.takethis.org/' target='_blank'>Take This</a> is a nonprofit that seeks to inform the gamer community about mental health issues, to provide education about mental disorders and mental illness prevention, and to reduce the stigma of mental illness.
br
p Congratulations to the winners of the last Take This Challenge, "Check Your HP": grand prize winner bookguinea, and runners-up abbey, SeanXiaaa, pushkin89, fightorflight, and ProfDzul. Plus, all participants in that Challenge have received a piece of the <a href='http://habitica.wikia.com/wiki/Event_Item_Sequences#Take_This_Armor_Set' target='_blank'>Take This item set</a>. Enjoy!
p.small.muted by Lemoness, SabreCat, Doctor B, and the Take This team
tr
td
h3 Guild Spotlight
p There's a new <a href='https://habitica.wordpress.com/category/guild-spotlight/' target='_blank'>Guild Spotlight on the blog</a>! Check it out now to learn about some awesome Habitica Guilds that highlight fun fandoms.
p.small.muted by Lemoness
h2 7/31/2016 - HABITICA NAMING DAY AND GRYPHON PETS/MOUNTS!
tr
td
span.Mount_Body_Gryphon-RoyalPurple.pull-right
span.Mount_Head_Gryphon-RoyalPurple.pull-right(style='margin:0')
h3 Habitica Naming Day
p Happy Habitica Naming day! In honor of the day when we changed the name of the app from HabitRPG to Habitica, we've given everyone an achievement, as well as some delicious cake for your pets and mounts.
p.small.muted by Lemoness and SabreCat
tr
td
.Pet-Gryphon-RoyalPurple.pull-right
h3 Habitica Gryphon Mount and Pet
p Speaking of pets and mounts, we've given all new users our Purple Gryphon Mount, Melior! If you already received Melior last year, we've given you his little sister Meliora, a Purple Gryphon Pet. You can find them in the Rare Pet/Rare Mount sections of the stable!
br
p Thanks for being a Habitica user -- you all mean so much to us. We hope that you enjoy your presents!
p.small.muted by Lemoness and Baconsaur
h2 7/29/2016 - LAST CHANCE FOR SUMMER SPLASH; MOBILE UPDATES
tr
td
+2 -3
View File
@@ -37,9 +37,8 @@ block content
a.gallery(href='/marketing/guild.png', title=env.t('marketing2Header'))
img.img-rendering-auto(src='/marketing/guild.png')
p.lead=env.t('marketing2Lead1')
a.gallery(href='/common/img/sprites/spritesmith/quests/quest_vice3.png', title=env.t('marketing2Lead2Title'))
img(src='/common/img/sprites/spritesmith/quests/quest_vice3.png')
a.gallery(href='/common/img/sprites/spritesmith/quests/bosses/quest_vice3.png', title=env.t('marketing2Lead2Title'))
img(src='/common/img/sprites/spritesmith/quests/bosses/quest_vice3.png')
p.lead!=env.t('marketing2Lead2')
.col-md-6
a.gallery(href='/marketing/challenge.png', title=env.t('challenges'))