APIv2: some finalization of APIv2. Add new routes, re-rigg

query/param/body requirements. This overhauls the API pretty
substantially, so we'll indeed be moving this to apiv2, using v1 for the
old API, and limiting it's use substantially
This commit is contained in:
Tyler Renelle
2013-12-15 14:49:22 -07:00
parent d0b07ba4db
commit 91e1287d06
7 changed files with 102 additions and 174 deletions

View File

@@ -64,7 +64,7 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', 'API_URL',
var selected = $scope.selectedEgg ? 'selectedEgg' : $scope.selectedPotion ? 'selectedPotion' : $scope.selectedFood ? 'selectedFood' : undefined;
if (selected) {
var type = $scope.selectedEgg ? 'eggs' : $scope.selectedPotion ? 'hatchingPotions' : $scope.selectedFood ? 'food' : undefined;
user.ops.sell({query:{type:type, key: $scope[selected].name}});
user.ops.sell({params:{type:type, key: $scope[selected].name}});
if (user.items[type][$scope[selected].name] < 1) {
$scope[selected] = null;
}
@@ -77,22 +77,18 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', 'API_URL',
$scope.hatch = function(egg, potion){
if (!confirm('Hatch a ' + potion.name + ' ' + egg.name + '?')) return;
user.ops.hatch({query:{egg:egg.name, hatchingPotion:potion.name}});
user.ops.hatch({params:{egg:egg.name, hatchingPotion:potion.name}});
$scope.selectedEgg = null;
$scope.selectedPotion = null;
}
$scope.buy = function(type, item){
$scope.purchase = function(type, item){
var gems = User.user.balance * 4;
if(gems < item.value) return $rootScope.modals.buyGems = true;
var string = (type == 'hatchingPotion') ? 'hatching potion' : type; // give hatchingPotion a space
var message = "Buy this " + string + " with " + item.value + " of your " + gems + " Gems?"
if(confirm(message)){
$http.post(API_URL + '/api/v1/market/buy?type=' + type, item)
.success(function(data){
User.user.items = data.items;
});
}
if(confirm(message))
User.user.ops.purchase({params:{type:type,key:item.name}});
}
$scope.choosePet = function(egg, potion){
@@ -106,17 +102,17 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', 'User', 'API_URL',
} else if (!confirm('Feed ' + pet + ' a ' + food.name + '?')) {
return;
}
User.user.ops.feed({query:{pet: pet, food: food.name}});
User.user.ops.feed({params:{pet: pet, food: food.name}});
$scope.selectedFood = null;
// Selecting Pet
} else {
User.user.ops.equip({query:{type: 'pet', key: pet}});
User.user.ops.equip({params:{type: 'pet', key: pet}});
}
}
$scope.chooseMount = function(egg, potion) {
User.user.ops.equip({query:{type: 'mount', key: egg + '-' + potion}});
User.user.ops.equip({params:{type: 'mount', key: egg + '-' + potion}});
}
}
]);

View File

