begins porting group model to es6

This commit is contained in:
Matteo Pagliazzi
2015-12-16 20:00:10 +01:00
parent 2e5c7df94f
commit bf7fc985d0
3 changed files with 199 additions and 173 deletions

View File

@@ -75,7 +75,7 @@
"comma-style": [2, "last"], "comma-style": [2, "last"],
"comma-dangle": [2, "always-multiline"], "comma-dangle": [2, "always-multiline"],
"computed-property-spacing": [2, "never"], "computed-property-spacing": [2, "never"],
"consistent-this": [2, "self"], "consistent-this": [0, "self"],
"func-names": 2, "func-names": 2,
"func-style": [2, "declaration", { "allowArrowFunctions": true }], "func-style": [2, "declaration", { "allowArrowFunctions": true }],
"block-spacing": [2, "always"], "block-spacing": [2, "always"],

View File

@@ -5,6 +5,8 @@ const SERVER_FILES = [
'./website/src/**/api-v3/**/*.js', './website/src/**/api-v3/**/*.js',
'./website/src/models/user.js', './website/src/models/user.js',
'./website/src/models/task.js', './website/src/models/task.js',
'./website/src/models/group.js',
'./website/src/models/tag.js',
'./website/src/models/emailUnsubscription.js', './website/src/models/emailUnsubscription.js',
'./website/src/server.js', './website/src/server.js',
]; ];

View File

