From afb13c3113738ba28ed2b07887f5cf42b0e1ab9a Mon Sep 17 00:00:00 2001 From: yangit Date: Thu, 13 Jun 2013 23:25:10 +0800 Subject: [PATCH 01/53] APIv2 init --- src/server/apiv2.coffee | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index 90e419e464..9bf9872b48 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -45,15 +45,27 @@ auth = (req, res, next) -> POST new actions ### router.post '/', auth, (req, res) -> + model = req.getModel() actions = req.body if _.isArray actions actions.forEach (action)-> - switch action.op - when score then - {} - when newTask then + + if action.op=="score" + misc.score(model, action.task, action.dir, true) + + if action.op=="newTask" req.user.set "tasks.#{req.task.id}", action.task + if action.op=="delTask" + model.del ("tasks."+action.task) + +# this API is only working with string or number variables. It should return error if object given or object is at the path. + if action.op=="set" + oldValue = model.get(action.path); + if (typeof action.value=="number"||typeof action.value=="string") + if (typeof oldValue=="number"||typeof oldValue=="string") + model.get(action.path,action.value) + console.log util.inspect req.body res.json 200, req.userObj From 268e377dc264f94e9a70803db6a4a2cb952413b1 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 13 Jun 2013 12:00:33 -0400 Subject: [PATCH 02/53] apiv2: model.ref('_user') before passing back to derby functions, much of our derby code requires _user --- src/server/apiv2.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index 9bf9872b48..e91dcc24f3 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -31,14 +31,14 @@ auth = (req, res, next) -> return res.json 401, NO_TOKEN_OR_UID unless uid || token model = req.getModel() - query = model.query('users').withIdAndToken(uid, token) - query.fetch (err, user) -> + model.query('users').withIdAndToken(uid, token).fetch (err, user) -> return res.json err: err if err req.user = user req.userObj = user.get() return res.json 401, NO_USER_FOUND if !req.userObj || _.isEmpty(req.userObj) req._isServer = true + model.ref('_user', user) next() ### From 236a81d3924d8306b3faffd0d962eac233040534 Mon Sep 17 00:00:00 2001 From: yangit Date: Fri, 14 Jun 2013 23:44:13 +0800 Subject: [PATCH 03/53] add some api --- src/server/apiv2.coffee | 43 +++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index e91dcc24f3..34e62573d5 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -10,8 +10,10 @@ check = validator.check sanitize = validator.sanitize misc = require '../app/misc' -NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request" -NO_USER_FOUND = err: "No user found." +NO_TOKEN_OR_UID = +err: "You must include a token and uid (user id) in your request" +NO_USER_FOUND = +err: "No user found." # ---------- /api/v1 API ------------ # Every url added beneath router is prefaced by /api/v2 @@ -47,27 +49,34 @@ POST new actions router.post '/', auth, (req, res) -> model = req.getModel() actions = req.body + console.log util.inspect req.body if _.isArray actions actions.forEach (action)-> + if action.op == "score" + if action.task.type=="daily" || action.task.type=="todo" +# flip completed state. Since checkbox is not binded to model unlike when you click through Derby website. + completed = model.get "tasks[#{action.task.id}].completed" + model.set("tasks[#{action.task.id}].completed", !completed) + misc.score(model, action.task.id, action.dir, true) - if action.op=="score" - misc.score(model, action.task, action.dir, true) + if action.op == "newTask" + req.user.set "tasks.#{req.task.id}", action.task - if action.op=="newTask" - req.user.set "tasks.#{req.task.id}", action.task + if action.op=="delTask" + model.del ("tasks."+action.task) - if action.op=="delTask" - model.del ("tasks."+action.task) + # this API is only working with string or number variables. It should return error if object given or object is at the path. + if action.op == "set" + oldValue = model.get(action.path); + if (typeof action.value == "number" || typeof action.value == "string") + if (typeof oldValue == "number" || typeof oldValue == "string") + model.get(action.path, action.value) -# this API is only working with string or number variables. It should return error if object given or object is at the path. - if action.op=="set" - oldValue = model.get(action.path); - if (typeof action.value=="number"||typeof action.value=="string") - if (typeof oldValue=="number"||typeof oldValue=="string") - model.get(action.path,action.value) - console.log util.inspect req.body - - res.json 200, req.userObj +#emulate slow\buggy API, TODO, REMOVE THIS PIECE OF CODE! + setTimeout (-> + res.json 200, req.userObj + console.log "Reply sent") + , 1000 module.exports = router \ No newline at end of file From 69ca37d3b04bf3a39c2d75d0d15067ceafe36188 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 14 Jun 2013 13:01:33 -0400 Subject: [PATCH 04/53] user.set('tasks...'), not model.set('tasks...') @yangit --- src/server/apiv2.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index 34e62573d5..02faf0d3aa 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -56,7 +56,7 @@ router.post '/', auth, (req, res) -> if action.task.type=="daily" || action.task.type=="todo" # flip completed state. Since checkbox is not binded to model unlike when you click through Derby website. completed = model.get "tasks[#{action.task.id}].completed" - model.set("tasks[#{action.task.id}].completed", !completed) + req.user.set("tasks.#{action.task.id}.completed", !completed) misc.score(model, action.task.id, action.dir, true) if action.op == "newTask" From 360a8f7d9ec6ad561a2130f26d875ac219693c44 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 14 Jun 2013 13:03:37 -0400 Subject: [PATCH 05/53] apiv2: same as before - user.get('tasks'), not model.get('tasks') --- src/server/apiv2.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index 02faf0d3aa..770dc6fcaf 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -55,7 +55,7 @@ router.post '/', auth, (req, res) -> if action.op == "score" if action.task.type=="daily" || action.task.type=="todo" # flip completed state. Since checkbox is not binded to model unlike when you click through Derby website. - completed = model.get "tasks[#{action.task.id}].completed" + completed = req.user.get "tasks.#{action.task.id}.completed" req.user.set("tasks.#{action.task.id}.completed", !completed) misc.score(model, action.task.id, action.dir, true) From a549d1d550be8f8431486e3fc77cfd2ebdff54f9 Mon Sep 17 00:00:00 2001 From: yangit Date: Mon, 17 Jun 2013 18:24:24 +0800 Subject: [PATCH 06/53] Add, edit, delete task\habit\reward. --- src/server/apiv2.coffee | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index 770dc6fcaf..a916fc69ba 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -48,35 +48,44 @@ POST new actions ### router.post '/', auth, (req, res) -> model = req.getModel() + user = req.user actions = req.body console.log util.inspect req.body + + _.each ['habit', 'daily', 'todo', 'reward'], (type) -> + model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" + if _.isArray actions actions.forEach (action)-> if action.op == "score" if action.task.type=="daily" || action.task.type=="todo" -# flip completed state. Since checkbox is not binded to model unlike when you click through Derby website. - completed = req.user.get "tasks.#{action.task.id}.completed" - req.user.set("tasks.#{action.task.id}.completed", !completed) +# switch completed state. Since checkbox is not binded to model unlike when you click through Derby website. + completed = if action.dir=="up" then true else false + user.set("tasks.#{action.task.id}.completed", completed) misc.score(model, action.task.id, action.dir, true) - if action.op == "newTask" - req.user.set "tasks.#{req.task.id}", action.task + if action.op == "addTask" + model.unshift "_#{action.task.type}List", action.task if action.op=="delTask" - model.del ("tasks."+action.task) + ids = user.get('habitIds') + ids.splice(ids.indexOf(action.task.id),1); + user.set('habitIds',ids) + user.del ("tasks."+action.task.id) + # this API is only working with string or number variables. It should return error if object given or object is at the path. if action.op == "set" - oldValue = model.get(action.path); + oldValue = user.get(action.path); if (typeof action.value == "number" || typeof action.value == "string") if (typeof oldValue == "number" || typeof oldValue == "string") - model.get(action.path, action.value) + user.set(action.path, action.value) #emulate slow\buggy API, TODO, REMOVE THIS PIECE OF CODE! setTimeout (-> - res.json 200, req.userObj + res.json 200, misc.hydrate(user.get()) console.log "Reply sent") - , 1000 + , 3000 module.exports = router \ No newline at end of file From da8a67e0fa01848945e020fae5ec8b775a0c839a Mon Sep 17 00:00:00 2001 From: yangit Date: Mon, 17 Jun 2013 19:41:52 +0800 Subject: [PATCH 07/53] transform user structure FROM user.tasks{} + user.habitIds[] TO user.habits[] + user.todos[] etc. --- src/server/apiv2.coffee | 43 +++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index a916fc69ba..b72ee82fb8 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -11,9 +11,9 @@ sanitize = validator.sanitize misc = require '../app/misc' NO_TOKEN_OR_UID = -err: "You must include a token and uid (user id) in your request" + err: "You must include a token and uid (user id) in your request" NO_USER_FOUND = -err: "No user found." + err: "No user found." # ---------- /api/v1 API ------------ # Every url added beneath router is prefaced by /api/v2 @@ -57,21 +57,25 @@ router.post '/', auth, (req, res) -> if _.isArray actions actions.forEach (action)-> + task = {} + if action.task? then task = action.task + if action.op == "score" - if action.task.type=="daily" || action.task.type=="todo" + if task.type == "daily" || task.type == "todo" # switch completed state. Since checkbox is not binded to model unlike when you click through Derby website. - completed = if action.dir=="up" then true else false - user.set("tasks.#{action.task.id}.completed", completed) - misc.score(model, action.task.id, action.dir, true) + completed = if action.dir == "up" then true else false + user.set("tasks.#{task.id}.completed", completed) + misc.score(model, task.id, action.dir, true) if action.op == "addTask" - model.unshift "_#{action.task.type}List", action.task + model.unshift "_#{task.type}List", task - if action.op=="delTask" - ids = user.get('habitIds') - ids.splice(ids.indexOf(action.task.id),1); - user.set('habitIds',ids) - user.del ("tasks."+action.task.id) + if action.op == "delTask" + ids = user.get(task.type + 'Ids') + console.log util.inspect ids + ids.splice(ids.indexOf(task.id), 1); + user.set(task.type + 'Ids', ids) + user.del ("tasks." + task.id) # this API is only working with string or number variables. It should return error if object given or object is at the path. @@ -82,10 +86,19 @@ router.post '/', auth, (req, res) -> user.set(action.path, action.value) -#emulate slow\buggy API, TODO, REMOVE THIS PIECE OF CODE! + #emulate slow\buggy API, TODO, REMOVE THIS PIECE OF CODE! setTimeout (-> - res.json 200, misc.hydrate(user.get()) + user = misc.hydrate user.get() + + #transform user structure FROM user.tasks{} + user.habitIds[] TO user.habits[] + user.todos[] etc. + ["habit", "daily", "todo", "reward"].forEach (type) -> + user[type + 's'] = [] + user[type + 'Ids'].forEach (id)-> + user[type + 's'].push(user.tasks[id]) + delete user[type + 'Ids'] + delete user.tasks + res.json 200, user console.log "Reply sent") - , 3000 + , 1000 module.exports = router \ No newline at end of file From 49d0214056a21b76848f5d0e93be19067cb2431f Mon Sep 17 00:00:00 2001 From: yangit Date: Sat, 6 Jul 2013 16:03:51 +0800 Subject: [PATCH 08/53] added sort API, removed debug code. --- src/server/apiv2.coffee | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index b72ee82fb8..3afa459b88 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -52,6 +52,7 @@ router.post '/', auth, (req, res) -> actions = req.body console.log util.inspect req.body + _.each ['habit', 'daily', 'todo', 'reward'], (type) -> model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" @@ -59,7 +60,7 @@ router.post '/', auth, (req, res) -> actions.forEach (action)-> task = {} if action.task? then task = action.task - + if action.op == "score" if task.type == "daily" || task.type == "todo" # switch completed state. Since checkbox is not binded to model unlike when you click through Derby website. @@ -67,14 +68,21 @@ router.post '/', auth, (req, res) -> user.set("tasks.#{task.id}.completed", completed) misc.score(model, task.id, action.dir, true) + if action.op == "sortTask" + a=user.get("tasks." + action.type + "Ids") + a.splice(action.to, 0, a.splice(action.from, 1)[0]) + user.set("tasks." + action.type + "Ids", a) + if action.op == "addTask" model.unshift "_#{task.type}List", task if action.op == "delTask" +# to make sure we update DOM on Derby client ids = user.get(task.type + 'Ids') - console.log util.inspect ids ids.splice(ids.indexOf(task.id), 1); user.set(task.type + 'Ids', ids) + + # Actually delete the task user.del ("tasks." + task.id) @@ -86,19 +94,16 @@ router.post '/', auth, (req, res) -> user.set(action.path, action.value) - #emulate slow\buggy API, TODO, REMOVE THIS PIECE OF CODE! - setTimeout (-> - user = misc.hydrate user.get() + user = misc.hydrate user.get() - #transform user structure FROM user.tasks{} + user.habitIds[] TO user.habits[] + user.todos[] etc. - ["habit", "daily", "todo", "reward"].forEach (type) -> - user[type + 's'] = [] - user[type + 'Ids'].forEach (id)-> - user[type + 's'].push(user.tasks[id]) - delete user[type + 'Ids'] - delete user.tasks - res.json 200, user - console.log "Reply sent") - , 1000 + #transform user structure FROM user.tasks{} + user.habitIds[] TO user.habits[] + user.todos[] etc. + ["habit", "daily", "todo", "reward"].forEach (type) -> + user[type + 's'] = [] + user[type + 'Ids'].forEach (id)-> + user[type + 's'].push(user.tasks[id]) + delete user[type + 'Ids'] + delete user.tasks + res.json 200, user + console.log "Reply sent" module.exports = router \ No newline at end of file From a9dd42cda551df72e10883476300b51455618174 Mon Sep 17 00:00:00 2001 From: yangit Date: Sat, 6 Jul 2013 20:03:05 +0800 Subject: [PATCH 09/53] fix sort API --- src/server/apiv2.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index 3afa459b88..820752429f 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -69,9 +69,10 @@ router.post '/', auth, (req, res) -> misc.score(model, task.id, action.dir, true) if action.op == "sortTask" - a=user.get("tasks." + action.type + "Ids") + path = action.task.type + "Ids" + a=user.get(path) a.splice(action.to, 0, a.splice(action.from, 1)[0]) - user.set("tasks." + action.type + "Ids", a) + user.set(path, a) if action.op == "addTask" model.unshift "_#{task.type}List", task From 50918ffea89d2b22540a2129d8fc6a467ba6f83b Mon Sep 17 00:00:00 2001 From: yangit Date: Fri, 12 Jul 2013 00:36:06 +0800 Subject: [PATCH 10/53] added cron update on each transaction. --- src/server/apiv2.coffee | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index 820752429f..5bfd446ef0 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -53,6 +53,13 @@ router.post '/', auth, (req, res) -> console.log util.inspect req.body + misc.batchTxn model, (uObj, paths) -> + # habitrpg-shared/algos requires uObj.habits, uObj.dailys etc instead of uObj.tasks + _.each ['habit','daily','todo','reward'], (type) -> uObj["#{type}s"] = _.where(uObj.tasks, {type}); true + algos.cron uObj, {paths} + ,{cron:true} + + _.each ['habit', 'daily', 'todo', 'reward'], (type) -> model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" From 4655b2fbc2b02661d7d45d78960914d5d173dc1a Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 19 Jul 2013 15:19:50 -0400 Subject: [PATCH 11/53] apiv2: remove some duplcation from api to apiv2 --- src/server/api.coffee | 2 ++ src/server/apiv2.coffee | 29 +++-------------------------- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 48c812dbb2..2539a5bdbd 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -316,3 +316,5 @@ router.post '/user/tasks/:taskId/:direction', auth, scoreTask module.exports = router module.exports.auth = auth module.exports.scoreTask = scoreTask # export so deprecated can call it +module.exports.NO_TOKEN_OR_UID = NO_TOKEN_OR_UID +module.exports.NO_USER_FOUND = NO_USER_FOUND diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index 6138f78bc6..84ebd0de2d 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -9,13 +9,9 @@ validator = require 'derby-auth/node_modules/validator' check = validator.check sanitize = validator.sanitize misc = require '../app/misc' +api = require './api' -NO_TOKEN_OR_UID = - err: "You must include a token and uid (user id) in your request" -NO_USER_FOUND = - err: "No user found." - -# ---------- /api/v1 API ------------ +# ---------- /api/v2 API ------------ # Every url added beneath router is prefaced by /api/v2 ### @@ -24,29 +20,10 @@ NO_USER_FOUND = router.get '/status', (req, res) -> res.json status: 'up' -### - beforeEach auth interceptor -### -auth = (req, res, next) -> - uid = req.headers['x-api-user'] - token = req.headers['x-api-key'] - return res.json 401, NO_TOKEN_OR_UID unless uid || token - - model = req.getModel() - - model.query('users').withIdAndToken(uid, token).fetch (err, user) -> - return res.json err: err if err - req.user = user - req.userObj = user.get() - return res.json 401, NO_USER_FOUND if !req.userObj || _.isEmpty(req.userObj) - req._isServer = true - model.ref('_user', user) - next() - ### POST new actions ### -router.post '/', auth, (req, res) -> +router.post '/', api.auth, (req, res) -> model = req.getModel() user = req.user actions = req.body From b88c2ad1a39fef4335634d63caad734bde77ca87 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 19 Jul 2013 16:53:44 -0400 Subject: [PATCH 12/53] apiv2 - much cleaned up, by re-using fucntions in api.coffee. misc.batchTxn takes an optional `user` param, in case we don't want to use private paths "_user" (which we don't want to do on the server!) --- src/app/misc.coffee | 3 +- src/server/api.coffee | 42 ++++++++++-------- src/server/apiv2.coffee | 96 +++++++++++++++++++---------------------- 3 files changed, 71 insertions(+), 70 deletions(-) diff --git a/src/app/misc.coffee b/src/app/misc.coffee index ce14ca9f2a..ee3ad2c173 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -4,7 +4,7 @@ items = require('habitrpg-shared/script/items').items helpers = require('habitrpg-shared/script/helpers') module.exports.batchTxn = batchTxn = (model, cb, options) -> - user = model.at("_user") + user = options?.user or model.at("_user") uObj = hydrate(user.get()) # see https://github.com/codeparty/racer/issues/116 batch = set: (k,v) -> helpers.dotSet(k,v,uObj); paths[k] = true @@ -19,6 +19,7 @@ module.exports.batchTxn = batchTxn = (model, cb, options) -> unless _.isEmpty paths setOps = _.reduce paths, ((m,v,k)-> m[k] = helpers.dotGet(k,uObj);m), {} user.set "update__", setOps, options?.done + else options?.done?() ret #TODO put this in habitrpg-shared diff --git a/src/server/api.coffee b/src/server/api.coffee index 2539a5bdbd..4d8bd79c4f 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -12,10 +12,23 @@ utils = require 'derby-auth/utils' NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request" NO_USER_FOUND = err: "No user found." -addTask = (user, task) -> +addTask = (user, task, cb) -> task.type ?= 'habit' - tid = user.add "tasks", task - user.push "#{task.type}Ids", tid + tid = user.add "tasks", task, -> + user.push "#{task.type}Ids", tid, cb + +deleteTask = (user, task, cb) -> + user.del "tasks.#{task.id}", -> + taskIds = user.get "#{task.type}Ids" + user.remove "#{task.type}Ids", taskIds.indexOf(task.id), 1, cb + +score = (model, user, taskId, direction, cb) -> + delta = 0 + misc.batchTxn model, (uObj, paths) -> + tObj = uObj.tasks[taskId] + delta = algos.score(uObj, tObj, direction, {paths}) + , {user, cb} + delta # ---------- /api/v1 API ------------ # Every url added beneath router is prefaced by /api/v1 @@ -202,12 +215,7 @@ router.put '/user/task/:id', auth, validateTask, (req, res) -> DELETE /user/task/:id ### router.delete '/user/task/:id', auth, validateTask, (req, res) -> - taskIds = req.user.get "#{req.task.type}Ids" - - req.user.del "tasks.#{req.task.id}" - # Remove one id from array of typeIds - req.user.remove "#{req.task.type}Ids", taskIds.indexOf(req.task.id), 1 - + deleteTask user, req.task.type, req.task.id res.send 204 ### @@ -297,15 +305,10 @@ scoreTask = (req, res, next) -> addTask user, task # TODO - could modify batchTxn to conform to this better - uObj = req.user.get() - tObj = uObj.tasks[taskId] - paths = {} - delta = algos.score(uObj, tObj, direction, {paths}) - _.each paths, (v,k) -> user.set(k,helpers.dotGet(k, uObj));true - - result = uObj.stats - result.delta = delta - res.json result + delta = score model, req.user, taskId, direction, -> + result = user.get('stats') + result.delta = delta + res.json result ### POST /user/tasks/:taskId/:direction @@ -318,3 +321,6 @@ module.exports.auth = auth module.exports.scoreTask = scoreTask # export so deprecated can call it module.exports.NO_TOKEN_OR_UID = NO_TOKEN_OR_UID module.exports.NO_USER_FOUND = NO_USER_FOUND +module.exports.addTask = addTask +module.exports.score = score +module.exports.deleteTask = deleteTask diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index 84ebd0de2d..33d2db0108 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -1,6 +1,6 @@ express = require 'express' router = new express.Router() -util = require('util') +util = require 'util' _ = require 'lodash' algos = require 'habitrpg-shared/script/algos' @@ -23,71 +23,65 @@ router.get '/status', (req, res) -> ### POST new actions ### -router.post '/', api.auth, (req, res) -> +router.post '/', api.auth, (req, res, next) -> model = req.getModel() - user = req.user + {user} = req actions = req.body - console.log util.inspect req.body + #console.log util.inspect req.body + doneCount = 0 + done = (err) -> + return next(err) if err + if --doneCount is 0 + uObj = misc.hydrate user.get() + #transform user structure FROM user.tasks{} + user.habitIds[] TO user.habits[] + user.todos[] etc. + _.each ['habit','daily','todo','reward'], (type) -> + uObj["#{type}s"] = _.where(uObj.tasks, {type}); true + delete uObj["#{type}Ids"] + delete uObj.tasks + res.json 200, uObj + console.log "Reply sent" misc.batchTxn model, (uObj, paths) -> - # habitrpg-shared/algos requires uObj.habits, uObj.dailys etc instead of uObj.tasks + doneCount++ + # habitrpg-shared/algos requires uObj.habits, uObj.dailys etc instead of uObj.tasks _.each ['habit','daily','todo','reward'], (type) -> uObj["#{type}s"] = _.where(uObj.tasks, {type}); true algos.cron uObj, {paths} - ,{cron:true} - - - _.each ['habit', 'daily', 'todo', 'reward'], (type) -> - model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" + , {user, done, cron:true} if _.isArray actions actions.forEach (action)-> - task = {} - if action.task? then task = action.task + doneCount++ - if action.op == "score" - if task.type == "daily" || task.type == "todo" -# switch completed state. Since checkbox is not binded to model unlike when you click through Derby website. - completed = if action.dir == "up" then true else false - user.set("tasks.#{task.id}.completed", completed) - misc.score(model, task.id, action.dir, true) + task = action.task ? {} - if action.op == "sortTask" - path = action.task.type + "Ids" - a=user.get(path) - a.splice(action.to, 0, a.splice(action.from, 1)[0]) - user.set(path, a) + switch action.op + when "score" + if task.type in ["daily","todo"] + # switch completed state. Since checkbox is not binded to model unlike when you click through Derby website. + completed = if action.dir is "up" then true else false + user.set "tasks.#{task.id}.completed", completed, done + doneCount++ + api.score model, user, task.id, action.dir, done - if action.op == "addTask" - model.unshift "_#{task.type}List", task + when "sortTask" + path = action.task.type + "Ids" + a = user.get(path) + a.splice(action.to, 0, a.splice(action.from, 1)[0]) + user.set(path, a) - if action.op == "delTask" -# to make sure we update DOM on Derby client - ids = user.get(task.type + 'Ids') - ids.splice(ids.indexOf(task.id), 1); - user.set(task.type + 'Ids', ids) + when "addTask" + api.addTask user, task, done - # Actually delete the task - user.del ("tasks." + task.id) + when "delTask" + api.deleteTask user, task, done - - # this API is only working with string or number variables. It should return error if object given or object is at the path. - if action.op == "set" - oldValue = user.get(action.path); - if (typeof action.value == "number" || typeof action.value == "string") - if (typeof oldValue == "number" || typeof oldValue == "string") - user.set(action.path, action.value) - - user = misc.hydrate user.get() - - #transform user structure FROM user.tasks{} + user.habitIds[] TO user.habits[] + user.todos[] etc. - ["habit", "daily", "todo", "reward"].forEach (type) -> - user[type + 's'] = [] - user[type + 'Ids'].forEach (id)-> - user[type + 's'].push(user.tasks[id]) - delete user[type + 'Ids'] - delete user.tasks - res.json 200, user - console.log "Reply sent" + # this API is only working with string or number variables. It should return error if object given or object is at the path. + when "set" + oldValue = user.get(action.path) + if _.isObject(action.value) or _.isObject(oldValue) + console.error "action.value was an object, which isn't currently supported. Tyler - double check this" + else + user.set action.path, action.value, done module.exports = router \ No newline at end of file From 592b8901374ab392a58c12d3b2cbd62c9d73edc5 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 19 Jul 2013 17:27:29 -0400 Subject: [PATCH 13/53] add in missing `require('misc')` --- src/server/api.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/api.coffee b/src/server/api.coffee index 4d8bd79c4f..9ccaebda24 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -8,6 +8,7 @@ validator = require 'derby-auth/node_modules/validator' check = validator.check sanitize = validator.sanitize utils = require 'derby-auth/utils' +misc = require '../app/misc' NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request" NO_USER_FOUND = err: "No user found." From 0c4efa3f8c2cd691f8330aa5c3a18a6ed08361df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomaz=CC=8C?= Date: Sat, 20 Jul 2013 03:23:41 +0200 Subject: [PATCH 14/53] fixes --- src/server/api.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/api.coffee b/src/server/api.coffee index 4d8bd79c4f..9ccaebda24 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -8,6 +8,7 @@ validator = require 'derby-auth/node_modules/validator' check = validator.check sanitize = validator.sanitize utils = require 'derby-auth/utils' +misc = require '../app/misc' NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request" NO_USER_FOUND = err: "No user found." From 01120a2b239d9a7eea7ef6dd11051b9291f9a014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomaz=CC=8C?= Date: Tue, 23 Jul 2013 14:33:58 +0200 Subject: [PATCH 15/53] no message --- src/server/apiv2.coffee | 127 +++++++++++++++++++++++++--------------- 1 file changed, 79 insertions(+), 48 deletions(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index 33d2db0108..2b13bd964f 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -1,6 +1,6 @@ express = require 'express' router = new express.Router() -util = require 'util' +util = require('util') _ = require 'lodash' algos = require 'habitrpg-shared/script/algos' @@ -9,9 +9,13 @@ validator = require 'derby-auth/node_modules/validator' check = validator.check sanitize = validator.sanitize misc = require '../app/misc' -api = require './api' -# ---------- /api/v2 API ------------ +NO_TOKEN_OR_UID = + err: "You must include a token and uid (user id) in your request" +NO_USER_FOUND = + err: "No user found." + +# ---------- /api/v1 API ------------ # Every url added beneath router is prefaced by /api/v2 ### @@ -20,68 +24,95 @@ api = require './api' router.get '/status', (req, res) -> res.json status: 'up' +### + beforeEach auth interceptor +### +auth = (req, res, next) -> + uid = req.headers['x-api-user'] + token = req.headers['x-api-key'] + console.log uid, token + return res.json 401, NO_TOKEN_OR_UID unless uid || token + + model = req.getModel() + + model.query('users').withIdAndToken(uid, token).fetch (err, user) -> + return res.json err: err if err + req.user = user + req.userObj = user.get() + return res.json 401, NO_USER_FOUND if !req.userObj || _.isEmpty(req.userObj) + req._isServer = true + model.ref('_user', user) + next() + ### POST new actions ### -router.post '/', api.auth, (req, res, next) -> +router.post '/', auth, (req, res) -> + console.log 'prisel sem notri' model = req.getModel() - {user} = req + user = req.user actions = req.body - #console.log util.inspect req.body + console.log util.inspect req.body - doneCount = 0 - done = (err) -> - return next(err) if err - if --doneCount is 0 - uObj = misc.hydrate user.get() - #transform user structure FROM user.tasks{} + user.habitIds[] TO user.habits[] + user.todos[] etc. - _.each ['habit','daily','todo','reward'], (type) -> - uObj["#{type}s"] = _.where(uObj.tasks, {type}); true - delete uObj["#{type}Ids"] - delete uObj.tasks - res.json 200, uObj - console.log "Reply sent" misc.batchTxn model, (uObj, paths) -> - doneCount++ - # habitrpg-shared/algos requires uObj.habits, uObj.dailys etc instead of uObj.tasks + # habitrpg-shared/algos requires uObj.habits, uObj.dailys etc instead of uObj.tasks _.each ['habit','daily','todo','reward'], (type) -> uObj["#{type}s"] = _.where(uObj.tasks, {type}); true algos.cron uObj, {paths} - , {user, done, cron:true} + ,{cron:true} + + + _.each ['habit', 'daily', 'todo', 'reward'], (type) -> + model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" if _.isArray actions actions.forEach (action)-> - doneCount++ + task = {} + if action.task? then task = action.task - task = action.task ? {} + if action.op == "score" + if task.type == "daily" || task.type == "todo" +# switch completed state. Since checkbox is not binded to model unlike when you click through Derby website. + completed = if action.dir == "up" then true else false + user.set("tasks.#{task.id}.completed", completed) + misc.score(model, task.id, action.dir, true) - switch action.op - when "score" - if task.type in ["daily","todo"] - # switch completed state. Since checkbox is not binded to model unlike when you click through Derby website. - completed = if action.dir is "up" then true else false - user.set "tasks.#{task.id}.completed", completed, done - doneCount++ - api.score model, user, task.id, action.dir, done + if action.op == "sortTask" + path = action.task.type + "Ids" + a=user.get(path) + a.splice(action.to, 0, a.splice(action.from, 1)[0]) + user.set(path, a) - when "sortTask" - path = action.task.type + "Ids" - a = user.get(path) - a.splice(action.to, 0, a.splice(action.from, 1)[0]) - user.set(path, a) + if action.op == "addTask" + model.unshift "_#{task.type}List", task - when "addTask" - api.addTask user, task, done + if action.op == "delTask" +# to make sure we update DOM on Derby client + ids = user.get(task.type + 'Ids') + ids.splice(ids.indexOf(task.id), 1); + user.set(task.type + 'Ids', ids) - when "delTask" - api.deleteTask user, task, done + # Actually delete the task + user.del ("tasks." + task.id) - # this API is only working with string or number variables. It should return error if object given or object is at the path. - when "set" - oldValue = user.get(action.path) - if _.isObject(action.value) or _.isObject(oldValue) - console.error "action.value was an object, which isn't currently supported. Tyler - double check this" - else - user.set action.path, action.value, done -module.exports = router \ No newline at end of file + # this API is only working with string or number variables. It should return error if object given or object is at the path. + if action.op == "set" + oldValue = user.get(action.path); + if typeof action.value != 'object' + user.set(action.path, action.value) + + + user = misc.hydrate user.get() + + #transform user structure FROM user.tasks{} + user.habitIds[] TO user.habits[] + user.todos[] etc. + ["habit", "daily", "todo", "reward"].forEach (type) -> + user[type + 's'] = [] + user[type + 'Ids'].forEach (id)-> + user[type + 's'].push(user.tasks[id]) + delete user[type + 'Ids'] + delete user.tasks + res.json 200, user + console.log "Reply sent" + +module.exports = router From 42e32dfb21e7f8a41693cc80400e1b8218911155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomaz=CC=8C?= Date: Fri, 26 Jul 2013 13:22:57 +0200 Subject: [PATCH 16/53] reverting the api since its not ok --- src/server/apiv2.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index 2b13bd964f..c836be7d18 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -48,7 +48,6 @@ auth = (req, res, next) -> POST new actions ### router.post '/', auth, (req, res) -> - console.log 'prisel sem notri' model = req.getModel() user = req.user actions = req.body From 0ae0c57858e2273851d4c05c6e9d696ad77f3dcb Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 26 Jul 2013 14:52:21 -0400 Subject: [PATCH 17/53] apiv2: reapplying cleanup fix of b88c2ad --- src/server/api.coffee | 6 +- src/server/apiv2.coffee | 126 +++++++++++++++------------------------- 2 files changed, 51 insertions(+), 81 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 9ccaebda24..f8fb804930 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -28,7 +28,7 @@ score = (model, user, taskId, direction, cb) -> misc.batchTxn model, (uObj, paths) -> tObj = uObj.tasks[taskId] delta = algos.score(uObj, tObj, direction, {paths}) - , {user, cb} + , {user, done:cb} delta # ---------- /api/v1 API ------------ @@ -216,7 +216,7 @@ router.put '/user/task/:id', auth, validateTask, (req, res) -> DELETE /user/task/:id ### router.delete '/user/task/:id', auth, validateTask, (req, res) -> - deleteTask user, req.task.type, req.task.id + deleteTask req.user, req.task.type, req.task.id res.send 204 ### @@ -306,7 +306,7 @@ scoreTask = (req, res, next) -> addTask user, task # TODO - could modify batchTxn to conform to this better - delta = score model, req.user, taskId, direction, -> + delta = score model, user, taskId, direction, -> result = user.get('stats') result.delta = delta res.json result diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index c836be7d18..33d2db0108 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -1,6 +1,6 @@ express = require 'express' router = new express.Router() -util = require('util') +util = require 'util' _ = require 'lodash' algos = require 'habitrpg-shared/script/algos' @@ -9,13 +9,9 @@ validator = require 'derby-auth/node_modules/validator' check = validator.check sanitize = validator.sanitize misc = require '../app/misc' +api = require './api' -NO_TOKEN_OR_UID = - err: "You must include a token and uid (user id) in your request" -NO_USER_FOUND = - err: "No user found." - -# ---------- /api/v1 API ------------ +# ---------- /api/v2 API ------------ # Every url added beneath router is prefaced by /api/v2 ### @@ -24,94 +20,68 @@ NO_USER_FOUND = router.get '/status', (req, res) -> res.json status: 'up' -### - beforeEach auth interceptor -### -auth = (req, res, next) -> - uid = req.headers['x-api-user'] - token = req.headers['x-api-key'] - console.log uid, token - return res.json 401, NO_TOKEN_OR_UID unless uid || token - - model = req.getModel() - - model.query('users').withIdAndToken(uid, token).fetch (err, user) -> - return res.json err: err if err - req.user = user - req.userObj = user.get() - return res.json 401, NO_USER_FOUND if !req.userObj || _.isEmpty(req.userObj) - req._isServer = true - model.ref('_user', user) - next() - ### POST new actions ### -router.post '/', auth, (req, res) -> +router.post '/', api.auth, (req, res, next) -> model = req.getModel() - user = req.user + {user} = req actions = req.body - console.log util.inspect req.body + #console.log util.inspect req.body + doneCount = 0 + done = (err) -> + return next(err) if err + if --doneCount is 0 + uObj = misc.hydrate user.get() + #transform user structure FROM user.tasks{} + user.habitIds[] TO user.habits[] + user.todos[] etc. + _.each ['habit','daily','todo','reward'], (type) -> + uObj["#{type}s"] = _.where(uObj.tasks, {type}); true + delete uObj["#{type}Ids"] + delete uObj.tasks + res.json 200, uObj + console.log "Reply sent" misc.batchTxn model, (uObj, paths) -> - # habitrpg-shared/algos requires uObj.habits, uObj.dailys etc instead of uObj.tasks + doneCount++ + # habitrpg-shared/algos requires uObj.habits, uObj.dailys etc instead of uObj.tasks _.each ['habit','daily','todo','reward'], (type) -> uObj["#{type}s"] = _.where(uObj.tasks, {type}); true algos.cron uObj, {paths} - ,{cron:true} - - - _.each ['habit', 'daily', 'todo', 'reward'], (type) -> - model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids" + , {user, done, cron:true} if _.isArray actions actions.forEach (action)-> - task = {} - if action.task? then task = action.task + doneCount++ - if action.op == "score" - if task.type == "daily" || task.type == "todo" -# switch completed state. Since checkbox is not binded to model unlike when you click through Derby website. - completed = if action.dir == "up" then true else false - user.set("tasks.#{task.id}.completed", completed) - misc.score(model, task.id, action.dir, true) + task = action.task ? {} - if action.op == "sortTask" - path = action.task.type + "Ids" - a=user.get(path) - a.splice(action.to, 0, a.splice(action.from, 1)[0]) - user.set(path, a) + switch action.op + when "score" + if task.type in ["daily","todo"] + # switch completed state. Since checkbox is not binded to model unlike when you click through Derby website. + completed = if action.dir is "up" then true else false + user.set "tasks.#{task.id}.completed", completed, done + doneCount++ + api.score model, user, task.id, action.dir, done - if action.op == "addTask" - model.unshift "_#{task.type}List", task + when "sortTask" + path = action.task.type + "Ids" + a = user.get(path) + a.splice(action.to, 0, a.splice(action.from, 1)[0]) + user.set(path, a) - if action.op == "delTask" -# to make sure we update DOM on Derby client - ids = user.get(task.type + 'Ids') - ids.splice(ids.indexOf(task.id), 1); - user.set(task.type + 'Ids', ids) + when "addTask" + api.addTask user, task, done - # Actually delete the task - user.del ("tasks." + task.id) + when "delTask" + api.deleteTask user, task, done + # this API is only working with string or number variables. It should return error if object given or object is at the path. + when "set" + oldValue = user.get(action.path) + if _.isObject(action.value) or _.isObject(oldValue) + console.error "action.value was an object, which isn't currently supported. Tyler - double check this" + else + user.set action.path, action.value, done - # this API is only working with string or number variables. It should return error if object given or object is at the path. - if action.op == "set" - oldValue = user.get(action.path); - if typeof action.value != 'object' - user.set(action.path, action.value) - - - user = misc.hydrate user.get() - - #transform user structure FROM user.tasks{} + user.habitIds[] TO user.habits[] + user.todos[] etc. - ["habit", "daily", "todo", "reward"].forEach (type) -> - user[type + 's'] = [] - user[type + 'Ids'].forEach (id)-> - user[type + 's'].push(user.tasks[id]) - delete user[type + 'Ids'] - delete user.tasks - res.json 200, user - console.log "Reply sent" - -module.exports = router +module.exports = router \ No newline at end of file From 9e2ef95d84bb63c699da92ba18870bb856a3282c Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Fri, 26 Jul 2013 16:35:28 -0400 Subject: [PATCH 18/53] apiv2: bugfix in handling doneCount (we really should move to Q instead) --- src/server/apiv2.coffee | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index 33d2db0108..80aa725519 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -27,9 +27,9 @@ router.post '/', api.auth, (req, res, next) -> model = req.getModel() {user} = req actions = req.body - #console.log util.inspect req.body - doneCount = 0 + doneCount = 1 + # cron + actions.length # standard operations done = (err) -> return next(err) if err if --doneCount is 0 @@ -43,7 +43,6 @@ router.post '/', api.auth, (req, res, next) -> console.log "Reply sent" misc.batchTxn model, (uObj, paths) -> - doneCount++ # habitrpg-shared/algos requires uObj.habits, uObj.dailys etc instead of uObj.tasks _.each ['habit','daily','todo','reward'], (type) -> uObj["#{type}s"] = _.where(uObj.tasks, {type}); true algos.cron uObj, {paths} @@ -51,24 +50,23 @@ router.post '/', api.auth, (req, res, next) -> if _.isArray actions actions.forEach (action)-> - doneCount++ task = action.task ? {} switch action.op when "score" + sendScore = -> api.score(model, user, task.id, action.dir, done) if task.type in ["daily","todo"] # switch completed state. Since checkbox is not binded to model unlike when you click through Derby website. completed = if action.dir is "up" then true else false - user.set "tasks.#{task.id}.completed", completed, done - doneCount++ - api.score model, user, task.id, action.dir, done + user.set "tasks.#{task.id}.completed", completed, sendScore + else sendScore() when "sortTask" path = action.task.type + "Ids" a = user.get(path) a.splice(action.to, 0, a.splice(action.from, 1)[0]) - user.set(path, a) + user.set path, a, done when "addTask" api.addTask user, task, done @@ -84,4 +82,6 @@ router.post '/', api.auth, (req, res, next) -> else user.set action.path, action.value, done + else done() + module.exports = router \ No newline at end of file From 8760d0721596ae7ea87a2e4cc33f9a35167d2d42 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 3 Aug 2013 14:06:57 -0400 Subject: [PATCH 19/53] apiv2 fix to pending op (@litenull) --- src/server/apiv2.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index 80aa725519..a538ff8f52 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -28,8 +28,9 @@ router.post '/', api.auth, (req, res, next) -> {user} = req actions = req.body + doneCount = 1 + # cron - actions.length # standard operations + _.size(actions) # standard operations done = (err) -> return next(err) if err if --doneCount is 0 @@ -42,6 +43,8 @@ router.post '/', api.auth, (req, res, next) -> res.json 200, uObj console.log "Reply sent" + if _.isObject(actions) and _.isEmpty(actions) then done() # first request, empty object + misc.batchTxn model, (uObj, paths) -> # habitrpg-shared/algos requires uObj.habits, uObj.dailys etc instead of uObj.tasks _.each ['habit','daily','todo','reward'], (type) -> uObj["#{type}s"] = _.where(uObj.tasks, {type}); true From 1a08c7b08463a7f2eb22689143935334bfa6fbcf Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 4 Aug 2013 09:57:31 -0400 Subject: [PATCH 20/53] change derby-ui-boot url to point to our fork, for #741 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 726c66b963..d5b86fd848 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "derby": "git://github.com/lefnire/derby#habitrpg", "racer": "git://github.com/lefnire/racer#habitrpg", "racer-db-mongo": "git://github.com/lefnire/racer-db-mongo#habitrpg", - "derby-ui-boot": "git://github.com/codeparty/derby-ui-boot#f04ba2c7e7b3a8f1462a6e70fe4f81055d74c4b2", + "derby-ui-boot": "git://github.com/HabitRPG/derby-ui-boot#habit0.3", "derby-auth": "git://github.com/lefnire/derby-auth#master", "connect-mongo": "*", "passport-facebook": "*", From 38d57cbc87881ec308a7dc37a153c7abfe0b9bfa Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 4 Aug 2013 11:53:37 -0400 Subject: [PATCH 21/53] possible better handling of empty POST, yan's weird User.log({}) thing. --- src/server/apiv2.coffee | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index a538ff8f52..a7598cea65 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -28,7 +28,6 @@ router.post '/', api.auth, (req, res, next) -> {user} = req actions = req.body - doneCount = 1 + # cron _.size(actions) # standard operations done = (err) -> @@ -43,7 +42,11 @@ router.post '/', api.auth, (req, res, next) -> res.json 200, uObj console.log "Reply sent" - if _.isObject(actions) and _.isEmpty(actions) then done() # first request, empty object + notEmpty = _.find actions, ((action) -> !_.isEmpty(action)) + unless notEmpty + # yan's strange User.log({}) thing + doneCount = 1 + return done() misc.batchTxn model, (uObj, paths) -> # habitrpg-shared/algos requires uObj.habits, uObj.dailys etc instead of uObj.tasks From a8e0015cbbb9f48f8af678ee362ecc52292915ac Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 4 Aug 2013 15:53:20 -0400 Subject: [PATCH 22/53] apiv2: unshift to tasks, rather than push --- src/server/api.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index f8fb804930..b653cb5a18 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -16,7 +16,7 @@ NO_USER_FOUND = err: "No user found." addTask = (user, task, cb) -> task.type ?= 'habit' tid = user.add "tasks", task, -> - user.push "#{task.type}Ids", tid, cb + user.unshift "#{task.type}Ids", tid, cb deleteTask = (user, task, cb) -> user.del "tasks.#{task.id}", -> From b50a78f0eaa690eb0343c86a8227d293de1e70f2 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 4 Aug 2013 16:08:40 -0400 Subject: [PATCH 23/53] add lib to gitignore for now, so we can compile. may want to remove this ignore in the future --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 01f730f678..c91cccb781 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ node_modules *.swp .idea* config.json -npm-debug.log \ No newline at end of file +npm-debug.log +lib \ No newline at end of file From 746153f417184b6aa5ffd0a651547aa1979723cb Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 4 Aug 2013 16:16:58 -0400 Subject: [PATCH 24/53] apiv2: fix transform form user.tasks => user.habits/dailies/etc, maintain sort order --- src/server/apiv2.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index a7598cea65..e9d2dcbbd5 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -36,7 +36,7 @@ router.post '/', api.auth, (req, res, next) -> uObj = misc.hydrate user.get() #transform user structure FROM user.tasks{} + user.habitIds[] TO user.habits[] + user.todos[] etc. _.each ['habit','daily','todo','reward'], (type) -> - uObj["#{type}s"] = _.where(uObj.tasks, {type}); true + uObj["#{type}s"] = _.transform uObj["#{type}Ids"], (result, tid) -> uObj.tasks[tid] delete uObj["#{type}Ids"] delete uObj.tasks res.json 200, uObj From 63ab1b197500852038d2db1fe735a01ebe12ca97 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 4 Aug 2013 16:24:30 -0400 Subject: [PATCH 25/53] apiv2 more of previous (task xform) --- package.json | 2 +- src/server/apiv2.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 726c66b963..48b33bb2fe 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "expect.js": "~0.2.0", "derby-i18n": "git://github.com/switz/derby-i18n#master", "relative-date": "~1.1.1", - "lodash": "~1.2.1", + "lodash": "~1.3.1", "async": "~0.2.9", "optimist": "~0.5.2" }, diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index e9d2dcbbd5..eef20400dc 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -36,7 +36,7 @@ router.post '/', api.auth, (req, res, next) -> uObj = misc.hydrate user.get() #transform user structure FROM user.tasks{} + user.habitIds[] TO user.habits[] + user.todos[] etc. _.each ['habit','daily','todo','reward'], (type) -> - uObj["#{type}s"] = _.transform uObj["#{type}Ids"], (result, tid) -> uObj.tasks[tid] + uObj["#{type}s"] = _.transform uObj["#{type}Ids"], (result, tid) -> result.push(uObj.tasks[tid]) delete uObj["#{type}Ids"] delete uObj.tasks res.json 200, uObj From b158e8896c84f4fca6c4a8045887237bb5b440bb Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 4 Aug 2013 23:43:54 -0400 Subject: [PATCH 26/53] apiv2: move 'revive' to habitrpg-shared so that it's available on mobile & api. Then call it from app & api --- src/app/profile.coffee | 22 +++------------------- src/server/apiv2.coffee | 9 ++++++++- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/app/profile.coffee b/src/app/profile.coffee index 4577ba1da7..97714561f3 100644 --- a/src/app/profile.coffee +++ b/src/app/profile.coffee @@ -9,24 +9,9 @@ module.exports.app = (appExports, model) -> user = model.at('_user') appExports.revive = -> - # Reset stats - user.set 'stats.hp', 50 - user.set 'stats.exp', 0 - user.set 'stats.gp', 0 - user.incr 'stats.lvl', -1 if user.get('stats.lvl') > 1 - - ## Lose a random item - loseThisItem = false - owned = user.get('items') - # unless they're already at 0-everything - if parseInt(owned.armor)>0 or parseInt(owned.head)>0 or parseInt(owned.shield)>0 or parseInt(owned.weapon)>0 - # find a random item to lose - until loseThisItem - #candidate = {0:'items.armor', 1:'items.head', 2:'items.shield', 3:'items.weapon', 4:'stats.gp'}[Math.random()*5|0] - candidate = {0:'armor', 1:'head', 2:'shield', 3:'weapon'}[Math.random()*4|0] - loseThisItem = candidate if owned[candidate] > 0 - user.set "items.#{loseThisItem}", 0 - + [uObj, paths] = [user.get(), {}] + algos.revive(uObj, {paths}) + _.each paths, ((v,k) -> user.set k, helpers.dotGet(k, uObj)) items.updateStore(model) appExports.reset = (e, el) -> @@ -96,4 +81,3 @@ module.exports.app = (appExports, model) -> appExports.toggleResting = -> model.set '_user.flags.rest', !model.get('_user.flags.rest') - diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index eef20400dc..9faf031f42 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -88,6 +88,13 @@ router.post '/', api.auth, (req, res, next) -> else user.set action.path, action.value, done + when "revive" + [uObj, paths] = [user.get(), {}] + algos.revive uObj, {paths} + doneCount += (_.size(paths) - 1) # once for each path, but +1 is already accounted for at top of this function (whole 'revive' counted as 1) + _.each paths, (v,k) -> + user.set k, helpers.dotGet(k,uObj), done + else done() -module.exports = router \ No newline at end of file +module.exports = router From af8b764a68a00cb1dde1d9962ba8b15a48cd1de8 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 6 Aug 2013 18:23:32 -0400 Subject: [PATCH 27/53] apiv2 make sure to handle empty object --- src/server/apiv2.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index 9faf031f42..04ba3558da 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -59,6 +59,8 @@ router.post '/', api.auth, (req, res, next) -> task = action.task ? {} + return done() if _.isEmpty(action) + switch action.op when "score" sendScore = -> api.score(model, user, task.id, action.dir, done) From e8f4a94abbb2fd1614749c52011011adfb01bd33 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 8 Aug 2013 13:59:08 -0400 Subject: [PATCH 28/53] make sure user owns the taskb efore scoring --- src/server/apiv2.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index 04ba3558da..d131c309e2 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -63,6 +63,7 @@ router.post '/', api.auth, (req, res, next) -> switch action.op when "score" + return done() unless user.get "tasks.#{task.id}" sendScore = -> api.score(model, user, task.id, action.dir, done) if task.type in ["daily","todo"] # switch completed state. Since checkbox is not binded to model unlike when you click through Derby website. From e667b3a2b037d71f332ad44535ad421134235547 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Thu, 8 Aug 2013 14:04:07 -0400 Subject: [PATCH 29/53] start adding v2 api tests (nothing here yet) --- test/api.mocha.coffee | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index 0b10eb7a93..b0aeec4391 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -398,7 +398,6 @@ describe 'API', -> expect(user.get("tasks.#{foundNewTask.id}")).to.eql id: foundNewTask.id, text: 'new task2', notes: 'notes2' done() - it 'POST /api/v1/user/auth/local', (done) -> userAuth = username: username @@ -434,3 +433,26 @@ describe 'API', -> expect(res.body.id).to.be newUser.id #expect(res.body.token).to.be newUser.apiToken done() + + it.only 'POST /api/v2', (done) -> + userBefore = {} +# user.set "lastCron", +new Date #FIXME this shouldn't be handled here + query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken) + query.fetch (err, user) -> userBefore = user.get() + console.log {userBefore} + + jsonRaw = + [{"op":"score","task":{"completed":true,"date":null,"down":null,"id":"049ee706-7992-408f-8bdd-a0f87b6cddee","notes":null,"price":null,"priority":null,"streak":1,"text":"asdasd","type":"daily","up":null,"value":-15.159032750819472},"dir":"down"},{"op":"score","task":{"completed":true,"date":null,"down":null,"id":"049ee706-7992-408f-8bdd-a0f87b6cddee","notes":null,"price":null,"priority":null,"streak":1,"text":"asdasd","type":"daily","up":null,"value":-15.159032750819472},"dir":"up"},{},{},{},{},{"op":"score","task":{"completed":true,"date":null,"down":null,"id":"049ee706-7992-408f-8bdd-a0f87b6cddee","notes":null,"price":null,"priority":null,"streak":1,"text":"asdasd","type":"daily","up":null,"value":-16.63136866553572},"dir":"down"},{"op":"score","task":{"completed":true,"date":null,"down":null,"id":"049ee706-7992-408f-8bdd-a0f87b6cddee","notes":null,"price":null,"priority":null,"streak":1,"text":"asdasd","type":"daily","up":null,"value":-16.63136866553572},"dir":"up"},{},{},{"op":"score","task":{"completed":true,"date":null,"down":null,"history":[{"date":1370796966979,"value":-1.9263318037820194},{"date":1371394179245,"value":-9.632667221983818},{"date":1371987764419,"value":-8.899142684639843},{"date":1371901709065,"value":-8.697714139260505},{"date":1371987764419,"value":-9.947389352351902},{"date":1372072605105,"value":-8.65704735207246},{"date":1372185464158,"value":-9.905420946093672},{"date":1372348155721,"value":-8.616465915449927},{"date":1372404365115,"value":-7.369389857231249},{"date":1372619315210,"value":-6.16153655829917},{"date":1372797766170,"value":-4.990495966065229},{"date":1373266931264,"value":-12.356300657493207},{"date":1373322727209,"value":-10.983796434663102},{"date":1373407801484,"value":-9.658725758132753},{"date":1373639117325,"value":-8.377893411957398},{"date":1373719671601,"value":-7.138418159258412},{"date":1373826521297,"value":-5.902428899644443},{"date":1373839445467,"value":-8.264210410818091},{"date":1373929050162,"value":-8.224444248693572},{"date":1374058202835,"value":-9.459055183497052},{"date":1374102966396,"value":-8.184759693251706},{"date":1374187619046,"value":-6.951403643619735},{"date":1374342649005,"value":-5.72148344218024},{"date":1374434356841,"value":-8.072174632205511},{"date":1374562742400,"value":-10.571154014907808},{"date":1374886027789,"value":-16.097395454285916},{"date":1375011848715,"value":-17.607991906162557},{"date":1375436884647,"value":-16.037773949824242},{"date":1375563074478,"value":-17.546064223707074},{"date":1375568230260,"value":-19.11379232897715},{"date":1375734631073,"value":-20.745784396408645},{"date":1375785010434,"value":-18.767794224069412},{"date":1375826853480,"value":-18.636651213045212}],"id":"fe4b9061-eb58-468c-9b25-10c72be772e6","notes":"","price":null,"priority":null,"repeat":{"su":true,"m":true,"t":true,"w":true,"th":true,"f":true,"s":true},"streak":1,"tags":{"40492758-1202-4d85-8cb3-d40e45f4dd1d":false},"text":"Read 50 pages","type":"daily","up":null,"value":-17.024492021142215},"dir":"up"},{},{},{}] + + request.post("http://localhost:1337/api/v2") + .set('Accept', 'application/json') + .set('X-API-User', currentUser.id) + .set('X-API-Key', currentUser.apiToken) + .send(jsonRaw) + .end (res) -> + expect(res.body.err).to.be undefined + expect(res.statusCode).to.be 200 + tasks = res.body.tasks + + done() + From c7381485a36d55b4225277bfb1e5cfac85cc98ff Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Sat, 10 Aug 2013 15:18:56 -0500 Subject: [PATCH 30/53] Fix broken image on 500 error page Replaced nonexistent male armor sprite with Grim Reaper; removed rotation --- public/500.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/500.html b/public/500.html index fb28f601f6..2bf49b561a 100644 --- a/public/500.html +++ b/public/500.html @@ -11,7 +11,7 @@ - + -->
- +