@@ -45,15 +45,9 @@ habitrpg.controller('SettingsCtrl',
}
$scope.reroll = function(){
$http.post(API_URL + '/api/v1/user/reroll')
.success(function(){
window.location.href = '/';
// FIXME, I can't get the tasks to update in the browser, even with _.extend(user,data). refreshing for now
})
.error(function(data){
alert(data.err)
})
User.user.ops.reroll({});
$rootScope.modals.reroll = false;
$rootScope.$state.go('tasks');
}
$scope.changePassword = function(changePass){
@@ -90,17 +84,13 @@ habitrpg.controller('SettingsCtrl',
});
$rootScope.modals.restore = false;
}
$scope.reset = function(){
$http.post(API_URL + '/api/v1/user/reset')
.success(function(){
User.user._v--;
User.log({});
User.user.ops.reset({});
$rootScope.modals.reset = false;
})
.error(function(data){
alert(data);
});
$rootScope.$state.go('tasks');
}
$scope['delete'] = function(){
$http['delete'](API_URL + '/api/v1/user')
.success(function(){

View File

@@ -64,7 +64,7 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
$scope.unlink = function(task, keep) {
// TODO move this to userServices, turn userSerivces.user into ng-resource
$http.post(API_URL + '/api/v1/user/task/' + task.id + '/unlink?keep=' + keep)
$http.post(API_URL + '/api/v1/user/tasks/' + task.id + '/unlink?keep=' + keep)
.success(function(){
User.log({});
});
@@ -81,14 +81,8 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
})
$scope.buy = function(item) {
var hasEnough = User.user.ops.buy({query:{key:item.key}});
if (hasEnough) {
Notification.text("Item purchased.");
User.user.ops.buy({params:{key:item.key}});
$scope.itemStore = User.user.fns.updateStore();
} else {
// Notification.text("Not enough Gold!");
// handled by userServices interceptor
}
};

View File

@@ -15,23 +15,7 @@ var Challenge = require('./../models/challenge').model;
var acceptablePUTPaths;
var api = module.exports;
// FIXME put this in a proper location
api.marketBuy = function(req, res, next){
var user = res.locals.user,
type = req.query.type,
item = req.body;
if (!_.contains(['hatchingPotion', 'egg', 'food'], type))
return res.json(400, {err: "Type must be 'hatchingPotion', 'egg', or 'food'"});
type = (type == 'food' ? type : type + 's'); // I'm stupid, we're passing up 'hatchingPotion' but we need 'hatchingPotions'
if (!user.items[type][item.name]) user.items[type][item.name] = 0;
user.items[type][item.name]++;
user.balance -= (item.value/4);
user.save(function(err, saved){
if (err) return res.json(500, {err:err});
res.json(saved);
})
}
// api.purchase // Shared.ops
/*
------------------------------------------------------------------------
@@ -45,15 +29,8 @@ api.marketBuy = function(req, res, next){
---------------
*/
/*
Validate task
*/
api.verifyTaskExists = function(req, res, next) {
// If we're updating, get the task from the user
var task = res.locals.user.tasks[req.params.id];
if (_.isEmpty(task)) return res.json(400, {err: "No task found."});
res.locals.task = task;
return next();
findTask = function(req, res) {
return task = res.locals.user.tasks[req.params.id];
};
/*
@@ -66,6 +43,9 @@ api.verifyTaskExists = function(req, res, next) {
Export it also so we can call it from deprecated.coffee
*/
api.score = function(req, res, next) {
var task = findTask(req,res);
if (!task) return res.json(404, {err: "No task found."});
var id = req.params.id,
direction = req.params.direction,
user = res.locals.user,
@@ -129,29 +109,17 @@ api.getTasks = function(req, res, next) {
* Get Task
*/
api.getTask = function(req, res, next) {
var task = res.locals.user.tasks[req.params.id];
if (_.isEmpty(task)) return res.json(400, {err: "No task found."});
var task = findTask(req,res);
if (!task) return res.json(404, {err: "No task found."});
return res.json(200, task);
};
/**
* Delete Task
*/
api.deleteTask = function(req, res, next) {
api.verifyTaskExists(req, res, function(){
var user = res.locals.user;
user.deleteTask(res.locals.task.id);
user.save(function(err) {
if (err) return res.json(500, {err: err});
res.send(204);
});
})
};
/*
Update Task
*/
//api.deleteTask // see Shared.ops
// api.updateTask // handled in Shared.ops
// api.addTask // handled in Shared.ops
// api.sortTask // handled in Shared.ops #TODO updated api, mention in docs
@@ -231,44 +199,8 @@ api.cron = function(req, res, next) {
user.save(next);
};
api.reroll = function(req, res, next) {
var user = res.locals.user;
if (user.balance < 1) return res.json(401, {err: "Not enough tokens."});
user.balance -= 1;
_.each(['habits','dailys','todos'], function(type){
_.each(user[type], function(task){
task.value = 0;
})
})
user.stats.hp = 50;
user.save(function(err, saved) {
if (err) return res.json(500, {err: err});
return res.json(200, saved);
});
};
api.reset = function(req, res){
var user = res.locals.user;
user.habits = [];
user.dailys = [];
user.todos = [];
user.rewards = [];
user.stats.hp = 50;
user.stats.lvl = 1;
user.stats.gp = 0;
user.stats.exp = 0;
user.items.armor = 0;
user.items.weapon = 0;
user.items.head = 0;
user.items.shield = 0;
user.save(function(err, saved){
if (err) return res.json(500,{err:err});
res.json(saved);
})
}
// api.reroll // Shared.ops
// api.reset // Shared.ops
api['delete'] = function(req, res) {
res.locals.user.remove(function(err){

View File

@@ -266,10 +266,7 @@ var UserSchema = new Schema({
});
UserSchema.methods.deleteTask = function(tid) {
//user[t.type+'s'].id(t.id).remove();
var task = this.tasks[tid];
var i = this[task.type+'s'].indexOf(task);
if (~i) this[task.type+'s'].splice(i,1);
this.ops.deleteTask(tid); // TODO remove this whole method, since it just proxies, and change all references to this method
}
UserSchema.methods.toJSON = function() {

View File

@@ -1,3 +1,12 @@
/***
* ---------- /api/v2 API ------------
* Every url added to router is prefaced by /api/v2
* Note: Many user-route ops exist in habitrpg-shard/script/index.coffee#user.ops, so that they can (1) be called both
* client and server.
* v1 user. Requires x-api-user (user id) and x-api-key (api key) headers, Test with:
* $ mocha test/user.mocha.coffee
*/
var express = require('express');
var router = new express.Router();
var user = require('../controllers/user');
@@ -9,16 +18,6 @@ var dataexport = require('../controllers/dataexport');
var nconf = require('nconf');
var middleware = require('../middleware');
/*
---------- /api/v1 API ------------
Every url added to router is prefaced by /api/v1
See ./routes/coffee for routes
v1 user. Requires x-api-user (user id) and x-api-key (api key) headers, Test with:
$ cd node_modules/racer && npm install && cd ../..
$ mocha test/user.mocha.coffee
*/
var cron = user.cron;
router.get('/status', function(req, res) {
@@ -27,48 +26,64 @@ router.get('/status', function(req, res) {
});
});
/* Data export */
// ---------------------------------
// User
// ---------------------------------
// Data Export
router.get('/export/history',auth.auth,dataexport.history); //[todo] encode data output options in the data controller and use these to build routes
/* Scoring*/
router.post('/user/task/:id/:direction', auth.auth, cron, user.score);
// Scoring
router.post('/user/tasks/:id/:direction', auth.auth, cron, user.score);
/* Tasks*/
// Tasks
router.get('/user/tasks', auth.auth, cron, user.getTasks);
router.get('/user/task/:id', auth.auth, cron, user.getTask);
router.put('/user/task/:id', auth.auth, cron, user.updateTask);
router["delete"]('/user/task/:id', auth.auth, cron, user.deleteTask);
router.post('/user/task', auth.auth, cron, user.addTask);
router.put('/user/task/:id/sort', auth.auth, cron, user.sortTask);
router.post('/user/clear-completed', auth.auth, cron, user.clearCompleted);
router.post('/user/task/:id/unlink', auth.auth, challenges.unlink); // removing cron since they may want to remove task first
if (nconf.get('NODE_ENV') == 'development') {
router.post('/user/addTenGems', auth.auth, user.addTenGems);
}
router.get('/user/tasks/:id', auth.auth, cron, user.getTask);
router.put('/user/tasks/:id', auth.auth, cron, user.updateTask); //Shared.ops | body={}
router["delete"]('/user/tasks/:id', auth.auth, cron, user.deleteTask); //Shared.ops
router.post('/user/tasks', auth.auth, cron, user.addTask); //Shared.ops | body={}
router.post('/user/tasks/:id/sort', auth.auth, cron, user.sortTask); //Shared.ops | query={to,from}
router.post('/user/tasks/clear-completed', auth.auth, cron, user.clearCompleted); //Shared.ops
router.post('/user/tasks/:id/unlink', auth.auth, challenges.unlink); // removing cron since they may want to remove task first
/* Items*/
router.post('/user/buy/:key', auth.auth, cron, user.buy);
// Inventory
router.post('/user/inventory/buy/:key', auth.auth, cron, user.buy); // Shared.ops
router.post('/user/inventory/sell/:type/:key', auth.auth, cron, user.sell); // Shared.ops
router.post('/user/inventory/purchase/:type/:key', auth.auth, user.purchase); //Shared.ops
router.post('/user/inventory/feed/:pet/:food', auth.auth, user.feed); //Shared.ops
router.post('/user/inventory/equip/:type/:key', auth.auth, user.equip); //Shared.ops
router.post('/user/inventory/hatch/:egg/:hatchingPotion', auth.auth, user.hatch); //Shared.ops
/* User*/
// User
router.get('/user', auth.auth, cron, user.getUser);
router.put('/user', auth.auth, cron, user.update);
router.post('/user/revive', auth.auth, cron, user.revive);
router.post('/user/batch-update', middleware.forceRefresh, auth.auth, cron, user.batchUpdate);
router.post('/user/reroll', auth.auth, cron, user.reroll);
router.post('/user/buy-gems', auth.auth, user.buyGems);
router.post('/user/buy-gems/paypal-ipn', user.buyGemsPaypalIPN);
router.post('/user/unlock', auth.auth, cron, user.unlock);
router.post('/user/reset', auth.auth, user.reset);
router.post('/user/cast/:spell', auth.auth, user.cast);
router.put('/user', auth.auth, cron, user.update); // body={}
router['delete']('/user', auth.auth, user['delete']);
/* Tags */
router.post('/user/tags', auth.auth, user.addTag);
router.put('/user/tags/:id', auth.auth, user.updateTag);
router['delete']('/user/tags/:id', auth.auth, user.deleteTag);
router.post('/user/revive', auth.auth, cron, user.revive); // Shared.ops
router.post('/user/reroll', auth.auth, cron, user.reroll); // Shared.ops
router.post('/user/reset', auth.auth, user.reset); // Shared.ops
router.post('/user/sleep', auth.auth, cron, user.sleep); //Shared.opss
/* Groups*/
router.post('/user/class/change', auth.auth, cron, user.changeClass); //Shared.ops | query={class}
router.post('/user/class/allocate', auth.auth, cron, user.allocate); //Shared.ops | query={stat}
router.post('/user/class/cast/:spell', auth.auth, user.cast);
router.post('/user/unlock', auth.auth, cron, user.unlock); // Shared.ops
router.post('/user/buy-gems', auth.auth, user.buyGems);
router.post('/user/buy-gems/paypal-ipn', user.buyGemsPaypalIPN);
router.post('/user/batch-update', middleware.forceRefresh, auth.auth, cron, user.batchUpdate);
if (nconf.get('NODE_ENV') == 'development') router.post('/user/addTenGems', auth.auth, user.addTenGems);
// Tags
router.post('/user/tags', auth.auth, user.addTag); //Shared.ops | body={}
router.put('/user/tags/:id', auth.auth, user.updateTag); //Shared.ops | body={}
router['delete']('/user/tags/:id', auth.auth, user.deleteTag); //Shared.ops | body={}
// ---------------------------------
// Groups
// ---------------------------------
router.get('/groups', auth.auth, groups.list);
router.post('/groups', auth.auth, groups.create);
router.get('/groups/:gid', auth.auth, groups.get);
@@ -86,18 +101,22 @@ router.post('/groups/:gid/chat', auth.auth, groups.attachGroup, groups.postChat)
router["delete"]('/groups/:gid/chat/:messageId', auth.auth, groups.attachGroup, groups.deleteChatMessage);
//PUT /groups/:gid/chat/:messageId
/* Members */
// ---------------------------------
// Members
// ---------------------------------
router.get('/members/:uid', groups.getMember);
/* Admin */
// ---------------------------------
// Admin
// ---------------------------------
router.get('/admin/members', auth.auth, admin.ensureAdmin, admin.listMembers);
router.get('/admin/members/:uid', auth.auth, admin.ensureAdmin, admin.getMember);
router.post('/admin/members/:uid', auth.auth, admin.ensureAdmin, admin.updateMember);
// Market
router.post('/market/buy', auth.auth, user.marketBuy);
// ---------------------------------
// Challenges
// ---------------------------------
/* Challenges */
// Note: while challenges belong to groups, and would therefore make sense as a nested resource
// (eg /groups/:gid/challenges/:cid), they will also be referenced by users from the "challenges" tab
// without knowing which group they belong to. So to prevent unecessary lookups, we have them as a top-level resource

View File

@@ -36,7 +36,7 @@ script(type='text/ng-template', id='partials/options.inventory.inventory.html')
li.customize-menu
menu.pets-menu(label='{{label}}', ng-repeat='(klass,label) in {base:"Base", warrior:"Warrior", wizard:"Wizard", rogue:"Rogue", special:"Special"}', ng-show='gear[klass]')
div(ng-repeat='item in gear[klass]')
button.customize-option(popover='{{item.notes}}', popover-title='{{item.text}}', popover-trigger='mouseenter', popover-placement='right', ng-click='user.ops.equip({query:{key:item.key}})', class='shop_{{item.key}}', ng-class='{selectableInventory: user.items.gear.equipped[item.type] == item.key}')
button.customize-option(popover='{{item.notes}}', popover-title='{{item.text}}', popover-trigger='mouseenter', popover-placement='right', ng-click='user.ops.equip({params:{key:item.key}})', class='shop_{{item.key}}', ng-class='{selectableInventory: user.items.gear.equipped[item.type] == item.key}')
label.checkbox.inline
input(type="checkbox", ng-model="user.preferences.costume", ng-click='set({"preferences.costume":!user.preferences.costume})')
| Use Costume&nbsp;
@@ -44,7 +44,7 @@ script(type='text/ng-template', id='partials/options.inventory.inventory.html')
li.customize-menu(ng-if='user.preferences.costume')
menu.pets-menu(label='{{label}}', ng-repeat='(klass,label) in {base:"Base", warrior:"Warrior", wizard:"Wizard", rogue:"Rogue", special:"Special"}', ng-show='gear[klass]')
div(ng-repeat='item in gear[klass]')
button.customize-option(popover='{{item.notes}}', popover-title='{{item.text}}', popover-trigger='mouseenter', popover-placement='right', ng-click='user.ops.equip({query:{type:"costume", key:item.key}})', class='shop_{{item.key}}', ng-class='{selectableInventory: user.items.gear.costume[item.type] == item.key}')
button.customize-option(popover='{{item.notes}}', popover-title='{{item.text}}', popover-trigger='mouseenter', popover-placement='right', ng-click='user.ops.equip({params:{type:"costume", key:item.key}})', class='shop_{{item.key}}', ng-class='{selectableInventory: user.items.gear.costume[item.type] == item.key}')
.span6
h2 Market
@@ -73,7 +73,7 @@ script(type='text/ng-template', id='partials/options.inventory.inventory.html')
li.customize-menu
menu.pets-menu(label='Eggs')
div(ng-repeat='egg in Content.eggs')
button.customize-option(popover='{{egg.notes}}', popover-title='{{egg.text}} Egg', popover-trigger='mouseenter', popover-placement='left', ng-click='buy("egg", egg)', class='Pet_Egg_{{egg.name}}')
button.customize-option(popover='{{egg.notes}}', popover-title='{{egg.text}} Egg', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("eggs", egg)', class='Pet_Egg_{{egg.name}}')
p
| {{egg.value}}
span.Pet_Currency_Gem1x.inline-gems
@@ -81,7 +81,7 @@ script(type='text/ng-template', id='partials/options.inventory.inventory.html')
li.customize-menu
menu.pets-menu(label='Hatching Potions')
div(ng-repeat='pot in Content.hatchingPotions')
button.customize-option(popover='{{pot.notes}}', popover-title='{{pot.text}} Potion', popover-trigger='mouseenter', popover-placement='left', ng-click='buy("hatchingPotion", pot)', class='Pet_HatchingPotion_{{pot.name}}')
button.customize-option(popover='{{pot.notes}}', popover-title='{{pot.text}} Potion', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("hatchingPotions", pot)', class='Pet_HatchingPotion_{{pot.name}}')
p
| {{pot.value}}
span.Pet_Currency_Gem1x.inline-gems
@@ -89,13 +89,13 @@ script(type='text/ng-template', id='partials/options.inventory.inventory.html')
li.customize-menu
menu.pets-menu(label='Food')
div(ng-repeat='food in Content.food', ng-show='food.name !== "Saddle"')
button.customize-option(popover='{{food.notes}}', popover-title='{{food.text}}', popover-trigger='mouseenter', popover-placement='left', ng-click='buy("food", food)', class='Pet_Food_{{food.name}}')
button.customize-option(popover='{{food.notes}}', popover-title='{{food.text}}', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("food", food)', class='Pet_Food_{{food.name}}')
p
| {{food.value}}
span.Pet_Currency_Gem1x.inline-gems
menu.pets-menu(label='Saddle')
div
button.customize-option(popover='{{Content.food.Saddle.notes}}', popover-title='{{Content.food.Saddle.text}}', popover-trigger='mouseenter', popover-placement='left', ng-click='buy("food", Content.food.Saddle)', class='Pet_Food_{{Content.food.Saddle.name}}')
button.customize-option(popover='{{Content.food.Saddle.notes}}', popover-title='{{Content.food.Saddle.text}}', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("food", Content.food.Saddle)', class='Pet_Food_{{Content.food.Saddle.name}}')
p
| {{Content.food.Saddle.value}}
span.Pet_Currency_Gem1x.inline-gems