@@ -1,26 +1,28 @@
var mongoose = require("mongoose"); import mongoose from 'mongoose';
var Schema = mongoose.Schema; import { model as User} from './user';
var User = require('./user').model; import shared from '../../../common';
var shared = require('../../../common'); import _ from 'lodash';
var _ = require('lodash'); // var async = require('async');
var async = require('async'); import logger from '../libs/api-v3/logger';
var logging = require('../libs/api-v2/logging'); // var Challenge = require('./../models/challenge').model;
var Challenge = require('./../models/challenge').model; import firebase from '../libs/api-v2/firebase';
var firebase = require('../libs/api-v2/firebase'); import baseModel from '../libs/api-v3/baseModel';
import Q from 'q';
// NOTE any change to groups' members in MongoDB will have to be run through the API let Schema = mongoose.Schema;
// NOTE once Firebase is enabled any change to groups' members in MongoDB will have to be run through the API
// changes made directly to the db will cause Firebase to get out of sync // changes made directly to the db will cause Firebase to get out of sync
var GroupSchema = new Schema({ export let schema = new Schema({
_id: {type: String, 'default': shared.uuid}, name: {type: String, required: true},
name: String,
description: String, description: String,
leader: {type: String, ref: 'User'}, leader: {type: String, ref: 'User'},
members: [{type: String, ref: 'User'}], members: [{type: String, ref: 'User'}], // TODO do we need this? could depend on back-ref instead (User.find({group:GID})
invites: [{type: String, ref: 'User'}], invites: [{type: String, ref: 'User'}], // TODO do we need this? could depend on back-ref instead (User.find({group:GID})
type: {type: String, "enum": ['guild', 'party']}, type: {type: String, enum: ['guild', 'party'], required: true},
privacy: {type: String, "enum": ['private', 'public'], 'default':'private'}, privacy: {type: String, enum: ['private', 'public'], default: 'private', required: true},
//_v: {type: Number,'default': 0}, // _v: {type: Number,'default': 0}, // TODO ?
chat: Array, chat: Array, // TODO ?
/* /*
# [{ # [{
# timestamp: Date # timestamp: Date
@@ -32,41 +34,52 @@ var GroupSchema = new Schema({
# }] # }]
*/ */
leaderOnly: { // restrict group actions to leader (members can't do them) leaderOnly: { // restrict group actions to leader (members can't do them)
challenges: {type:Boolean, 'default':false}, challenges: {type: Boolean, default: false, required: true},
//invites: {type:Boolean, 'default':false} // invites: {type:Boolean, 'default':false} // TODO ?
}, },
memberCount: {type: Number, 'default': 0}, memberCount: {type: Number, default: 0},
challengeCount: {type: Number, 'default': 0}, challengeCount: {type: Number, default: 0},
balance: Number, balance: {type: Number, default: 0},
logo: String, logo: String,
leaderMessage: String, leaderMessage: String,
challenges: [{type:'String', ref:'Challenge'}], // do we need this? could depend on back-ref instead (Challenge.find({group:GID})) challenges: [{type: String, ref: 'Challenge'}], // TODO do we need this? could depend on back-ref instead (Challenge.find({group:GID}))
quest: { quest: {
key: String, key: String,
active: {type:Boolean, 'default':false}, active: {type: Boolean, default: false},
leader: {type:String, ref:'User'}, leader: {type: String, ref: 'User'},
progress:{ progress: {
hp: Number, hp: Number,
collect: {type:Schema.Types.Mixed, 'default':{}}, // {feather: 5, ingot: 3} collect: {type: Schema.Types.Mixed, default: () => {
return {};
}}, // {feather: 5, ingot: 3}
rage: Number, // limit break / "energy stored in shell", for explosion-attacks rage: Number, // limit break / "energy stored in shell", for explosion-attacks
}, },
//Shows boolean for each party-member who has accepted the quest. Eg {UUID: true, UUID: false}. Once all users click // Shows boolean for each party-member who has accepted the quest. Eg {UUID: true, UUID: false}. Once all users click
//'Accept', the quest begins. If a false user waits too long, probably a good sign to prod them or boot them. // 'Accept', the quest begins. If a false user waits too long, probably a good sign to prod them or boot them.
//TODO when booting user, remove from .joined and check again if we can now start the quest // TODO when booting user, remove from .joined and check again if we can now start the quest
members: Schema.Types.Mixed, members: {type: Schema.Types.Mixed, default: () => {
extra: Schema.Types.Mixed return {};
} }},
extra: {type: Schema.Types.Mixed, default: () => {
return {};
}},
},
}, { }, {
strict: 'throw', strict: true,
minimize: false // So empty objects are returned minimize: false, // So empty objects are returned
}); });
schema.plugin(baseModel, {
noSet: ['_id'],
});
// TODO migration
/** /**
* Derby duplicated stuff. This is a temporary solution, once we're completely off derby we'll run an mongo migration * Derby duplicated stuff. This is a temporary solution, once we're completely off derby we'll run an mongo migration
* to remove duplicates, then take these fucntions out * to remove duplicates, then take these fucntions out
*/ */
function removeDuplicates(doc){ /* function removeDuplicates(doc){
// Remove duplicate members // Remove duplicate members
if (doc.members) { if (doc.members) {
var uniqMembers = _.uniq(doc.members); var uniqMembers = _.uniq(doc.members);
@@ -74,219 +87,238 @@ function removeDuplicates(doc){
doc.members = uniqMembers; doc.members = uniqMembers;
} }
} }
} }*/
// FIXME this isn't always triggered, since we sometimes use update() or findByIdAndUpdate() // FIXME this isn't always triggered, since we sometimes use update() or findByIdAndUpdate()
// @see https://github.com/LearnBoost/mongoose/issues/964 // @see https://github.com/LearnBoost/mongoose/issues/964 -> Add update pre?
GroupSchema.pre('save', function(next){ // TODO necessary?
removeDuplicates(this); schema.pre('save', function preSaveGroup (next) {
// removeDuplicates(this);
this.memberCount = _.size(this.members); this.memberCount = _.size(this.members);
this.challengeCount = _.size(this.challenges); this.challengeCount = _.size(this.challenges);
next(); return next();
})
GroupSchema.pre('remove', function(next) {
var group = this;
async.waterfall([
function(cb) {
var invitationQuery = {};
var groupType = group.type;
//Add an 's' to group type guild because the model has the plural version
if (group.type == "guild") groupType += "s";
invitationQuery['invitations.' + groupType + '.id'] = group._id;
User.find(invitationQuery, cb);
},
function(users, cb) {
if (users) {
users.forEach(function (user, index, array) {
if ( group.type == "party" ) {
user.invitations.party = {};
} else {
var i = _.findIndex(user.invitations.guilds, {id: group._id});
user.invitations.guilds.splice(i, 1);
}
user.save();
});
}
cb();
}
], next);
}); });
GroupSchema.post('remove', function(group) { schema.pre('remove', true, function preRemoveGroup (next, done) {
next();
let group = this;
// Remove invitations when group is deleted
// TODO verify it works fir everything
User.find({
// TODO remove need for guilds s in migration? same for id -> _id
[`invitations.${group.type}${group.type === 'guild' ? 's' : ''}.id`]: group._id,
}).exec()
.then(users => {
return Q.all(users.map(user => {
if (group.type === 'party') {
user.invitations.party = {};
} else {
let i = _.findIndex(user.invitations.guilds, {id: group._id});
user.invitations.guilds.splice(i, 1);
}
return user.save(); // TODO update?
}));
})
.then(done)
.catch(done);
});
schema.post('remove', function postRemoveGroup (group) {
firebase.deleteGroup(group._id); firebase.deleteGroup(group._id);
}); });
GroupSchema.methods.toJSON = function(){ schema.methods.toJSON = function groupToJSON () {
var doc = this.toObject(); let doc = this.toObject();
removeDuplicates(doc); // removeDuplicates(doc);
doc._isMember = this._isMember; doc._isMember = this._isMember; // TODO ?
//fix(groups): temp fix to remove chat entries stored as strings (not sure why that's happening..). // TODO migration
// fix(groups): temp fix to remove chat entries stored as strings (not sure why that's happening..).
// Required as angular 1.3 is strict on dupes, and no message.id to `track by` // Required as angular 1.3 is strict on dupes, and no message.id to `track by`
_.remove(doc.chat,function(msg){return !msg.id}); _.remove(doc.chat, msg => !msg.id);
// TODO should not be needed here
// @see pre('save') comment above // @see pre('save') comment above
this.memberCount = _.size(this.members); this.memberCount = _.size(this.members);
this.challengeCount = _.size(this.challenges); this.challengeCount = _.size(this.challenges);
return doc; return doc;
} };
var chatDefaults = module.exports.chatDefaults = function(msg,user){ // TODO move to its own model
var message = { export function chatDefaults (msg, user) {
let message = {
id: shared.uuid(), id: shared.uuid(),
text: msg, text: msg,
timestamp: +new Date, timestamp: Number(new Date()),
likes: {}, likes: {},
flags: {}, flags: {},
flagCount: 0 flagCount: 0,
}; };
if (user) { if (user) {
_.defaults(message, { _.defaults(message, {
uuid: user._id, uuid: user._id,
contributor: user.contributor && user.contributor.toObject(), contributor: user.contributor && user.contributor.toObject(),
backer: user.backer && user.backer.toObject(), backer: user.backer && user.backer.toObject(),
user: user.profile.name user: user.profile.name,
}); });
} else { } else {
message.uuid = 'system'; message.uuid = 'system';
} }
return message; return message;
} }
GroupSchema.methods.sendChat = function(message, user){
var group = this; schema.methods.sendChat = function sendChat (message, user) {
group.chat.unshift(chatDefaults(message,user)); this.chat.unshift(chatDefaults(message, user));
group.chat.splice(200); this.chat.splice(200);
// Kick off chat notifications in the background.
var lastSeenUpdate = {$set:{}, $inc:{_v:1}}; // Kick off chat notifications in the background. // TODO refactor
lastSeenUpdate['$set']['newMessages.'+group._id] = {name:group.name,value:true}; let lastSeenUpdate = {$set: {}, $inc: {_v: 1}}; // TODO standardize this _v inc at the user level
if (group._id == 'habitrpg') { lastSeenUpdate.$set[`newMessages.${this._id}`] = {name: this.name, value: true};
if (this._id === 'habitrpg') {
// TODO For Tavern, only notify them if their name was mentioned // TODO For Tavern, only notify them if their name was mentioned
// var profileNames = [] // get usernames from regex of @xyz. how to handle space-delimited profile names? // var profileNames = [] // get usernames from regex of @xyz. how to handle space-delimited profile names?
// User.update({'profile.name':{$in:profileNames}},lastSeenUpdate,{multi:true}).exec(); // User.update({'profile.name':{$in:profileNames}},lastSeenUpdate,{multi:true}).exec();
} else { } else {
mongoose.model('User').update({_id:{$in:group.members, $ne: user ? user._id : ''}},lastSeenUpdate,{multi:true}).exec(); User.update({
_id: {$in: this.members, $ne: user ? user._id : ''},
}, lastSeenUpdate, {multi: true}).exec();
} }
} };
var cleanQuestProgress = function(merge){ function _cleanQuestProgress (merge) {
var clean = { // TODO clone? (also in sendChat message)
let clean = {
key: null, key: null,
progress: { progress: {
up: 0, up: 0,
down: 0, down: 0,
collect: {} collect: {},
}, },
completed: null, completed: null,
RSVPNeeded: false RSVPNeeded: false, // TODO absolutely change this cryptic name
}; };
merge = merge || {progress:{}};
_.merge(clean, _.omit(merge,'progress'));
_.merge(clean.progress, merge.progress);
return clean;
}
GroupSchema.statics.cleanQuestProgress = cleanQuestProgress;
// Participants: Grant rewards & achievements, finish quest if (merge) { // TODO why does it do 2 merges?
GroupSchema.methods.finishQuest = function(quest, cb) { _.merge(clean, _.omit(merge, 'progress'));
var group = this; _.merge(clean.progress, merge.progress);
var questK = quest.key;
var updates = {$inc:{},$set:{}};
updates['$inc']['achievements.quests.' + questK] = 1;
updates['$inc']['stats.gp'] = +quest.drop.gp;
updates['$inc']['stats.exp'] = +quest.drop.exp;
updates['$inc']['_v'] = 1;
if (group._id == 'habitrpg') {
updates['$set']['party.quest.completed'] = questK; // Just show the notif
} else {
updates['$set']['party.quest'] = cleanQuestProgress({completed: questK}); // clear quest progress
} }
_.each(quest.drop.items, function(item){ return clean;
var dropK = item.key; }
schema.statics.cleanQuestProgress = _cleanQuestProgress;
// Participants: Grant rewards & achievements, finish quest
// TODO transform in promise
schema.methods.finishQuest = function finishQuest (quest, cb) {
let questK = quest.key;
let updates = {$inc: {}, $set: {}};
updates.$inc[`achievements.quests.${questK}`] = 1;
updates.$inc['stats.gp'] = Number(quest.drop.gp); // TODO are this castings necessary?
updates.$inc['stats.exp'] = Number(quest.drop.exp);
updates.$inc._v = 1;
if (this._id === 'habitrpg') {
updates.$set['party.quest.completed'] = questK; // Just show the notif
} else {
updates.$set['party.quest'] = _cleanQuestProgress({completed: questK}); // clear quest progress
}
_.each(quest.drop.items, (item) => {
let dropK = item.key;
switch (item.type) { switch (item.type) {
case 'gear': case 'gear':
// TODO This means they can lose their new gear on death, is that what we want? // TODO This means they can lose their new gear on death, is that what we want?
updates['$set']['items.gear.owned.'+dropK] = true; updates.$set[`items.gear.owned.${dropK}`] = true;
break; break;
case 'eggs': case 'eggs':
case 'food': case 'food':
case 'hatchingPotions': case 'hatchingPotions':
case 'quests': case 'quests':
updates['$inc']['items.'+item.type+'.'+dropK] = _.where(quest.drop.items,{type:item.type,key:item.key}).length; updates.$inc[`items.${item.type}.${dropK}`] = _.where(quest.drop.items, {type: item.type, key: item.key}).length;
break; break;
case 'pets': case 'pets':
updates['$set']['items.pets.'+dropK] = 5; updates.$set[`items.pets.${dropK}`] = 5;
break; break;
case 'mounts': case 'mounts':
updates['$set']['items.mounts.'+dropK] = true; updates.$set[`items.mounts.${dropK}`] = true;
break; break;
} }
}) });
var q = group._id === 'habitrpg' ? {} : {_id:{$in:_.keys(group.quest.members)}};
group.quest = {};group.markModified('quest');
mongoose.model('User').update(q, updates, {multi:true}, cb);
}
function isOnQuest(user,progress,group){ let q = this._id === 'habitrpg' ? {} : {_id: {$in: _.keys(this.quest.members)}};
this.quest = {};
this.markModified('quest');
User.update(q, updates, {multi: true}, cb);
};
function _isOnQuest (user, progress, group) {
return group && progress && group.quest && group.quest.active && group.quest.members[user._id] === true; return group && progress && group.quest && group.quest.active && group.quest.members[user._id] === true;
} }
GroupSchema.statics.collectQuest = function(user, progress, cb) { // TODO use promise
this.findOne({type: 'party', members: {'$in': [user._id]}},function(err, group){ schema.statics.collectQuest = function collectQuest (user, progress, cb) {
if (!isOnQuest(user,progress,group)) return cb(null); this.findOne({
var quest = shared.content.quests[group.quest.key]; type: 'party',
members: {$in: [user._id]},
}).then(group => {
if (!_isOnQuest(user, progress, group)) return cb();
let quest = shared.content.quests[group.quest.key];
_.each(progress.collect,function(v,k){ _.each(progress.collect, (v, k) => {
group.quest.progress.collect[k] += v; group.quest.progress.collect[k] += v;
}); });
var foundText = _.reduce(progress.collect, function(m,v,k){ let foundText = _.reduce(progress.collect, (m, v, k) => {
m.push(v + ' ' + quest.collect[k].text('en')); m.push(`${v} ${quest.collect[k].text('en')}`);
return m; return m;
}, []); }, []);
foundText = foundText ? foundText.join(', ') : 'nothing'; foundText = foundText ? foundText.join(', ') : 'nothing';
group.sendChat("`" + user.profile.name + " found "+foundText+".`"); group.sendChat(`\`${user.profile.name} found ${foundText}.\``);
group.markModified('quest.progress.collect'); group.markModified('quest.progress.collect');
// Still needs completing // Still needs completing
if (_.find(shared.content.quests[group.quest.key].collect, function(v,k){ if (_.find(shared.content.quests[group.quest.key].collect, (v, k) => {
return group.quest.progress.collect[k] < v.count; return group.quest.progress.collect[k] < v.count;
})) return group.save(cb); })) return group.save(cb);
async.series([ // TODO use promise
function(cb2){ group.finishQuest(quest, () => {
group.finishQuest(quest,cb2); group.sendChat('`All items found! Party has received their rewards.`');
}, group.save(cb);
function(cb2){ });
group.sendChat('`All items found! Party has received their rewards.`');
group.save(cb2);
}
],cb);
}) })
} .catch(cb);
};
// to set a boss: `db.groups.update({_id:'habitrpg'},{$set:{quest:{key:'dilatory',active:true,progress:{hp:1000,rage:1500}}}})` // to set a boss: `db.groups.update({_id:'habitrpg'},{$set:{quest:{key:'dilatory',active:true,progress:{hp:1000,rage:1500}}}})`
module.exports.tavernQuest = {}; // we export an empty object that is then populated with the query-returned data
var tavernQ = {_id:'habitrpg','quest.key':{$ne:null}}; export let tavernQuest = {};
process.nextTick(function(){ process.nextTick(function(){
mongoose.model('Group').findOne(tavernQ, function(err,tavern){ mongoose.model('Group').findOne({_id: 'habitrpg', 'quest.key': {$ne: null}}, (err, tavern) => {
// TODO handle error?
if (!tavern) return; // No tavern quest if (!tavern) return; // No tavern quest
var quest = tavern.quest.toObject();
// Using _assign so we don't lose the reference to the exported tavernQuest // Using _assign so we don't lose the reference to the exported tavernQuest
_.assign(module.exports.tavernQuest, quest); _.assign(tavernQuest, tavern.quest.toObject());
}); });
}); });
GroupSchema.statics.tavernBoss = function(user,progress) { schema.statics.tavernBoss = function tavernBoss (user, progress) {
if (!progress) return; if (!progress) return;
// hack: prevent crazy damage to world boss // hack: prevent crazy damage to world boss
var dmg = Math.min(900, Math.abs(progress.up||0)), let dmg = Math.min(900, Math.abs(progress.up || 0));
rage = -Math.min(900, Math.abs(progress.down||0)); let rage = -Math.min(900, Math.abs(progress.down || 0));
async.waterfall([ async.waterfall([
function(cb){ function(cb){
@@ -339,12 +371,12 @@ GroupSchema.statics.tavernBoss = function(user,progress) {
} }
],function(err,res){ ],function(err,res){
if (err === true) return; // no current quest if (err === true) return; // no current quest
if (err) return logging.error(err); if (err) return logger.error(err);
dmg = rage = null; dmg = rage = null;
}) })
} }
GroupSchema.statics.bossQuest = function(user, progress, cb) { schema.statics.bossQuest = function bossQuest (user, progress, cb) {
this.findOne({type: 'party', members: {'$in': [user._id]}},function(err, group){ this.findOne({type: 'party', members: {'$in': [user._id]}},function(err, group){
if (!isOnQuest(user,progress,group)) return cb(null); if (!isOnQuest(user,progress,group)) return cb(null);
var quest = shared.content.quests[group.quest.key]; var quest = shared.content.quests[group.quest.key];
@@ -386,7 +418,7 @@ GroupSchema.statics.bossQuest = function(user, progress, cb) {
} }
// Remove user from this group // Remove user from this group
GroupSchema.methods.leave = function(user, keep, mainCb){ schema.methods.leave = function leaveGroup (user, keep, mainCb){
if(!user) return mainCb(new Error('Missing user.')); if(!user) return mainCb(new Error('Missing user.'));
if(keep && typeof keep === 'function'){ if(keep && typeof keep === 'function'){
@@ -472,22 +504,14 @@ GroupSchema.methods.leave = function(user, keep, mainCb){
}); });
}; };
export let model = mongoose.model('Group', schema);
GroupSchema.methods.toJSON = function() {
var doc = this.toObject();
return doc;
};
module.exports.schema = GroupSchema;
var Group = module.exports.model = mongoose.model("Group", GroupSchema);
// initialize tavern if !exists (fresh installs) // initialize tavern if !exists (fresh installs)
Group.count({_id: 'habitrpg'}, function(err, ct){ // TODO use promise
model.count({_id: 'habitrpg'}, (err, ct) => {
if (ct > 0) return; if (ct > 0) return;
new Group({ new model({
_id: 'habitrpg', _id: 'habitrpg',
chat: [], chat: [],
leader: '9', leader: '9',