The server is restarting.

Try again in a few. We restart often due to this issue, and we're rewriting the site to fix it. (AngularJS developers, come join us!)

From e7d200f1b0ce5f1ef85858f96f273c88ba4412e2 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 10 Aug 2013 16:19:59 -0400 Subject: [PATCH 31/53] update apiv2 to use async.series instead of the previous count method. much better flow control --- src/server/apiv2.coffee | 141 ++++++++++++++++++++-------------------- 1 file changed, 70 insertions(+), 71 deletions(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index d131c309e2..96017b9424 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -1,6 +1,7 @@ express = require 'express' router = new express.Router() util = require 'util' +async = require 'async' _ = require 'lodash' algos = require 'habitrpg-shared/script/algos' @@ -26,78 +27,76 @@ POST new actions router.post '/', api.auth, (req, res, next) -> model = req.getModel() {user} = req - actions = req.body - doneCount = 1 + # cron - _.size(actions) # standard operations - done = (err) -> + performAction = (action, cb) -> + task = action.task ? {} + switch action.op + when "cron" + misc.batchTxn model, (uObj, paths) -> + # habitrpg-shared/algos requires uObj.habits, uObj.dailys etc instead of uObj.tasks + _.each ['habit','daily','todo','reward'], (type) -> uObj["#{type}s"] = _.where(uObj.tasks, {type}); true + algos.cron uObj, {paths} + , {user, cb, cron:true} + + when "score" + return cb() unless user.get "tasks.#{task.id}" + sendScore = -> api.score(model, user, task.id, action.dir, cb) + if task.type in ["daily","todo"] + # switch completed state. Since checkbox is not binded to model unlike when you click through Derby website. + completed = if action.dir is "up" then true else false + user.set "tasks.#{task.id}.completed", completed, sendScore + else sendScore() + + when "sortTask" + path = action.task.type + "Ids" + a = user.get(path) + a.splice(action.to, 0, a.splice(action.from, 1)[0]) + user.set path, a, cb + + when "addTask" + api.addTask user, task, cb + + when "delTask" + api.deleteTask user, task, cb + + # this API is only working with string or number variables. It should return error if object given or object is at the path. + when "set" + oldValue = user.get(action.path) + if _.isObject(action.value) or _.isObject(oldValue) + console.error "action.value was an object, which isn't currently supported. Tyler - double check this" + cb() + else + user.set action.path, action.value, cb + + when "revive" + [uObj, paths] = [user.get(), {}] + algos.revive uObj, {paths} + setOps = _.map paths, (v,k) -> + (reviveCb) -> user.set k, helpers.dotGet(k,uObj), reviveCb + console.log setOps + async.parallel setOps, cb + + else + cb() + + # Setup the array of functions we're going to call in parallel with async + req.body = [] if _.isEmpty req.body + actions = _.transform (req.body or []), (result, action) -> + unless _.isEmpty(action) + result.push (cb) -> performAction(action, cb) + # always run cron check + req.body.unshift({op: 'cron'}) unless _.isEmpty actions + + # call all the operations, then return the user object to the requester + async.series actions, (err) -> return next(err) if err - if --doneCount is 0 - uObj = misc.hydrate user.get() - #transform user structure FROM user.tasks{} + user.habitIds[] TO user.habits[] + user.todos[] etc. - _.each ['habit','daily','todo','reward'], (type) -> - uObj["#{type}s"] = _.transform uObj["#{type}Ids"], (result, tid) -> result.push(uObj.tasks[tid]) - delete uObj["#{type}Ids"] - delete uObj.tasks - res.json 200, uObj - console.log "Reply sent" - - notEmpty = _.find actions, ((action) -> !_.isEmpty(action)) - unless notEmpty - # yan's strange User.log({}) thing - doneCount = 1 - return done() - - misc.batchTxn model, (uObj, paths) -> - # habitrpg-shared/algos requires uObj.habits, uObj.dailys etc instead of uObj.tasks - _.each ['habit','daily','todo','reward'], (type) -> uObj["#{type}s"] = _.where(uObj.tasks, {type}); true - algos.cron uObj, {paths} - , {user, done, cron:true} - - if _.isArray actions - actions.forEach (action)-> - - task = action.task ? {} - - return done() if _.isEmpty(action) - - switch action.op - when "score" - return done() unless user.get "tasks.#{task.id}" - sendScore = -> api.score(model, user, task.id, action.dir, done) - if task.type in ["daily","todo"] - # switch completed state. Since checkbox is not binded to model unlike when you click through Derby website. - completed = if action.dir is "up" then true else false - user.set "tasks.#{task.id}.completed", completed, sendScore - else sendScore() - - when "sortTask" - path = action.task.type + "Ids" - a = user.get(path) - a.splice(action.to, 0, a.splice(action.from, 1)[0]) - user.set path, a, done - - when "addTask" - api.addTask user, task, done - - when "delTask" - api.deleteTask user, task, done - - # this API is only working with string or number variables. It should return error if object given or object is at the path. - when "set" - oldValue = user.get(action.path) - if _.isObject(action.value) or _.isObject(oldValue) - console.error "action.value was an object, which isn't currently supported. Tyler - double check this" - else - user.set action.path, action.value, done - - when "revive" - [uObj, paths] = [user.get(), {}] - algos.revive uObj, {paths} - doneCount += (_.size(paths) - 1) # once for each path, but +1 is already accounted for at top of this function (whole 'revive' counted as 1) - _.each paths, (v,k) -> - user.set k, helpers.dotGet(k,uObj), done - - else done() + uObj = misc.hydrate user.get() + #transform user structure FROM user.tasks{} + user.habitIds[] TO user.habits[] + user.todos[] etc. + _.each ['habit','daily','todo','reward'], (type) -> + uObj["#{type}s"] = _.transform uObj["#{type}Ids"], (result, tid) -> result.push(uObj.tasks[tid]) + delete uObj["#{type}Ids"] + delete uObj.tasks + res.json 200, uObj + console.log "Reply sent" module.exports = router From 420a8617d734e6af1a80d56070e81508068a22e9 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Sat, 10 Aug 2013 16:11:27 -0500 Subject: [PATCH 32/53] Server is respawning Humorous wording tweak --- public/500.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/500.html b/public/500.html index 2bf49b561a..2afed99308 100644 --- a/public/500.html +++ b/public/500.html @@ -26,7 +26,7 @@
-

The server is restarting.

+

The server is respawning.

Try again in a few. We restart often due to this issue, and we're rewriting the site to fix it. (AngularJS developers, come join us!)

If this page persists, the server may be experiencing issues; the developers have been notified. Try switching to the beta site or the main site.

From 4857e44f799f2f9a6b1977aa77f4fd0653aea725 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 10 Aug 2013 17:37:49 -0400 Subject: [PATCH 33/53] apiv2 fix on adding cron to queue --- src/server/apiv2.coffee | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee index 96017b9424..7fa8e9b80e 100644 --- a/src/server/apiv2.coffee +++ b/src/server/apiv2.coffee @@ -80,12 +80,10 @@ router.post '/', api.auth, (req, res, next) -> cb() # Setup the array of functions we're going to call in parallel with async - req.body = [] if _.isEmpty req.body - actions = _.transform (req.body or []), (result, action) -> - unless _.isEmpty(action) - result.push (cb) -> performAction(action, cb) - # always run cron check - req.body.unshift({op: 'cron'}) unless _.isEmpty actions + # Start with cron + (req.body or= []).unshift({op: 'cron'}) + actions = _.transform (req.body), (result, action) -> + result.push (cb) -> performAction(action, cb) unless _.isEmpty(action) # call all the operations, then return the user object to the requester async.series actions, (err) -> From 3263708c9d45401c5aa80a7eb5cee12981091419 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 10 Aug 2013 21:28:08 -0400 Subject: [PATCH 34/53] apiv2: BIG update - remove apiv2, it's going to be part of apiv1. Overhaul of PUT /user to be ready for this new API update --- src/server/api.coffee | 196 +++++++++++++++++++++++++--------------- src/server/apiv2.coffee | 100 -------------------- src/server/index.coffee | 1 - 3 files changed, 124 insertions(+), 173 deletions(-) delete mode 100644 src/server/apiv2.coffee diff --git a/src/server/api.coffee b/src/server/api.coffee index b653cb5a18..8730f0bea7 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -31,6 +31,50 @@ score = (model, user, taskId, direction, cb) -> , {user, done:cb} delta +### + This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login +### +scoreTask = (req, res, next) -> + {taskId, direction} = req.params + {title, service, icon, type} = req.body + type ||= 'habit' + + # Send error responses for improper API call + return res.send(500, ':taskId required') unless taskId + return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] + + model = req.getModel() + {user, userObj} = req + + existingTask = user.at "tasks.#{taskId}" + # TODO add service & icon to task + # If task exists, set it's compltion + if existingTask.get() + # Set completed if type is daily or todo + existingTask.set 'completed', (direction is 'up') if /^(daily|todo)$/.test existingTask.get('type') + else + task = + id: taskId + type: type + text: (title || taskId) + value: 0 + notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." + + switch type + when 'habit' + task.up = true + task.down = true + when 'daily', 'todo' + task.completed = direction is 'up' + + addTask user, task + + # TODO - could modify batchTxn to conform to this better + delta = score model, user, taskId, direction, -> + result = user.get('stats') + result.delta = delta + res.json result + # ---------- /api/v1 API ------------ # Every url added beneath router is prefaced by /api/v1 @@ -81,36 +125,6 @@ router.get '/user', auth, (req, res) -> res.json user -### - TODO POST /user - when a put attempt didn't work, create a new one with POST -### - -### - PUT /user -### -router.put '/user', auth, (req, res) -> - user = req.user - partialUser = req.body.user - - # REVISIT is this the best way of handling protected v acceptable attr mass-setting? Possible pitfalls: (1) we have to remember - # to update here when we add new schema attrs in the future, (2) developers can't assign random variables (which - # is currently beneficial for Kevin & Paul). Pros: protects accidental or malicious user data corruption - - # TODO - this accounts for single-nested items (stats.hp, stats.exp) but will clobber any other depth. - # See http://stackoverflow.com/a/6394168/362790 for when we need to cross that road - - acceptableAttrs = ['flags', 'history', 'items', 'preferences', 'profile', 'stats'] - user.set 'lastCron', partialUser.lastCron if partialUser.lastCron? - _.each acceptableAttrs, (attr) -> - _.each partialUser[attr], (val, key) -> user.set("#{attr}.#{key}", val);true - - updateTasks partialUser.tasks, req.user, req.getModel() if partialUser.tasks? - - userObj = user.get() - userObj.tasks = _.toArray(userObj.tasks) # FIXME figure out how we're going to consistently handle this. should always be array - res.json 201, userObj - ### POST /user/auth/local ### @@ -167,7 +181,6 @@ router.post '/user/auth/facebook', (req, res) -> router.get '/user/task/:id', auth, (req, res) -> task = req.userObj.tasks[req.params.id] return res.json 400, err: "No task found." if !task || _.isEmpty(task) - res.json 200, task ### @@ -209,7 +222,6 @@ validateTask = (req, res, next) -> ### router.put '/user/task/:id', auth, validateTask, (req, res) -> req.user.set "tasks.#{req.task.id}", req.task - res.json 200, req.task ### @@ -268,48 +280,93 @@ router.get '/user/tasks', auth, (req, res) -> res.json 200, tasks ### - This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login + PUT /user ### -scoreTask = (req, res, next) -> - {taskId, direction} = req.params - {title, service, icon, type} = req.body - type ||= 'habit' +router.put '/user', auth, (req, res, next) -> - # Send error responses for improper API call - return res.send(500, ':taskId required') unless taskId - return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] + # FIXME we need to do some crazy sanitiazation if they're using the old `PUT /user {data}` method. + # The new `PUT /user {'stats.hp':50} + # FIXME - one-by-one we want to widdle down this list, instead replacing each needed set path with API operations + # Note: custom is for 3rd party apps + acceptableAttrs = 'achievements filters flags invitations items lastCron party preferences profile stats tags custom'.join(' ') + series = [] + _.each req.body, (v, k) -> + if (_.find acceptableAttrs, (attr)-> k.indexOf(attr) is 0)? + series.push (cb) -> req.user.set(k, v, cb) + async.series series, (err) -> + return next(err) if err + res.json 201, helpers.derbyUserToAPI(user) + +### +POST new actions +### +router.post '/batch-update', auth, (req, res, next) -> model = req.getModel() - {user, userObj} = req + {user} = req - existingTask = user.at "tasks.#{taskId}" - # TODO add service & icon to task - # If task exists, set it's compltion - if existingTask.get() - # Set completed if type is daily or todo - existingTask.set 'completed', (direction is 'up') if /^(daily|todo)$/.test existingTask.get('type') - else - task = - id: taskId - type: type - text: (title || taskId) - value: 0 - notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." + performAction = (action, cb) -> + task = action.task ? {} + switch action.op + when "cron" + misc.batchTxn model, (uObj, paths) -> + uObj = helpers.derbyUserToAPI(user) + algos.cron uObj, {paths} + , {user, done:cb, cron:true} - switch type - when 'habit' - task.up = true - task.down = true - when 'daily', 'todo' - task.completed = direction is 'up' + when "score" + return cb() unless user.get "tasks.#{task.id}" + sendScore = -> score(model, user, task.id, action.dir, cb) + if task.type in ["daily","todo"] + # switch completed state. Since checkbox is not binded to model unlike when you click through Derby website. + completed = if action.dir is "up" then true else false + user.set "tasks.#{task.id}.completed", completed, sendScore + else sendScore() - addTask user, task + when "sortTask" + path = action.task.type + "Ids" + a = user.get(path) + a.splice(action.to, 0, a.splice(action.from, 1)[0]) + user.set path, a, cb + + when "addTask" + addTask user, task, cb + + when "delTask" + deleteTask user, task, cb + + # this API is only working with string or number variables. It should return error if object given or object is at the path. + when "set" + oldValue = user.get(action.path) + if _.isObject(action.value) or _.isObject(oldValue) + console.error "action.value was an object, which isn't currently supported. Tyler - double check this" + cb() + else + user.set action.path, action.value, cb + + when "revive" + [uObj, paths] = [user.get(), {}] + algos.revive uObj, {paths} + setOps = _.map paths, (v,k) -> + (reviveCb) -> user.set k, helpers.dotGet(k,uObj), reviveCb + console.log setOps + async.serial setOps, cb + + else + cb() + + # Setup the array of functions we're going to call in parallel with async + # Start with cron + (req.body or= []).unshift({op: 'cron'}) + actions = _.transform (req.body), (result, action) -> + result.push (cb) -> performAction(action, cb) unless _.isEmpty(action) + + # call all the operations, then return the user object to the requester + async.series actions, (err) -> + return next(err) if err + res.json 200, helpers.derbyUserToAPI(user) + console.log "Reply sent" - # TODO - could modify batchTxn to conform to this better - delta = score model, user, taskId, direction, -> - result = user.get('stats') - result.delta = delta - res.json result ### POST /user/tasks/:taskId/:direction @@ -319,9 +376,4 @@ router.post '/user/tasks/:taskId/:direction', auth, scoreTask module.exports = router module.exports.auth = auth -module.exports.scoreTask = scoreTask # export so deprecated can call it -module.exports.NO_TOKEN_OR_UID = NO_TOKEN_OR_UID -module.exports.NO_USER_FOUND = NO_USER_FOUND -module.exports.addTask = addTask -module.exports.score = score -module.exports.deleteTask = deleteTask +module.exports.scoreTask = scoreTask # export so deprecated can call it \ No newline at end of file diff --git a/src/server/apiv2.coffee b/src/server/apiv2.coffee deleted file mode 100644 index 7fa8e9b80e..0000000000 --- a/src/server/apiv2.coffee +++ /dev/null @@ -1,100 +0,0 @@ -express = require 'express' -router = new express.Router() -util = require 'util' -async = require 'async' - -_ = require 'lodash' -algos = require 'habitrpg-shared/script/algos' -helpers = require 'habitrpg-shared/script/helpers' -validator = require 'derby-auth/node_modules/validator' -check = validator.check -sanitize = validator.sanitize -misc = require '../app/misc' -api = require './api' - -# ---------- /api/v2 API ------------ -# Every url added beneath router is prefaced by /api/v2 - -### - API Status -### -router.get '/status', (req, res) -> - res.json status: 'up' - -### -POST new actions -### -router.post '/', api.auth, (req, res, next) -> - model = req.getModel() - {user} = req - - performAction = (action, cb) -> - task = action.task ? {} - switch action.op - when "cron" - misc.batchTxn model, (uObj, paths) -> - # habitrpg-shared/algos requires uObj.habits, uObj.dailys etc instead of uObj.tasks - _.each ['habit','daily','todo','reward'], (type) -> uObj["#{type}s"] = _.where(uObj.tasks, {type}); true - algos.cron uObj, {paths} - , {user, cb, cron:true} - - when "score" - return cb() unless user.get "tasks.#{task.id}" - sendScore = -> api.score(model, user, task.id, action.dir, cb) - if task.type in ["daily","todo"] - # switch completed state. Since checkbox is not binded to model unlike when you click through Derby website. - completed = if action.dir is "up" then true else false - user.set "tasks.#{task.id}.completed", completed, sendScore - else sendScore() - - when "sortTask" - path = action.task.type + "Ids" - a = user.get(path) - a.splice(action.to, 0, a.splice(action.from, 1)[0]) - user.set path, a, cb - - when "addTask" - api.addTask user, task, cb - - when "delTask" - api.deleteTask user, task, cb - - # this API is only working with string or number variables. It should return error if object given or object is at the path. - when "set" - oldValue = user.get(action.path) - if _.isObject(action.value) or _.isObject(oldValue) - console.error "action.value was an object, which isn't currently supported. Tyler - double check this" - cb() - else - user.set action.path, action.value, cb - - when "revive" - [uObj, paths] = [user.get(), {}] - algos.revive uObj, {paths} - setOps = _.map paths, (v,k) -> - (reviveCb) -> user.set k, helpers.dotGet(k,uObj), reviveCb - console.log setOps - async.parallel setOps, cb - - else - cb() - - # Setup the array of functions we're going to call in parallel with async - # Start with cron - (req.body or= []).unshift({op: 'cron'}) - actions = _.transform (req.body), (result, action) -> - result.push (cb) -> performAction(action, cb) unless _.isEmpty(action) - - # call all the operations, then return the user object to the requester - async.series actions, (err) -> - return next(err) if err - uObj = misc.hydrate user.get() - #transform user structure FROM user.tasks{} + user.habitIds[] TO user.habits[] + user.todos[] etc. - _.each ['habit','daily','todo','reward'], (type) -> - uObj["#{type}s"] = _.transform uObj["#{type}Ids"], (result, tid) -> result.push(uObj.tasks[tid]) - delete uObj["#{type}Ids"] - delete uObj.tasks - res.json 200, uObj - console.log "Reply sent" - -module.exports = router diff --git a/src/server/index.coffee b/src/server/index.coffee index 4c67ea04ba..a723545ae5 100644 --- a/src/server/index.coffee +++ b/src/server/index.coffee @@ -77,7 +77,6 @@ mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, -> .use(middleware.translate) # API should be hit before all other routes .use('/api/v1', require('./api').middleware) - .use('/api/v2', require('./apiv2').middleware) .use(require('./deprecated').middleware) # Show splash page for newcomers .use(middleware.splash) From d17bd6fe90e28b1c341bf4a760600b4d74afcd36 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 10 Aug 2013 23:29:24 -0400 Subject: [PATCH 35/53] api: export all of api to functions, called by route. This way we can have our /batch-update route access the api functions --- src/app/misc.coffee | 14 +- src/server/api.coffee | 431 ++++++++++++++++++++------------------- src/server/index.coffee | 2 +- src/server/routes.coffee | 51 +++++ test/api.mocha.coffee | 10 +- 5 files changed, 276 insertions(+), 232 deletions(-) create mode 100644 src/server/routes.coffee diff --git a/src/app/misc.coffee b/src/app/misc.coffee index ee3ad2c173..ef27421631 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -5,7 +5,7 @@ helpers = require('habitrpg-shared/script/helpers') module.exports.batchTxn = batchTxn = (model, cb, options) -> user = options?.user or model.at("_user") - uObj = hydrate(user.get()) # see https://github.com/codeparty/racer/issues/116 + uObj = helpers.hydrate(user.get()) # see https://github.com/codeparty/racer/issues/116 batch = set: (k,v) -> helpers.dotSet(k,v,uObj); paths[k] = true get: (k) -> helpers.dotGet(k,uObj) @@ -87,18 +87,6 @@ module.exports.score = (model, taskId, direction, allowUndo=false) -> delta -### - Make sure model.get() returns all properties, see https://github.com/codeparty/racer/issues/116 -### -module.exports.hydrate = hydrate = (spec) -> - if _.isObject(spec) and !_.isArray(spec) - hydrated = {} - keys = _.keys(spec).concat(_.keys(spec.__proto__)) - keys.forEach (k) -> hydrated[k] = hydrate(spec[k]) - hydrated - else spec - - ### Cleanup task-corruption (null tasks, rogue/invisible tasks, etc) Obviously none of this should be happening, but we'll stop-gap until we can find & fix diff --git a/src/server/api.coffee b/src/server/api.coffee index 8730f0bea7..60121c6af3 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -1,7 +1,7 @@ -express = require 'express' -router = new express.Router() +# @see ./routes.coffee for routing _ = require 'lodash' +async = require 'async' algos = require 'habitrpg-shared/script/algos' helpers = require 'habitrpg-shared/script/helpers' validator = require 'derby-auth/node_modules/validator' @@ -10,9 +10,42 @@ sanitize = validator.sanitize utils = require 'derby-auth/utils' misc = require '../app/misc' +api = module.exports + +### + ------------------------------------------------------------------------ + Misc + ------------------------------------------------------------------------ +#### + NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request" NO_USER_FOUND = err: "No user found." +### + beforeEach auth interceptor +### +api.auth = (req, res, next) -> + uid = req.headers['x-api-user'] + token = req.headers['x-api-key'] + return res.json 401, NO_TOKEN_OR_UID unless uid || token + + model = req.getModel() + query = model.query('users').withIdAndToken(uid, token) + + query.fetch (err, user) -> + return res.json err: err if err + (req.habit ?= {}).user = user + req.habit.userObj = user.get() + return res.json 401, NO_USER_FOUND if _.isEmpty(req.habit.userObj) + req._isServer = true + next() + +### + ------------------------------------------------------------------------ + Tasks + ------------------------------------------------------------------------ +### + addTask = (user, task, cb) -> task.type ?= 'habit' tid = user.add "tasks", task, -> @@ -33,20 +66,21 @@ score = (model, user, taskId, direction, cb) -> ### This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login + Export it also so we can call it from deprecated.coffee ### -scoreTask = (req, res, next) -> - {taskId, direction} = req.params +api.scoreTask = (req, res, next) -> + {id, direction} = req.params {title, service, icon, type} = req.body type ||= 'habit' # Send error responses for improper API call - return res.send(500, ':taskId required') unless taskId - return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down'] + return res.json 500, {err: ':id required'} unless id + return res.json 500, {err: ":direction must be 'up' or 'down'"} unless direction in ['up','down'] model = req.getModel() - {user, userObj} = req + {user, userObj} = req.habit - existingTask = user.at "tasks.#{taskId}" + existingTask = user.at "tasks.#{id}" # TODO add service & icon to task # If task exists, set it's compltion if existingTask.get() @@ -54,9 +88,9 @@ scoreTask = (req, res, next) -> existingTask.set 'completed', (direction is 'up') if /^(daily|todo)$/.test existingTask.get('type') else task = - id: taskId + id: id type: type - text: (title || taskId) + text: (title || id) value: 0 notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." @@ -70,129 +104,42 @@ scoreTask = (req, res, next) -> addTask user, task # TODO - could modify batchTxn to conform to this better - delta = score model, user, taskId, direction, -> + delta = score model, user, id, direction, -> result = user.get('stats') - result.delta = delta - res.json result - -# ---------- /api/v1 API ------------ -# Every url added beneath router is prefaced by /api/v1 - -### - v1 API. Requires api-v1-user (user id) and api-v1-key (api key) headers, Test with: - $ cd node_modules/racer && npm install && cd ../.. - $ mocha test/api.mocha.coffee -### - -### - API Status -### -router.get '/status', (req, res) -> - res.json status: 'up' - -### - beforeEach auth interceptor -### -auth = (req, res, next) -> - uid = req.headers['x-api-user'] - token = req.headers['x-api-key'] - return res.json 401, NO_TOKEN_OR_UID unless uid || token - - model = req.getModel() - query = model.query('users').withIdAndToken(uid, token) - - query.fetch (err, user) -> - return res.json err: err if err - req.user = user - req.userObj = user.get() - return res.json 401, NO_USER_FOUND if _.isEmpty(req.userObj) - req._isServer = true + req.habit.result = data: _.extend(result, delta: delta) next() ### - GET /user + Get all tasks ### -router.get '/user', auth, (req, res) -> - user = req.userObj - - user.stats.toNextLevel = algos.tnl user.stats.lvl - user.stats.maxHealth = 50 - - delete user.apiToken - if user.auth - delete user.auth.hashed_password - delete user.auth.salt - - res.json user +api.getTasks = (req, res, next) -> + return res.json 400, NO_USER_FOUND if _.isEmpty(req.habit.userObj) + types = + if /^(habit|todo|daily|reward)$/.test(req.query.type) then [req.query.type] + else ['habit','todo','daily','reward'] + tasks = _.toArray (_.filter req.habit.user.get('tasks'), (t)-> t.type in types) + req.habit.result = data: tasks + next() ### - POST /user/auth/local + Get Task ### -router.post '/user/auth/local', (req, res) -> - username = req.body.username - password = req.body.password - return res.json 401, err: 'No username or password' unless username and password - - model = req.getModel() - - q = model.query("users").withUsername(username) - q.fetch (err, result1) -> - return res.json 401, { err } if err - u1 = result1.get() - return res.json 401, err: 'Username not found' unless u1 # user not found - - # We needed the whole user object first so we can get his salt to encrypt password comparison - q = model.query("users").withLogin(username, utils.encryptPassword(password, u1.auth.local.salt)) - q.fetch (err, result2) -> - return res.json 401, { err } if err - - # joshua tree? - u2 = result2.get() - return res.json 401, err: 'Incorrect password' unless u2 - - res.json - id: u2.id - token: u2.apiToken - -### - POST /user/auth/facebook -### -router.post '/user/auth/facebook', (req, res) -> - {facebook_id, email, name} = req.body - return res.json 401, err: 'No facebook id provided' unless facebook_id - model = req.getModel() - q = model.query("users").withProvider('facebook', facebook_id) - q.fetch (err, result) -> - return res.json 401, { err } if err - u = result.get() - console.log {facebook_id, u} - if u - return res.json - id: u.id - token: u.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." - - -### - GET /user/task/:id -### -router.get '/user/task/:id', auth, (req, res) -> - task = req.userObj.tasks[req.params.id] +api.getTask = (req, res, next) -> + task = req.habit.userObj.tasks[req.params.id] return res.json 400, err: "No task found." if !task || _.isEmpty(task) - res.json 200, task + req.habit.result = data: task + next() ### - validate task + Validate task ### -validateTask = (req, res, next) -> +api.validateTask = (req, res, next) -> task = {} newTask = { type, text, notes, value, up, down, completed } = req.body # If we're updating, get the task from the user if req.method is 'PUT' or req.method is 'DELETE' - task = req.userObj?.tasks[req.params.id] + task = req.habit.userObj?.tasks[req.params.id] return res.json 400, err: "No task found." if !task || _.isEmpty(task) # Strip for now type = undefined @@ -214,25 +161,19 @@ validateTask = (req, res, next) -> newTask.completed = false unless typeof completed is 'boolean' _.extend task, newTask - req.task = task + req.habit.task = task next() ### - PUT /user/task/:id + Delete Task ### -router.put '/user/task/:id', auth, validateTask, (req, res) -> - req.user.set "tasks.#{req.task.id}", req.task - res.json 200, req.task +api.deleteTask = (req, res, next) -> + deleteTask req.habit.user, req.habit.task, -> + req.habit.result = code: 204 + next() ### - DELETE /user/task/:id -### -router.delete '/user/task/:id', auth, validateTask, (req, res) -> - deleteTask req.user, req.task.type, req.task.id - res.send 204 - -### - POST /user/tasks + Helper function for updating multiple tasks ### updateTasks = (tasks, user, model) -> for idx, task of tasks @@ -253,127 +194,191 @@ updateTasks = (tasks, user, model) -> tasks[idx] = task return tasks -router.post '/user/tasks', auth, (req, res) -> - tasks = updateTasks req.body, req.user, req.getModel() - res.json 201, tasks - +### + Update Task +### +api.updateTask = (req, res, next) -> + req.habit.user.set "tasks.#{req.habit.task.id}", req.habit.task + req.habit.result = data: req.habit.task + next() ### - POST /user/task/ + Update tasks (plural). This will update, add new, delete, etc all at once. + Should we keep this? ### -router.post '/user/task', auth, validateTask, (req, res) -> - task = req.task - addTask req.user, task - res.json 201, task +api.updateTasks = (req, res, next) -> + tasks = updateTasks req.body, req.habit.user, req.getModel() + req.habit.result = code: 201, data: tasks + next() + +api.createTask = (req, res, next) -> + task = req.habit.task + addTask req.habit.user, task + req.habit.result = code: 201, data: task + next() + +api.sortTask = (req, res, next) -> + {id} = req.params + {to, from, type} = req.habit.task + {user} = req.habit + path = "#{type}Ids" + a = user.get(path) + a.splice(to, 0, a.splice(from, 1)[0]) + user.set path, a, next ### - GET /user/tasks + ------------------------------------------------------------------------ + User + ------------------------------------------------------------------------ ### -router.get '/user/tasks', auth, (req, res) -> - return res.json 400, NO_USER_FOUND if _.isEmpty(req.userObj) - - types = - if /^(habit|todo|daily|reward)$/.test(req.query.type) then [req.query.type] - else ['habit','todo','daily','reward'] - tasks = _.toArray (_.filter req.user.get('tasks'), (t)-> t.type in types) - - res.json 200, tasks ### - PUT /user + Get User ### -router.put '/user', auth, (req, res, next) -> +api.getUser = (req, res, next) -> + uObj = req.habit.userObj + + uObj.stats.toNextLevel = algos.tnl uObj.stats.lvl + uObj.stats.maxHealth = 50 + + delete uObj.apiToken + if uObj.auth + delete uObj.auth.hashed_password + delete uObj.auth.salt + + req.habit.result = data: uObj + next() + +### + Register new user with uname / password +### +api.loginLocal = (req, res, next) -> + username = req.body.username + password = req.body.password + return res.json 401, err: 'No username or password' unless username and password + + model = req.getModel() + + q = model.query("users").withUsername(username) + q.fetch (err, result1) -> + return res.json 401, { err } if err + u1 = result1.get() + return res.json 401, err: 'Username not found' unless u1 # user not found + + # We needed the whole user object first so we can get his salt to encrypt password comparison + q = model.query("users").withLogin(username, utils.encryptPassword(password, u1.auth.local.salt)) + q.fetch (err, result2) -> + return res.json 401, { err } if err + + # joshua tree? + u2 = result2.get() + return res.json 401, err: 'Incorrect password' unless u2 + + req.habit ?= {} + req.habit.result = data: + id: u2.id + token: u2.apiToken + next() + +### + POST /user/auth/facebook +### +api.loginFacebook = (req, res, next) -> + {facebook_id, email, name} = req.body + return res.json 401, err: 'No facebook id provided' unless facebook_id + model = req.getModel() + q = model.query("users").withProvider('facebook', facebook_id) + q.fetch (err, result) -> + return res.json 401, { err } if err + u = result.get() + if u + req.habit ?= {} + req.habit.result = data: + id: u.id + token: u.apiToken + next() + 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." + +### + Update user + FIXME add documentation here +### +api.updateUser = (req, res, next) -> + {user} = req.habit # FIXME we need to do some crazy sanitiazation if they're using the old `PUT /user {data}` method. # The new `PUT /user {'stats.hp':50} # FIXME - one-by-one we want to widdle down this list, instead replacing each needed set path with API operations # Note: custom is for 3rd party apps - acceptableAttrs = 'achievements filters flags invitations items lastCron party preferences profile stats tags custom'.join(' ') + acceptableAttrs = 'achievements filters flags invitations items lastCron party preferences profile stats tags custom'.split(' ') series = [] _.each req.body, (v, k) -> if (_.find acceptableAttrs, (attr)-> k.indexOf(attr) is 0)? - series.push (cb) -> req.user.set(k, v, cb) + series.push (cb) -> req.habit.user.set(k, v, cb) async.series series, (err) -> return next(err) if err - res.json 201, helpers.derbyUserToAPI(user) + req.habit.result = data: helpers.derbyUserToAPI(user) + next() + +api.cron = (req, res, next) -> + {user} = req.habit + misc.batchTxn req.getModel(), (uObj, paths) -> + uObj = helpers.derbyUserToAPI(user) + algos.cron uObj, {paths} + , {user, done:next, cron:true} + +api.revive = (req, res, next) -> + {user} = req.habit + [uObj, paths] = [user.get(), {}] + algos.revive uObj, {paths} + setOps = [] + _.each paths, (v,k) -> + setOps.push ((reviveCb) -> user.set k, helpers.dotGet(k,uObj), reviveCb) + async.series setOps, next ### -POST new actions + ------------------------------------------------------------------------ + Batch Update + Run a bunch of updates all at once + ------------------------------------------------------------------------ ### -router.post '/batch-update', auth, (req, res, next) -> - model = req.getModel() - {user} = req +api.batchUpdate = (req, res, next) -> + {user} = req.habit performAction = (action, cb) -> - task = action.task ? {} + req.params.id = action.data?.id + req.params.direction = action.dir + req.body = action.data + switch action.op when "cron" - misc.batchTxn model, (uObj, paths) -> - uObj = helpers.derbyUserToAPI(user) - algos.cron uObj, {paths} - , {user, done:cb, cron:true} - + api.cron(req, res, cb) when "score" - return cb() unless user.get "tasks.#{task.id}" - sendScore = -> score(model, user, task.id, action.dir, cb) - if task.type in ["daily","todo"] - # switch completed state. Since checkbox is not binded to model unlike when you click through Derby website. - completed = if action.dir is "up" then true else false - user.set "tasks.#{task.id}.completed", completed, sendScore - else sendScore() - + api.scoreTask(req, res, cb) when "sortTask" - path = action.task.type + "Ids" - a = user.get(path) - a.splice(action.to, 0, a.splice(action.from, 1)[0]) - user.set path, a, cb - + api.sortTask(req, res, cb) when "addTask" - addTask user, task, cb - + api.validateTask req, res, -> + api.createTask(req, res, cb) when "delTask" - deleteTask user, task, cb - - # this API is only working with string or number variables. It should return error if object given or object is at the path. + api.deleteTask(req, res, cb) when "set" - oldValue = user.get(action.path) - if _.isObject(action.value) or _.isObject(oldValue) - console.error "action.value was an object, which isn't currently supported. Tyler - double check this" - cb() - else - user.set action.path, action.value, cb - + api.updateUser(req, res, cb) when "revive" - [uObj, paths] = [user.get(), {}] - algos.revive uObj, {paths} - setOps = _.map paths, (v,k) -> - (reviveCb) -> user.set k, helpers.dotGet(k,uObj), reviveCb - console.log setOps - async.serial setOps, cb - - else - cb() + api.revive(req, res, cb) + else cb() # Setup the array of functions we're going to call in parallel with async - # Start with cron - (req.body or= []).unshift({op: 'cron'}) - actions = _.transform (req.body), (result, action) -> - result.push (cb) -> performAction(action, cb) unless _.isEmpty(action) + actions = [{op: 'cron'}].concat(_.cloneDeep(req.body) ? []) # Start with cron + actions = _.transform (actions), (result, action) -> + unless _.isEmpty(action) + result.push (cb) -> performAction(action, cb) # call all the operations, then return the user object to the requester async.series actions, (err) -> return next(err) if err res.json 200, helpers.derbyUserToAPI(user) - console.log "Reply sent" - - -### - POST /user/tasks/:taskId/:direction -### -router.post '/user/task/:taskId/:direction', auth, scoreTask -router.post '/user/tasks/:taskId/:direction', auth, scoreTask - -module.exports = router -module.exports.auth = auth -module.exports.scoreTask = scoreTask # export so deprecated can call it \ No newline at end of file + console.log "Reply sent" \ No newline at end of file diff --git a/src/server/index.coffee b/src/server/index.coffee index a723545ae5..b235c949eb 100644 --- a/src/server/index.coffee +++ b/src/server/index.coffee @@ -76,7 +76,7 @@ mongo_store = new MongoStore {url: process.env.NODE_DB_URI}, -> .use(store.modelMiddleware()) .use(middleware.translate) # API should be hit before all other routes - .use('/api/v1', require('./api').middleware) + .use('/api/v1', require('./routes').middleware) .use(require('./deprecated').middleware) # Show splash page for newcomers .use(middleware.splash) diff --git a/src/server/routes.coffee b/src/server/routes.coffee new file mode 100644 index 0000000000..9fef8344ef --- /dev/null +++ b/src/server/routes.coffee @@ -0,0 +1,51 @@ +express = require 'express' +router = new express.Router() +api = require './api' + +### + ---------- /api/v1 API ------------ + Every url added to router is prefaced by /api/v1 + See ./routes/coffee for routes + + v1 API. Requires x-api-user (user id) and x-api-key (api key) headers, Test with: + $ cd node_modules/racer && npm install && cd ../.. + $ mocha test/api.mocha.coffee +### + +{auth, validateTask} = api + +### + We don't want the api functions to actually res.send results (unless there was an error) + because we'll be re-using the same functions when apiv2 rolls around, but returning different results. + So handle sending results for apiv1 here +### +v1Send = (req, res, next) -> + {result} = req.habit + if (result.code and result.data) then res.json result.code, result.data + else if result.code then res.send result.code + else if result.data then res.json result.data + else res.send 200 + +router.get '/status', (req, res) -> res.json status: 'up' + +# Scoring +router.post '/user/task/:id/:direction', auth, api.scoreTask, v1Send +router.post '/user/tasks/:id/:direction', auth, api.scoreTask, v1Send + +# Tasks +router.get '/user/tasks', auth, api.getTasks, v1Send # plural +router.get '/user/task/:id', auth, api.getTask, v1Send +router.put '/user/task/:id', auth, validateTask, api.updateTask, v1Send +router.post '/user/tasks', auth, api.updateTasks, v1Send # plural +router.delete '/user/task/:id', auth, validateTask, api.deleteTask, v1Send +router.post '/user/task', auth, validateTask, api.createTask, v1Send +router.put '/user/task/:id/sort', auth, validateTask, api.sortTask, v1Send + +# User +router.get '/user', auth, api.getUser, v1Send +router.post '/user/auth/local', api.loginLocal, v1Send +router.post '/user/auth/facebook', api.loginFacebook, v1Send +router.put '/user', auth, api.updateUser, v1Send +router.post '/user/batch-update', auth, api.batchUpdate # this one we're handling specially + +module.exports = router \ No newline at end of file diff --git a/test/api.mocha.coffee b/test/api.mocha.coffee index b0aeec4391..8b0656f6b6 100644 --- a/test/api.mocha.coffee +++ b/test/api.mocha.coffee @@ -379,7 +379,7 @@ describe 'API', -> .send(user: userUpdates) .end (res) -> expect(res.body.err).to.be undefined - expect(res.statusCode).to.be 201 + expect(res.statusCode).to.be 200 tasks = res.body.tasks expect(_.find(tasks,{id:habitId})).to.eql {id: habitId,text: 'hello2',notes: 'note2'} @@ -421,7 +421,7 @@ describe 'API', -> id: userAuth.facebook_id name: userAuth.name email: userAuth.email - console.log {newUser} + #console.log {newUser} model.set "users.#{id}", newUser, -> request.post("#{baseURL}/user/auth/facebook") @@ -434,17 +434,17 @@ describe 'API', -> #expect(res.body.token).to.be newUser.apiToken done() - it.only 'POST /api/v2', (done) -> + it 'PUT /api/v1/batch-update', (done) -> userBefore = {} # user.set "lastCron", +new Date #FIXME this shouldn't be handled here query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken) query.fetch (err, user) -> userBefore = user.get() - console.log {userBefore} + #console.log {userBefore} jsonRaw = [{"op":"score","task":{"completed":true,"date":null,"down":null,"id":"049ee706-7992-408f-8bdd-a0f87b6cddee","notes":null,"price":null,"priority":null,"streak":1,"text":"asdasd","type":"daily","up":null,"value":-15.159032750819472},"dir":"down"},{"op":"score","task":{"completed":true,"date":null,"down":null,"id":"049ee706-7992-408f-8bdd-a0f87b6cddee","notes":null,"price":null,"priority":null,"streak":1,"text":"asdasd","type":"daily","up":null,"value":-15.159032750819472},"dir":"up"},{},{},{},{},{"op":"score","task":{"completed":true,"date":null,"down":null,"id":"049ee706-7992-408f-8bdd-a0f87b6cddee","notes":null,"price":null,"priority":null,"streak":1,"text":"asdasd","type":"daily","up":null,"value":-16.63136866553572},"dir":"down"},{"op":"score","task":{"completed":true,"date":null,"down":null,"id":"049ee706-7992-408f-8bdd-a0f87b6cddee","notes":null,"price":null,"priority":null,"streak":1,"text":"asdasd","type":"daily","up":null,"value":-16.63136866553572},"dir":"up"},{},{},{"op":"score","task":{"completed":true,"date":null,"down":null,"history":[{"date":1370796966979,"value":-1.9263318037820194},{"date":1371394179245,"value":-9.632667221983818},{"date":1371987764419,"value":-8.899142684639843},{"date":1371901709065,"value":-8.697714139260505},{"date":1371987764419,"value":-9.947389352351902},{"date":1372072605105,"value":-8.65704735207246},{"date":1372185464158,"value":-9.905420946093672},{"date":1372348155721,"value":-8.616465915449927},{"date":1372404365115,"value":-7.369389857231249},{"date":1372619315210,"value":-6.16153655829917},{"date":1372797766170,"value":-4.990495966065229},{"date":1373266931264,"value":-12.356300657493207},{"date":1373322727209,"value":-10.983796434663102},{"date":1373407801484,"value":-9.658725758132753},{"date":1373639117325,"value":-8.377893411957398},{"date":1373719671601,"value":-7.138418159258412},{"date":1373826521297,"value":-5.902428899644443},{"date":1373839445467,"value":-8.264210410818091},{"date":1373929050162,"value":-8.224444248693572},{"date":1374058202835,"value":-9.459055183497052},{"date":1374102966396,"value":-8.184759693251706},{"date":1374187619046,"value":-6.951403643619735},{"date":1374342649005,"value":-5.72148344218024},{"date":1374434356841,"value":-8.072174632205511},{"date":1374562742400,"value":-10.571154014907808},{"date":1374886027789,"value":-16.097395454285916},{"date":1375011848715,"value":-17.607991906162557},{"date":1375436884647,"value":-16.037773949824242},{"date":1375563074478,"value":-17.546064223707074},{"date":1375568230260,"value":-19.11379232897715},{"date":1375734631073,"value":-20.745784396408645},{"date":1375785010434,"value":-18.767794224069412},{"date":1375826853480,"value":-18.636651213045212}],"id":"fe4b9061-eb58-468c-9b25-10c72be772e6","notes":"","price":null,"priority":null,"repeat":{"su":true,"m":true,"t":true,"w":true,"th":true,"f":true,"s":true},"streak":1,"tags":{"40492758-1202-4d85-8cb3-d40e45f4dd1d":false},"text":"Read 50 pages","type":"daily","up":null,"value":-17.024492021142215},"dir":"up"},{},{},{}] - request.post("http://localhost:1337/api/v2") + request.put("http://localhost:1337/api/v1/batch-update") .set('Accept', 'application/json') .set('X-API-User', currentUser.id) .set('X-API-Key', currentUser.apiToken) From 96b4f57adcd579c22e59f9cad837e12bbfdc286e Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 11 Aug 2013 00:50:00 -0400 Subject: [PATCH 36/53] good-old racer unshift/push bug. gotta manually set the id list, works like a charm :/ --- src/server/api.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 60121c6af3..4e01c080d6 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -49,7 +49,9 @@ api.auth = (req, res, next) -> addTask = (user, task, cb) -> task.type ?= 'habit' tid = user.add "tasks", task, -> - user.unshift "#{task.type}Ids", tid, cb + ids = user.get "#{task.type}Ids" + ids.unshift tid + user.set "#{task.type}Ids", ids, cb deleteTask = (user, task, cb) -> user.del "tasks.#{task.id}", -> From 5b71e93fb5483cf63c319df9ef50a6fc09aaf3b6 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 11 Aug 2013 00:56:47 -0400 Subject: [PATCH 37/53] api: allow PUT /user to update tasks for now, will change lager --- src/server/api.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 4e01c080d6..30c584399d 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -315,7 +315,7 @@ api.updateUser = (req, res, next) -> # FIXME - one-by-one we want to widdle down this list, instead replacing each needed set path with API operations # Note: custom is for 3rd party apps - acceptableAttrs = 'achievements filters flags invitations items lastCron party preferences profile stats tags custom'.split(' ') + acceptableAttrs = 'tasks achievements filters flags invitations items lastCron party preferences profile stats tags custom'.split(' ') series = [] _.each req.body, (v, k) -> if (_.find acceptableAttrs, (attr)-> k.indexOf(attr) is 0)? From eb28ae3dd28a04e39d94f0968fc24a9e2806f918 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 11 Aug 2013 00:58:23 -0400 Subject: [PATCH 38/53] api: bug fix on delete task --- src/server/api.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 30c584399d..7b402fbd3c 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -366,7 +366,8 @@ api.batchUpdate = (req, res, next) -> api.validateTask req, res, -> api.createTask(req, res, cb) when "delTask" - api.deleteTask(req, res, cb) + api.validateTask req, res, -> + api.deleteTask(req, res, cb) when "set" api.updateUser(req, res, cb) when "revive" From 23f31547643eeb8ce935187aa0752e2e825fc5cc Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 11 Aug 2013 10:06:10 -0400 Subject: [PATCH 39/53] api res.json error in batch --- src/server/api.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 7b402fbd3c..8031062236 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -382,6 +382,6 @@ api.batchUpdate = (req, res, next) -> # call all the operations, then return the user object to the requester async.series actions, (err) -> - return next(err) if err + return res.send 500, {err} if err res.json 200, helpers.derbyUserToAPI(user) console.log "Reply sent" \ No newline at end of file From 7a83be70b1abed165ced2cd2dda967ffb2a489ee Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 11 Aug 2013 10:20:27 -0400 Subject: [PATCH 40/53] woops, res.json the error --- src/server/api.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 8031062236..f015514a6e 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -382,6 +382,6 @@ api.batchUpdate = (req, res, next) -> # call all the operations, then return the user object to the requester async.series actions, (err) -> - return res.send 500, {err} if err + return res.json 500, {err} if err res.json 200, helpers.derbyUserToAPI(user) console.log "Reply sent" \ No newline at end of file From fe6f478a60a8a870c3d95dddd112564ee4f02049 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Sun, 11 Aug 2013 11:04:51 -0500 Subject: [PATCH 41/53] Remove unneeded class reference Finish removing image rotation --- public/500.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/500.html b/public/500.html index 2afed99308..b03f0740bb 100644 --- a/public/500.html +++ b/public/500.html @@ -25,7 +25,7 @@
- +

The server is respawning.

Try again in a few. We restart often due to this issue, and we're rewriting the site to fix it. (AngularJS developers, come join us!)

From 1b3056e63d1303820d0c0af13705f193f7d1b7c5 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 11 Aug 2013 12:20:00 -0400 Subject: [PATCH 42/53] some api bug-fixes --- src/app/misc.coffee | 4 ++-- src/server/api.coffee | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/misc.coffee b/src/app/misc.coffee index ef27421631..2529ddb1df 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -12,12 +12,12 @@ module.exports.batchTxn = batchTxn = (model, cb, options) -> paths = {} model._dontPersist = true ret = cb uObj, paths, batch - _.each paths, (v,k) -> user.pass({cron:options?.cron}).set(k,helpers.dotGet(k, uObj));true + _.each paths, (v,k) -> user.pass({cron:options?.cron}).set(k,batch.get(k));true model._dontPersist = false # some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js # pass true if we have levelled to supress xp notification unless _.isEmpty paths - setOps = _.reduce paths, ((m,v,k)-> m[k] = helpers.dotGet(k,uObj);m), {} + setOps = _.reduce paths, ((m,v,k)-> m[k] = batch.get(k);m), {} user.set "update__", setOps, options?.done else options?.done?() ret diff --git a/src/server/api.coffee b/src/server/api.coffee index f015514a6e..730754ebe3 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -383,5 +383,6 @@ api.batchUpdate = (req, res, next) -> # call all the operations, then return the user object to the requester async.series actions, (err) -> return res.json 500, {err} if err - res.json 200, helpers.derbyUserToAPI(user) - console.log "Reply sent" \ No newline at end of file + return res.json helpers.derbyUserToAPI(user) + console.log "Reply sent" + From 7ffb990759e25506df6c540034e2a051496611aa Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 11 Aug 2013 15:13:53 -0400 Subject: [PATCH 43/53] api: add task, res.send as callback --- src/server/api.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 730754ebe3..d4ccc33915 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -215,9 +215,9 @@ api.updateTasks = (req, res, next) -> api.createTask = (req, res, next) -> task = req.habit.task - addTask req.habit.user, task - req.habit.result = code: 201, data: task - next() + addTask req.habit.user, task, -> + req.habit.result = code: 201, data: task + next() api.sortTask = (req, res, next) -> {id} = req.params From 9c08c7f77b74b42ead803ef32c13184993bedc52 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 11 Aug 2013 19:37:39 -0400 Subject: [PATCH 44/53] api: some fixes to scoreTask - still having trouble with habits, but fixes dailies --- src/server/api.coffee | 57 ++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index d4ccc33915..632cff99fd 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -72,44 +72,39 @@ score = (model, user, taskId, direction, cb) -> ### api.scoreTask = (req, res, next) -> {id, direction} = req.params - {title, service, icon, type} = req.body + {title, service, type} = req.body type ||= 'habit' # Send error responses for improper API call return res.json 500, {err: ':id required'} unless id return res.json 500, {err: ":direction must be 'up' or 'down'"} unless direction in ['up','down'] - model = req.getModel() - {user, userObj} = req.habit + {user} = req.habit - existingTask = user.at "tasks.#{id}" - # TODO add service & icon to task - # If task exists, set it's compltion - if existingTask.get() - # Set completed if type is daily or todo - existingTask.set 'completed', (direction is 'up') if /^(daily|todo)$/.test existingTask.get('type') + done = -> + # TODO - could modify batchTxn to conform to this better + delta = score req.getModel(), user, id, direction, -> + result = user.get('stats') + req.habit.result = data: _.extend(result, delta: delta) + next() + + # Set completed if type is daily or todo and task exists + if (existing = user.at "tasks.#{id}").get() + if existing.get('type') in ['daily', 'todo'] + existing.set 'completed', (direction is 'up'), done + else done() + + # If it doesn't exist, this is likely a 3rd party up/down - create a new one else - task = - id: id - type: type - text: (title || id) - value: 0 - notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." - - switch type - when 'habit' - task.up = true - task.down = true - when 'daily', 'todo' - task.completed = direction is 'up' - - addTask user, task - - # TODO - could modify batchTxn to conform to this better - delta = score model, user, id, direction, -> - result = user.get('stats') - req.habit.result = data: _.extend(result, delta: delta) - next() + task = {id, type, value: 0} + task.text = title or id + task.notes = "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." + if type is 'habit' + task.up = true + task.down = true + if type in ['daily', 'todo'] + task.completed = direction is 'up' + addTask user, task, done ### Get all tasks @@ -383,6 +378,6 @@ api.batchUpdate = (req, res, next) -> # call all the operations, then return the user object to the requester async.series actions, (err) -> return res.json 500, {err} if err - return res.json helpers.derbyUserToAPI(user) + res.json helpers.derbyUserToAPI(user) console.log "Reply sent" From a70e7599abec7f58f85e8d25f1f4d045743c3bba Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 11 Aug 2013 19:56:22 -0400 Subject: [PATCH 45/53] very weird fix. batchTxn() pass in option to skip hydration, which sometimes achieves exactly the effect *hydrating* is supposed to achieve. very strange, but anyway this fixes the habit scoring --- src/app/misc.coffee | 15 +++++++++------ src/server/api.coffee | 4 ++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/app/misc.coffee b/src/app/misc.coffee index 2529ddb1df..83c7489935 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -3,23 +3,26 @@ algos = require 'habitrpg-shared/script/algos' items = require('habitrpg-shared/script/items').items helpers = require('habitrpg-shared/script/helpers') -module.exports.batchTxn = batchTxn = (model, cb, options) -> - user = options?.user or model.at("_user") - uObj = helpers.hydrate(user.get()) # see https://github.com/codeparty/racer/issues/116 +module.exports.batchTxn = batchTxn = (model, cb, options={}) -> + _.defaults options, {user: model.at("_user"), hydrate: true, cron: false, done: ->} + {user} = options + # see https://github.com/codeparty/racer/issues/116 + # But sometimes we get the exact opposite effect if we hydrate. I don't understand it, and I can't wait to start using Mongoose instead. + uObj = if options.hydrate then helpers.hydrate(user.get()) else user.get() batch = set: (k,v) -> helpers.dotSet(k,v,uObj); paths[k] = true get: (k) -> helpers.dotGet(k,uObj) paths = {} model._dontPersist = true ret = cb uObj, paths, batch - _.each paths, (v,k) -> user.pass({cron:options?.cron}).set(k,batch.get(k));true + _.each paths, (v,k) -> user.pass({cron:options.cron}).set(k,batch.get(k));true model._dontPersist = false # some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js # pass true if we have levelled to supress xp notification unless _.isEmpty paths setOps = _.reduce paths, ((m,v,k)-> m[k] = batch.get(k);m), {} - user.set "update__", setOps, options?.done - else options?.done?() + user.set "update__", setOps, options.done + else options.done() ret #TODO put this in habitrpg-shared diff --git a/src/server/api.coffee b/src/server/api.coffee index 632cff99fd..c17f0aa2a0 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -58,12 +58,12 @@ deleteTask = (user, task, cb) -> taskIds = user.get "#{task.type}Ids" user.remove "#{task.type}Ids", taskIds.indexOf(task.id), 1, cb -score = (model, user, taskId, direction, cb) -> +score = (model, user, taskId, direction, done) -> delta = 0 misc.batchTxn model, (uObj, paths) -> tObj = uObj.tasks[taskId] delta = algos.score(uObj, tObj, direction, {paths}) - , {user, done:cb} + , {user, done, hydrate: false} delta ### From c8a8c466ca43e41b88b65c9a75da136e2c0cfad7 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 11 Aug 2013 21:16:52 -0400 Subject: [PATCH 46/53] revert previous hydrate bit --- src/server/api.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index c17f0aa2a0..3e4eb704ae 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -63,7 +63,8 @@ score = (model, user, taskId, direction, done) -> misc.batchTxn model, (uObj, paths) -> tObj = uObj.tasks[taskId] delta = algos.score(uObj, tObj, direction, {paths}) - , {user, done, hydrate: false} + , {user, done} + #, {user, done, hydrate: false} delta ### From d8c1b720b979913a3ba1abe0f60cabd06c672e94 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 11 Aug 2013 21:50:19 -0400 Subject: [PATCH 47/53] augment previous hydrate: false - make sure on cron too. This is so weird.. --- src/server/api.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 3e4eb704ae..9111a1d24f 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -63,8 +63,8 @@ score = (model, user, taskId, direction, done) -> misc.batchTxn model, (uObj, paths) -> tObj = uObj.tasks[taskId] delta = algos.score(uObj, tObj, direction, {paths}) - , {user, done} - #, {user, done, hydrate: false} + #, {user, done} + , {user, done, hydrate: false} delta ### @@ -326,7 +326,7 @@ api.cron = (req, res, next) -> misc.batchTxn req.getModel(), (uObj, paths) -> uObj = helpers.derbyUserToAPI(user) algos.cron uObj, {paths} - , {user, done:next, cron:true} + , {user, done:next, cron:true, hydrate: false} api.revive = (req, res, next) -> {user} = req.habit From 61d0f51de7ef4b8337a0eceeab61daa553b7abd2 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 12 Aug 2013 20:05:41 -0400 Subject: [PATCH 48/53] remove hydrate-skips, cleanup api revive --- src/app/misc.coffee | 6 +++--- src/server/api.coffee | 17 +++++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/app/misc.coffee b/src/app/misc.coffee index 83c7489935..ada432134f 100644 --- a/src/app/misc.coffee +++ b/src/app/misc.coffee @@ -4,17 +4,17 @@ items = require('habitrpg-shared/script/items').items helpers = require('habitrpg-shared/script/helpers') module.exports.batchTxn = batchTxn = (model, cb, options={}) -> - _.defaults options, {user: model.at("_user"), hydrate: true, cron: false, done: ->} + _.defaults options, {user: model.at("_user"), cron: false, done: ->} {user} = options # see https://github.com/codeparty/racer/issues/116 - # But sometimes we get the exact opposite effect if we hydrate. I don't understand it, and I can't wait to start using Mongoose instead. - uObj = if options.hydrate then helpers.hydrate(user.get()) else user.get() + uObj = helpers.hydrate user.get() batch = set: (k,v) -> helpers.dotSet(k,v,uObj); paths[k] = true get: (k) -> helpers.dotGet(k,uObj) paths = {} model._dontPersist = true ret = cb uObj, paths, batch + console.log {cron: options.cron} _.each paths, (v,k) -> user.pass({cron:options.cron}).set(k,batch.get(k));true model._dontPersist = false # some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js diff --git a/src/server/api.coffee b/src/server/api.coffee index 9111a1d24f..ade8058fdf 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -64,7 +64,7 @@ score = (model, user, taskId, direction, done) -> tObj = uObj.tasks[taskId] delta = algos.score(uObj, tObj, direction, {paths}) #, {user, done} - , {user, done, hydrate: false} + , {user, done} delta ### @@ -326,16 +326,17 @@ api.cron = (req, res, next) -> misc.batchTxn req.getModel(), (uObj, paths) -> uObj = helpers.derbyUserToAPI(user) algos.cron uObj, {paths} - , {user, done:next, cron:true, hydrate: false} + , {user, done:next, cron:true} api.revive = (req, res, next) -> {user} = req.habit - [uObj, paths] = [user.get(), {}] - algos.revive uObj, {paths} - setOps = [] - _.each paths, (v,k) -> - setOps.push ((reviveCb) -> user.set k, helpers.dotGet(k,uObj), reviveCb) - async.series setOps, next + done = -> + req.habit.result = data: helpers.derbyUserToAPI(user) + next() + misc.batchTxn req.getModel(), (uObj, paths) -> + algos.revive uObj, {paths} + , {user, done} + ### ------------------------------------------------------------------------ From 552eabfef8f33e65bbb83f6d0c52df0f3487dc58 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 12 Aug 2013 20:06:30 -0400 Subject: [PATCH 49/53] add revive & cron to routes, add cron as interceptor to every API call --- src/server/api.coffee | 5 +---- src/server/routes.coffee | 31 ++++++++++++++++--------------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index ade8058fdf..544e7eecd3 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -353,8 +353,6 @@ api.batchUpdate = (req, res, next) -> req.body = action.data switch action.op - when "cron" - api.cron(req, res, cb) when "score" api.scoreTask(req, res, cb) when "sortTask" @@ -372,8 +370,7 @@ api.batchUpdate = (req, res, next) -> else cb() # Setup the array of functions we're going to call in parallel with async - actions = [{op: 'cron'}].concat(_.cloneDeep(req.body) ? []) # Start with cron - actions = _.transform (actions), (result, action) -> + actions = _.transform (req.body ? []), (result, action) -> unless _.isEmpty(action) result.push (cb) -> performAction(action, cb) diff --git a/src/server/routes.coffee b/src/server/routes.coffee index 9fef8344ef..db82857d0f 100644 --- a/src/server/routes.coffee +++ b/src/server/routes.coffee @@ -12,7 +12,7 @@ api = require './api' $ mocha test/api.mocha.coffee ### -{auth, validateTask} = api +{auth, validateTask, cron} = api ### We don't want the api functions to actually res.send results (unless there was an error) @@ -29,23 +29,24 @@ v1Send = (req, res, next) -> router.get '/status', (req, res) -> res.json status: 'up' # Scoring -router.post '/user/task/:id/:direction', auth, api.scoreTask, v1Send -router.post '/user/tasks/:id/:direction', auth, api.scoreTask, v1Send +router.post '/user/task/:id/:direction', auth, cron, api.scoreTask, v1Send +router.post '/user/tasks/:id/:direction', auth, cron, api.scoreTask, v1Send # Tasks -router.get '/user/tasks', auth, api.getTasks, v1Send # plural -router.get '/user/task/:id', auth, api.getTask, v1Send -router.put '/user/task/:id', auth, validateTask, api.updateTask, v1Send -router.post '/user/tasks', auth, api.updateTasks, v1Send # plural -router.delete '/user/task/:id', auth, validateTask, api.deleteTask, v1Send -router.post '/user/task', auth, validateTask, api.createTask, v1Send -router.put '/user/task/:id/sort', auth, validateTask, api.sortTask, v1Send +router.get '/user/tasks', auth, cron, api.getTasks, v1Send # plural +router.get '/user/task/:id', auth, cron, api.getTask, v1Send +router.put '/user/task/:id', auth, cron, validateTask, api.updateTask, v1Send +router.post '/user/tasks', auth, cron, api.updateTasks, v1Send # plural +router.delete '/user/task/:id', auth, cron, validateTask, api.deleteTask, v1Send +router.post '/user/task', auth, cron, validateTask, api.createTask, v1Send +router.put '/user/task/:id/sort', auth, cron, validateTask, api.sortTask, v1Send # User -router.get '/user', auth, api.getUser, v1Send -router.post '/user/auth/local', api.loginLocal, v1Send -router.post '/user/auth/facebook', api.loginFacebook, v1Send -router.put '/user', auth, api.updateUser, v1Send -router.post '/user/batch-update', auth, api.batchUpdate # this one we're handling specially +router.get '/user', auth, cron, api.getUser, v1Send +router.post '/user/auth/local', api.loginLocal, v1Send +router.post '/user/auth/facebook', api.loginFacebook, v1Send +router.put '/user', auth, cron, api.updateUser, v1Send +router.post '/user/revive', auth, cron, api.revive, v1Send +router.post '/user/batch-update', auth, cron, api.batchUpdate # this one we're handling specially module.exports = router \ No newline at end of file From 932dd6df69824047b792908b04149f629490eec3 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 12 Aug 2013 20:07:33 -0400 Subject: [PATCH 50/53] HUGE hotfix - don't process todo / daily scoring if that checkbox didn't come from the local model. This is the fix to the negative xp / gp bug!! --- src/app/tasks.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index 4403bb027b..945eb1d784 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -101,7 +101,7 @@ module.exports.app = (appExports, model) -> the above function doesn't work so we need a listener here ### user.on 'set', 'tasks.*.completed', (i, completed, previous, isLocal, passed) -> - return if passed?.cron # Don't do this stuff on cron + return if !isLocal or passed?.cron # Don't do this stuff on cron direction = if completed then 'up' else 'down' misc.score(model, i, direction, true) From f5d80d082982cf96d39cc207d84136d9514833c3 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 12 Aug 2013 20:19:51 -0400 Subject: [PATCH 51/53] api: make sure userObj is fresh between calls, since many of them will be chained and will modify user spec --- src/server/api.coffee | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 544e7eecd3..1a1db114b7 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -27,7 +27,7 @@ NO_USER_FOUND = err: "No user found." api.auth = (req, res, next) -> uid = req.headers['x-api-user'] token = req.headers['x-api-key'] - return res.json 401, NO_TOKEN_OR_UID unless uid || token + return res.json 401, NO_TOKEN_OR_UID unless uid and token model = req.getModel() query = model.query('users').withIdAndToken(uid, token) @@ -35,8 +35,7 @@ api.auth = (req, res, next) -> query.fetch (err, user) -> return res.json err: err if err (req.habit ?= {}).user = user - req.habit.userObj = user.get() - return res.json 401, NO_USER_FOUND if _.isEmpty(req.habit.userObj) + return res.json 401, NO_USER_FOUND if _.isEmpty(user.get()) req._isServer = true next() @@ -123,7 +122,7 @@ api.getTasks = (req, res, next) -> Get Task ### api.getTask = (req, res, next) -> - task = req.habit.userObj.tasks[req.params.id] + task = req.habit.user.get "tasks.#{req.params.id}" return res.json 400, err: "No task found." if !task || _.isEmpty(task) req.habit.result = data: task next() @@ -137,7 +136,7 @@ api.validateTask = (req, res, next) -> # If we're updating, get the task from the user if req.method is 'PUT' or req.method is 'DELETE' - task = req.habit.userObj?.tasks[req.params.id] + task = req.habit.user.get "tasks.#{req.params.id}" return res.json 400, err: "No task found." if !task || _.isEmpty(task) # Strip for now type = undefined @@ -234,7 +233,7 @@ api.sortTask = (req, res, next) -> Get User ### api.getUser = (req, res, next) -> - uObj = req.habit.userObj + uObj = req.habit.user.get() uObj.stats.toNextLevel = algos.tnl uObj.stats.lvl uObj.stats.maxHealth = 50 From 65f819046dc78cb9e929c50e91fbfc0d07b5a8e3 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 12 Aug 2013 20:20:07 -0400 Subject: [PATCH 52/53] return 400 if NO_USER_FOUND unecessary since handled in auth --- src/server/api.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 1a1db114b7..129ba9afe8 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -110,7 +110,6 @@ api.scoreTask = (req, res, next) -> Get all tasks ### api.getTasks = (req, res, next) -> - return res.json 400, NO_USER_FOUND if _.isEmpty(req.habit.userObj) types = if /^(habit|todo|daily|reward)$/.test(req.query.type) then [req.query.type] else ['habit','todo','daily','reward'] From ec17a5f3794f92d52f1a111d100531a7c1ec80ce Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 12 Aug 2013 20:41:19 -0400 Subject: [PATCH 53/53] pass in cron user as object, otherwise we're modifying a different user object --- src/server/api.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api.coffee b/src/server/api.coffee index 129ba9afe8..a234e63a23 100644 --- a/src/server/api.coffee +++ b/src/server/api.coffee @@ -322,7 +322,7 @@ api.updateUser = (req, res, next) -> api.cron = (req, res, next) -> {user} = req.habit misc.batchTxn req.getModel(), (uObj, paths) -> - uObj = helpers.derbyUserToAPI(user) + uObj = helpers.derbyUserToAPI(uObj, {asScope:false}) algos.cron uObj, {paths} , {user, done:next, cron:true}