diff --git a/migrations/20130204_count_habits.js b/migrations/20130204_count_habits.js new file mode 100644 index 0000000000..1459a97133 --- /dev/null +++ b/migrations/20130204_count_habits.js @@ -0,0 +1,13 @@ +// %mongo server:27017/dbname underscore.js my_commands.js +// %mongo server:27017/dbname underscore.js --shell +var habits = 0, + dailies = 0, + todos = 0, + registered = { $or: [ { 'auth.local': { $exists: true } }, { 'auth.facebook': { $exists: true} } ]}; + +db.user.find(registered).forEach(function(u){ + //TODO this isn't working?? + habits += _.where(u.tasks, {type:'habit'}).length; + dailies += _.where(u.tasks, {type:'daily'}).length; + todos += _.where(u.tasks, {type:'todo'}).length; +}) diff --git a/package.json b/package.json index ba7d6a4490..e4eb8e9832 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "main": "./server.js", "dependencies": { "derby": "git://github.com/codeparty/derby#master", - "racer": "git://github.com/lefnire/racer#master", - "racer-db-mongo": "git://github.com/codeparty/racer-db-mongo#master", + "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#master", "derby-auth": "git://github.com/lefnire/derby-auth#master", "connect-mongo": "0.2.0", diff --git a/src/app/browser.coffee b/src/app/browser.coffee index e52e367bd6..6e302ddeef 100644 --- a/src/app/browser.coffee +++ b/src/app/browser.coffee @@ -97,17 +97,33 @@ module.exports.setupGrowlNotifications = (model) -> user.on 'set', 'items.itemsEnabled', (captures, args) -> return unless captures == true + message = "Congratulations, you have unlocked the Item Store! You can now buy weapons, armor, potions, etc. Read each item's comment for more information." $('ul.items').popover - title: content.items.unlockedMessage.title + title: "Item Store Unlocked" placement: 'left' trigger: 'manual' html: true content: "
- - #{content.items.unlockedMessage.content} [Close] -
" + + #{message} [Close] + " $('ul.items').popover 'show' + user.on 'set', 'flags.partyEnabled', (captures, args) -> + return unless captures == true + message = "Congratulations, you have unlocked the Party System! You can now group with your friends by adding their User Ids." + $('#add-party-button').popover + title: "Pary System Unlocked" + placement: 'bottom' + trigger: 'manual' + html: true + content: "
+ + #{message} [Close] +
" + $('#add-party-button').popover 'show' + + # Setup listeners which trigger notifications user.on 'set', 'stats.hp', (captures, args) -> num = captures - args diff --git a/src/app/content.coffee b/src/app/content.coffee index 6750047ee1..00791b068e 100644 --- a/src/app/content.coffee +++ b/src/app/content.coffee @@ -55,9 +55,6 @@ module.exports = ] items: - unlockedMessage: - title: "Item Store Unlocked" - content: "Congratulations, you have unlocked the Item Store! You can now buy weapons, armor, potions, etc. Read each item's comment for more information." #TODO: figure out how to calculate index & type without having to store it in the JSON weapon: [ {type: 'weapon', index: 0, text: "Sword 1", icon: "item-sword1", notes:'Training weapon.', value:0} diff --git a/src/app/helpers.coffee b/src/app/helpers.coffee index da74ee35dc..1429b50168 100644 --- a/src/app/helpers.coffee +++ b/src/app/helpers.coffee @@ -54,4 +54,25 @@ module.exports.viewHelpers = (view) -> a < b view.fn "tokens", (money) -> - return money/0.25 \ No newline at end of file + return money/0.25 + + view.fn 'currentArmor', (gender, armor, armorSet) -> + if gender == 'f' + str = "armor#{armor}_f" + if parseInt(armor) > 1 + armorSet = if armorSet then armorSet else 'v1' + str += '_' + armorSet + return "#{str}.png" + else + return "armor#{armor}_m.png" + + view.fn "username", (auth) -> + if auth?.facebook?.displayName? + auth.facebook.displayName + else if auth?.facebook? + fb = auth.facebook + if fb._raw then "#{fb.name.givenName} #{fb.name.familyName}" else fb.name + else if auth?.local? + auth.local.username + else + 'Anonymous' \ No newline at end of file diff --git a/src/app/index.coffee b/src/app/index.coffee index 9eeb2a7dfe..40d7b4d29c 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -24,15 +24,9 @@ setupModelFns = (model) -> # also update in scoring.coffee. TODO create a function accessible in both locations (lvl*100)/5 - model.fn '_user._armor', '_user.items.armor', '_user.preferences.armorSet', '_user.preferences.gender', (armor, armorSet, gender) -> - if gender == 'f' - str = "armor#{armor}_f" - if parseInt(armor) > 1 - armorSet = if armorSet then armorSet else 'v1' - str += '_' + armorSet - return "#{str}.png" - else - "armor#{armor}_m.png" +# model.fn '_party', '_user.party', (ids) -> +# model.fetch model.query('users').party(ids), (err, party) -> +# model.set '_view.party', party # ========== ROUTES ========== @@ -51,18 +45,11 @@ get '/', (page, model, next) -> model.subscribe q, (err, user) -> #user = result.at(0) model.ref '_user', user - userObj = user.get() - - return page.redirect '/500.html' unless userObj? #this should never happen, but it is. Looking into it - - # support legacy Everyauth schema (now using derby-auth, Passport) - if username = userObj.auth?.local?.username - _view.loginName = username - else if fb = userObj.auth?.facebook - _view.loginName = if fb._raw then "#{fb.name.givenName} #{fb.name.familyName}" else fb.name + batch = new schema.BatchUpdate(model) + batch.startTransaction() # Setup Item Store - items = userObj.items + items = user.get('items') _view.items = armor: content.items.armor[parseInt(items?.armor || 0) + 1] weapon: content.items.weapon[parseInt(items?.weapon || 0) + 1] @@ -71,10 +58,17 @@ get '/', (page, model, next) -> model.set '_view', _view - schema.updateUser(user, userObj) + schema.updateUser(batch) + batch.commit() + setupListReferences(model) setupModelFns(model) + # Subscribe to friends + if !_.isEmpty(user.get('party')) + model.subscribe model.query('users').party(user.get('party')), (err, party) -> + model.ref '_party', party + page.render() # ========== CONTROLLER FUNCTIONS ========== @@ -86,13 +80,13 @@ resetDom = (model) -> ready (model) -> user = model.at('_user') + scoring.setModel(model) #set cron immediately lastCron = user.get('lastCron') - user.set('lastCron', +new Date) if (!lastCron or lastCron == 'new') + user.set('lastCron', +new Date) if (!lastCron? or lastCron == 'new') # Setup model in scoring functions - scoring.setModel(model) scoring.cron(resetDom) # Load all the jQuery, Growl, Tour, etc @@ -256,40 +250,38 @@ ready (model) -> task = model.at $(el).parents('li')[0] scoring.score(task.get('id'), direction) - revive = (userObj, animateHp = false) -> + revive = (batch) -> # Reset stats - userObj.stats.hp = 50 unless animateHp # if we're animating hp-reset, we'll set to 50 ourselves later in our functions - userObj.stats.lvl = 1; userObj.stats.money = 0; userObj.stats.exp = 0 + batch.set 'stats.hp', 50 + batch.set 'stats.lvl', 1 + batch.set 'stats.money', 0 + batch.set 'stats.exp', 0 # Reset items - userObj.items.armor = 0; userObj.items.weapon = 0 + batch.set 'items.armor', 0 + batch.set 'items.weapon', 0 # Reset item store model.set '_view.items.armor', content.items.armor[1] model.set '_view.items.weapon', content.items.weapon[1] exports.revive = (e, el) -> - userObj = user.get() - revive(userObj, true) - - user.set 'stats', userObj.stats - user.set 'items', userObj.items - # Re-render (since we replaced objects en-masse, see https://github.com/lefnire/habitrpg/issues/80) + batch = new schema.BatchUpdate(model) + batch.startTransaction() + revive(batch) + batch.commit() resetDom(model) - setTimeout (-> user.set 'stats.hp', 50), 0 # animate hp loss exports.reset = (e, el) -> - userObj = user.get() + batch = new schema.BatchUpdate(model) + batch.startTransaction() taskTypes = ['habit', 'daily', 'todo', 'reward'] - userObj.tasks = {} - _.each taskTypes, (type) -> userObj["#{type}Ids"] = [] - userObj.balance = 2 if userObj.balance < 2 #only if they haven't manually bought tokens - revive(userObj, true) - - # Set new user - model.set "users.#{userObj.id}", userObj + batch.set 'tasks', {} + _.each taskTypes, (type) -> batch.set "#{type}Ids", [] + batch.set 'balance', 2 if user.get('balance') < 2 #only if they haven't manually bought tokens + revive(batch, true) + batch.commit() resetDom(model) - setTimeout (-> user.set 'stats.hp', 50), 0 # animate hp loss exports.closeKickstarterNofitication = (e, el) -> user.set('notifications.kickstarter', 'hide') @@ -297,4 +289,28 @@ ready (model) -> exports.setMale = -> user.set('preferences.gender', 'm') exports.setFemale = -> user.set('preferences.gender', 'f') exports.setArmorsetV1 = -> user.set('preferences.armorSet', 'v1') - exports.setArmorsetV2 = -> user.set('preferences.armorSet', 'v2') \ No newline at end of file + exports.setArmorsetV2 = -> user.set('preferences.armorSet', 'v2') + + exports.addParty = -> + id = model.get('_newPartyMember').replace(/[\s"]/g, '') + debugger + return if _.isEmpty(id) + if user.get('party').indexOf(id) != -1 + model.set "_view.addPartyError", "#{id} already in party." + return + query = model.query('users').party([id]) + model.fetch query, (err, users) -> + partyMember = users.at(0).get() + if partyMember?.id? + user.push('party', id) + $('#add-party-modal').modal('hide') + window.location.reload() #TODO break old subscription, setup new subscript, remove this reload + model.set '_newPartyMember', '' + else + model.set "_view.addPartyError", "User with id #{id} not found." + + exports.emulateNextDay = -> + yesterday = +moment().subtract('days', 1).toDate() + user.set 'lastCron', yesterday + window.location.reload() + diff --git a/src/app/schema.coffee b/src/app/schema.coffee index 8ef6d927c3..d8f601cdb1 100644 --- a/src/app/schema.coffee +++ b/src/app/schema.coffee @@ -1,22 +1,27 @@ content = require './content' moment = require 'moment' _ = require 'underscore' +lodash = require 'lodash' derby = require 'derby' +userSchema = + lastCron: 'new' #this will be replaced with `+new Date` on first run + balance: 2 + stats: { money: 0, exp: 0, lvl: 1, hp: 50 } + items: { itemsEnabled: false, armor: 0, weapon: 0 } + notifications: { kickstarter: 'show' } + preferences: { gender: 'm', armorSet: 'v1' } + flags: { partyEnabled: false } + party: [] + tasks: {} + habitIds: [] + dailyIds: [] + todoIds: [] + rewardIds: [] + module.exports.newUserObject = -> # deep clone, else further new users get duplicate objects - newUser = require('lodash').cloneDeep - lastCron: 'new' #this will be replaced with `+new Date` on first run - balance: 2 - stats: { money: 0, exp: 0, lvl: 1, hp: 50 } - items: { itemsEnabled: false, armor: 0, weapon: 0 } - notifications: { kickstarter: 'show' } - preferences: { gender: 'm', armorSet: 'v1' } - tasks: {} - habitIds: [] - dailyIds: [] - todoIds: [] - rewardIds: [] + newUser = require('lodash').cloneDeep userSchema for task in content.defaultTasks guid = task.id = require('racer').uuid() newUser.tasks[guid] = task @@ -27,30 +32,90 @@ module.exports.newUserObject = -> when 'reward' then newUser.rewardIds.push guid return newUser -module.exports.updateUser = (user, userObj) -> - user.set 'notifications.kickstarter', 'show' unless userObj.notifications?.kickstarter? +module.exports.updateUser = (batch) -> + user = batch.user + + batch.set('notifications.kickstarter', 'show') unless user.get('notifications.kickstarter') + batch.set('party', []) unless !_.isEmpty(user.get('party')) # Preferences, including API key # Some side-stepping to avoid unecessary set (one day, model.update... one day..) - prefs = _.clone(userObj.preferences) - prefs = _.defaults prefs, { gender: 'm', armorSet: 'v1', api_token: derby.uuid() } - user.set 'preferences', prefs unless _.isEqual(prefs, userObj.preferences) + currentPrefs = _.clone user.get('preferences') + mergedPrefs = _.defaults currentPrefs, { gender: 'm', armorSet: 'v1', api_token: derby.uuid() } + batch.set('preferences', mergedPrefs) ## Task List Cleanup # FIXME temporary hack to fix lists (Need to figure out why these are happening) # FIXME consolidate these all under user.listIds so we can set them en-masse + tasks = user.get('tasks') _.each ['habit','daily','todo','reward'], (type) -> path = "#{type}Ids" # 1. remove duplicates # 2. restore missing zombie tasks back into list - where = {type:type} - taskIds = _.pluck( _.where(userObj.tasks, where), 'id') - union = _.union userObj[path], taskIds + taskIds = _.pluck( _.where(tasks, {type:type}), 'id') + union = _.union user.get(path), taskIds # 2. remove empty (grey) tasks preened = _.filter(union, (val) -> _.contains(taskIds, val)) # There were indeed issues found, set the new list - # TODO _.difference might still be empty for duplicates in one list? - user.set(path, preened) if _.difference(preened, userObj[path]).length != 0 \ No newline at end of file + batch.set(path, preened) # if _.difference(preened, userObj[path]).length != 0 + +module.exports.BatchUpdate = BatchUpdate = (model) -> + user = model.at("_user") + transactionInProgress = false + obj = {} + updates = {} + + { + user: user + + obj: -> obj + + startTransaction: -> + # start a batch transaction - nothing between now and @commit() will be set immediately + transactionInProgress = true + model._dontPersist = true + + # Really strange, user.get() seems to only return attributes which have previously been accessed. So in + # many cases, userObj.tasks.{taskId}.value is undefined - so we manually .get() each attribute here. + # Additionally, for some reason after getting the user object, changing properies manually (userObj.stats.hp = 50) + # seems to actually run user.set('stats.hp',50) which we don't want to do - so we deepClone here + #_.each Object.keys(userSchema), (key) -> obj[key] = lodash.cloneDeep user.get(key) + obj = model.get('users.'+user.get('id'), true) + + ### + Handles updating the user model. If this is an en-mass operation (eg, server cron), changes are queued + but not actually set to the model. It also modifies userObj in case you need to access properties manually later. + If transaction not in progress, it just runs standard model.set() + ### + set: (path, val) -> + updates[path] = val if transactionInProgress + user.set(path, val) + + ### + Hack to get around dom bindings being lost if parent objects are replaced whole-sale + eg, user.set('stats', {hp:50, exp:10...}) will break dom bindings, but user.set('stats.hp',50) is ok + ### + setStats: (stats) -> + stats ?= obj.stats + that = @ + _.each Object.keys(stats), (key) -> that.set "stats.#{key}", stats[key] + +# queue: (path, val) -> +# # Special function for setting object properties by string dot-notation. See http://stackoverflow.com/a/6394168/362790 +# arr = path.split('.') +# arr.reduce (curr, next, index) -> +# if (arr.length - 1) == index +# curr[next] = val +# curr[next] +# , obj + + commit: -> + model._dontPersist = false + # some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js + user.set "update__", updates + transactionInProgress = false + updates = {} + } diff --git a/src/app/scoring.coffee b/src/app/scoring.coffee index c6f5dae618..6a3de2725e 100644 --- a/src/app/scoring.coffee +++ b/src/app/scoring.coffee @@ -4,7 +4,8 @@ _ = require 'underscore' content = require './content' helpers = require './helpers' browser = require './browser' -MODIFIER = .03 # each new level, armor, weapon add 3% modifier (this number may change) +schema = require './schema' +MODIFIER = .03 # each new level, armor, weapon add 3% modifier (this number may change) user = undefined model = undefined @@ -52,73 +53,67 @@ taskDeltaFormula = (currentValue, direction) -> delta = if (currentValue < 0) then (( -0.1 * currentValue + 1 ) * sign) else (( Math.pow(0.9,currentValue) ) * sign) return delta - -### - Handles updating the user model. If this is an en-mass operation (eg, server cron), pass the user object as {update}. - otherwise, null means commit the changes immediately -### -userSet = (path, value, update) -> - if update - # Special function for setting object properties by string dot-notation. See http://stackoverflow.com/a/6394168/362790 - arr = path.split('.') - arr.reduce (curr, next, index) -> - if (arr.length - 1) == index - curr[next] = value - curr[next] - , update - else - user.set path, value - ### Updates user stats with new stats. Handles death, leveling up, etc {stats} new stats {update} if aggregated changes, pass in userObj as update. otherwise commits will be made immediately ### -updateStats = (newStats, update) -> - userObj = update || user.get() +updateStats = (newStats, batch) -> + obj = batch.obj() # if user is dead, dont do anything - return if userObj.stats.lvl == 0 - + return if obj.stats.lvl == 0 + if newStats.hp? # Game Over if newStats.hp <= 0 - userSet 'stats.lvl', 0, update # signifies dead - userSet 'stats.hp', 0, update + obj.stats.lvl = 0 # signifies dead + obj.stats.hp = 0 return else - userSet 'stats.hp', newStats.hp, update + obj.stats.hp = newStats.hp if newStats.exp? # level up & carry-over exp tnl = user.get '_tnl' if newStats.exp >= tnl newStats.exp -= tnl - userSet 'stats.lvl', userObj.stats.lvl + 1, update - userSet 'stats.hp', 50, update - if !userObj.items?.itemsEnabled and newStats.exp >=15 - user.set 'items.itemsEnabled', true #bit of trouble using userSet here - userSet 'stats.exp', newStats.exp, update + obj.stats.lvl++ + obj.stats.hp = 50 + if !obj.items.itemsEnabled and obj.stats.lvl >= 2 + # Set to object, then also send to browser right away to get model.on() subscription notification + batch.set 'items.itemsEnabled', true + obj.items.itemsEnabled = true +# if !obj.flags.partyEnabled and obj.stats.lvl >= 3 +# batch.set 'flags.partyEnabled', true +# obj.flags.partyEnabled = true + obj.stats.exp = newStats.exp if newStats.money? + #FIXME what was I doing here? I can't remember, money isn't defined money = 0.0 if (!money? or money<0) - userSet 'stats.money', newStats.money, update + obj.stats.money = newStats.money # {taskId} task you want to score # {direction} 'up' or 'down' # {times} # times to call score on this task (1 unless cron, usually) # {update} if we're running updates en-mass (eg, cron on server) pass in userObj -score = (taskId, direction, times, update) -> - times ?= 1 +score = (taskId, direction, times, batch, cron) -> + commit = false + unless batch? + commit = true + batch = new schema.BatchUpdate(model) + batch.startTransaction() + obj = batch.obj() - userObj = update or user.get() - {money, hp, exp, lvl} = userObj.stats + {money, hp, exp, lvl} = obj.stats taskPath = "tasks.#{taskId}" - taskObj = userObj.tasks[taskId] + taskObj = obj.tasks[taskId] {type, value} = taskObj delta = 0 + times ?= 1 calculateDelta = (adjustvalue=true) -> # If multiple days have passed, multiply times days missed _.times times, (n) -> @@ -144,20 +139,23 @@ score = (taskId, direction, times, update) -> adjustvalue = if (taskObj.up==false or taskObj.down==false) then false else true calculateDelta(adjustvalue) # Add habit value to habit-history (if different) - historyEntry = { date: +new Date(), value: value } if taskObj.value != value if (delta > 0) then addPoints() else subtractPoints() - model.push "_user.#{taskPath}.history", historyEntry + taskObj.history ?= [] + if taskObj.value != value + historyEntry = { date: +new Date, value: value } + taskObj.history.push historyEntry + batch.set "#{taskPath}.history", taskObj.history when 'daily' calculateDelta() - if update? # cron + if cron? # cron subtractPoints() else addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes when 'todo' calculateDelta() - unless update? # don't touch stats on cron + unless cron? # don't touch stats on cron addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes when 'reward' @@ -171,8 +169,17 @@ score = (taskId, direction, times, update) -> hp += money # hp - money difference money = 0 - userSet "#{taskPath}.value", value, update - updateStats {hp: hp, exp: exp, money: money}, update + taskObj.value = value + batch.set "#{taskPath}.value", taskObj.value + origStats = _.clone obj.stats + updateStats {hp: hp, exp: exp, money: money}, batch + if commit + # newStats / origStats is a glorious hack to trick Derby into seeing the change in model.on(*) + newStats = _.clone batch.obj().stats + _.each Object.keys(origStats), (key) -> obj.stats[key] = origStats[key] + batch.setStats(newStats) +# batch.setStats() + batch.commit() return delta ### @@ -183,56 +190,61 @@ cron = (resetDom_cb) -> today = +new Date daysPassed = helpers.daysBetween(today, user.get('lastCron')) if daysPassed > 0 - user.set 'lastCron', today - userObj = user.get() - hpBefore = userObj.stats.hp #we'll use this later so we can animate hp loss + batch = new schema.BatchUpdate(model) + batch.startTransaction() + batch.set 'lastCron', today + obj = batch.obj() + hpBefore = obj.stats.hp #we'll use this later so we can animate hp loss # Tally each task todoTally = 0 - _.each userObj.tasks, (taskObj) -> - #FIXME remove broken tasks - if taskObj.id? # a task had a null id during cron, this should not be happening - {id, type, completed, repeat} = taskObj - if type in ['todo', 'daily'] - # Deduct experience for missed Daily tasks, - # but not for Todos (just increase todo's value) - unless completed - # for todos & typical dailies, these are equivalent - daysFailed = daysPassed - # however, for dailys which have repeat dates, need - # to calculate how many they've missed according to their own schedule - if type=='daily' && repeat - daysFailed = 0 - _.times daysPassed, (n) -> - thatDay = moment().subtract('days', n+1) - if repeat[helpers.dayMapping[thatDay.day()]]==true - daysFailed++ - score id, 'down', daysFailed, userObj + _.each obj.tasks, (taskObj) -> + unless taskObj.id? + console.error "a task had a null id during cron, this should not be happening" + return - value = taskObj.value #get updated value - if type == 'daily' - taskObj.history ?= [] - taskObj.history.push { date: today, value: value } - taskObj.completed = false - else - absVal = if (completed) then Math.abs(value) else value - todoTally += absVal - user.set 'tasks.' + taskObj.id, taskObj + {id, type, completed, repeat} = taskObj + if type in ['todo', 'daily'] + # Deduct experience for missed Daily tasks, + # but not for Todos (just increase todo's value) + unless completed + # for todos & typical dailies, these are equivalent + daysFailed = daysPassed + # however, for dailys which have repeat dates, need + # to calculate how many they've missed according to their own schedule + if type=='daily' && repeat + daysFailed = 0 + _.times daysPassed, (n) -> + thatDay = moment().subtract('days', n+1) + if repeat[helpers.dayMapping[thatDay.day()]]==true + daysFailed++ + score id, 'down', daysFailed, batch, true + + if type == 'daily' + taskObj.history ?= [] + taskObj.history.push { date: +new Date, value: value } + batch.set "tasks.#{taskObj.id}.history", taskObj.history + batch.set "tasks.#{taskObj.id}.completed", false + else + value = obj.tasks[taskObj.id].value #get updated value + absVal = if (completed) then Math.abs(value) else value + todoTally += absVal # Finished tallying - userObj.history ?= {}; userObj.history.todos ?= []; userObj.history.exp ?= [] - userObj.history.todos.push { date: today, value: todoTally } + obj.history ?= {}; obj.history.todos ?= []; obj.history.exp ?= [] + obj.history.todos.push { date: today, value: todoTally } # tally experience - expTally = userObj.stats.exp + expTally = obj.stats.exp lvl = 0 #iterator - while lvl < (userObj.stats.lvl-1) + while lvl < (obj.stats.lvl-1) lvl++ expTally += (lvl*100)/5 - userObj.history.exp.push { date: today, value: expTally } + obj.history.exp.push { date: today, value: expTally } # Set the new user specs, and animate HP loss - [hpAfter, userObj.stats.hp] = [userObj.stats.hp, hpBefore] - user.set 'stats', userObj.stats - user.set 'history', userObj.history + [hpAfter, obj.stats.hp] = [obj.stats.hp, hpBefore] + batch.setStats() + batch.set('history', obj.history) + batch.commit() resetDom_cb(model) setTimeout (-> user.set 'stats.hp', hpAfter), 1000 # animate hp loss diff --git a/src/server/index.coffee b/src/server/index.coffee index 1ac2071144..e7a4e5de14 100644 --- a/src/server/index.coffee +++ b/src/server/index.coffee @@ -10,16 +10,16 @@ auth = require 'derby-auth' priv = require './private' ## Run server cron ## -#require('./cron').deleteStaleAccounts() +require('./cron').deleteStaleAccounts() ## RACER CONFIGURATION ## racer = require 'racer' racer.io.set('transports', ['xhr-polling']) racer.set('bundleTimeout', 40000) -unless process.env.NODE_ENV == 'production' - racer.use(racer.logPlugin) - derby.use(derby.logPlugin) +#unless process.env.NODE_ENV == 'production' +# racer.use(racer.logPlugin) +# derby.use(derby.logPlugin) ## SERVER CONFIGURATION ## diff --git a/src/server/private.coffee b/src/server/private.coffee index 6a02c4c5ea..2b7898bb6a 100644 --- a/src/server/private.coffee +++ b/src/server/private.coffee @@ -1,6 +1,69 @@ +_ = require 'underscore' + module.exports.middleware = (req, res, next) -> + model = req.getModel() + model.set '_stripePubKey', process.env.STRIPE_PUB_KEY return next() -module.exports.app= (appExports, model) -> +module.exports.app = (appExports, model) -> -module.exports.routes = (expressApp) -> \ No newline at end of file + appExports.showStripe = (e, el) -> + token = (res) -> + console.log(res); + $.ajax({ + type:"POST", + url:"/charge", + data:res + }).success -> + window.location.href = "/" + .error (err) -> + alert err.responseText + + StripeCheckout.open + key: model.get('_stripePubKey') + address: false + amount: 500 + name: "Checkout" + description: "Removes ads and grants 20 additional tokens." + panelLabel: "Checkout" + token: token + + ### + Buy Reroll Button + ### + appExports.buyReroll = (e, el, next) -> + user = model.at('_user') + tasks = user.get('tasks') + user.set('balance', user.get('balance')-1) + _.each tasks, (task) -> task.value = 0 unless task.type == 'reward' + user.set('tasks', tasks) + window.DERBY.app.dom.clear() + window.DERBY.app.view.render(model) + +module.exports.routes = (expressApp) -> + ### + Setup Stripe response when posting payment + ### + expressApp.post '/charge', (req, res) -> + stripeCallback = (err, response) -> + if err + console.error(err, 'Stripe Error') + return res.send(500, err.response.error.message) + else + model = req.getModel() + userId = model.session.userId + model.fetch "users.#{userId}", (err, user) -> + model.ref '_user', "users.#{userId}" + model.set('_user.balance', model.get('_user.balance')+5) + model.set('_user.flags.ads','hide') + return res.send(200) + + api_key = process.env.STRIPE_API_KEY # secret stripe API key + stripe = require("stripe")(api_key) + token = req.body.id + # console.dir {token:token, req:req}, 'stripe' + stripe.charges.create + amount: "500" # $5 + currency: "usd" + card: token + , stripeCallback \ No newline at end of file diff --git a/src/server/serverRoutes.coffee b/src/server/serverRoutes.coffee index 5b5235eba4..abd0ca8ee2 100644 --- a/src/server/serverRoutes.coffee +++ b/src/server/serverRoutes.coffee @@ -12,35 +12,38 @@ module.exports = (expressApp, root, derby) -> expressApp.get '/terms', (req, res) -> staticPages.render 'terms', res - # ---------- REST API ------------ + # ---------- Deprecated Paths ------------ - # Deprecated API (will remove soon) - deprecatedMessage = 'This REST resource is no longer supported, use /users/:uid/tasks/:taskId/:direction instead.' - expressApp.get '/:uid/up/:score?', (req, res) -> - res.send(200, deprecatedMessage) - expressApp.get '/:uid/down/:score?', (req, res) -> - res.send(200, deprecatedMessage) + deprecatedMessage = 'This API is no longer supported, see https://github.com/lefnire/habitrpg/wiki/API for new protocol' + expressApp.get '/:uid/up/:score?', (req, res) -> res.send(500, deprecatedMessage) + expressApp.get '/:uid/down/:score?', (req, res) -> res.send(500, deprecatedMessage) + expressApp.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) -> res.send(500, deprecatedMessage) - # New API - # test with `curl -X POST -H "Content-Type:application/json" localhost:3000/users/{uid}/tasks/productivity/up` + # ---------- v1 API ------------ + + ### + v1 API. Requires user-id and api_token, task-id, direction. Test with: + curl -X POST -H "Content-Type:application/json" -d '{"api_token":"{TOKEN}"}' localhost:3000/v1/users/{UID}/tasks/productivity/up + ### + # TODO /v1/.. expressApp.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> {uid, taskId, direction} = req.params - {title, service, icon} = req.body + {api_token, title, service, icon} = req.body console.log {params:req.params, body:req.body} if process.env.NODE_ENV == 'development' # Send error responses for improper API call + return res.send(500, 'request body "api_token" required') unless api_token return res.send(500, ':uid required') unless uid 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() - model.fetch "users.#{uid}", (err, user) -> + model.fetch model.query('users').withIdAndToken(uid, api_token), (err, result) -> return res.send(500, err) if err + user = result.at(0) userObj = user.get() - # Server crashes without this, I think some users are entering non-guid userIds and/or trying to use the API without having an account - unless userObj && !_.isEmpty(userObj.stats) - console.log {taskId:taskId, direction:direction, user:userObj, error: 'non-user attempted to score'} if process.env.NODE_ENV == 'development' - return res.send(500, "User #{uid} not found") + if _.isEmpty(userObj) + return res.send(500, "User with uid=#{uid}, token=#{api_token} not found. Make sure you're not using your username, but your User Id") model.ref('_user', user) diff --git a/src/server/store.coffee b/src/server/store.coffee index a7dedfaafb..d071a2a37e 100644 --- a/src/server/store.coffee +++ b/src/server/store.coffee @@ -14,4 +14,27 @@ module.exports = (store) -> return unless @session and @session.userId # https://github.com/codeparty/racer/issues/37 next = arguments[arguments.length - 1] isServer = not @req.socket - next(isServer) \ No newline at end of file + next(isServer) + + ### + Get user with API token + ### + store.query.expose "users", "withIdAndToken", (id, api_token) -> + @where("id").equals(id) + .where('preferences.api_token').equals(api_token) + .limit(1) + + store.queryAccess "users", "withIdAndToken", (id, token, next) -> + return next(false) unless @session and @session.userId # https://github.com/codeparty/racer/issues/37 + isServer = not @req.socket + next(isServer) + + ### + Party permissions + ### + store.query.expose "users", "party", (ids) -> + @where("id").within(ids) + .only('stats', 'preferences.gender', 'preferences.armorSet', 'items', 'auth.local.username', 'auth.facebook.displayName') + + store.queryAccess "users", "party", (ids, next) -> + next(true) # no harm in public user stats diff --git a/styles/app/index.styl b/styles/app/index.styl index 79a0db9fb2..5ffb9dfa96 100644 --- a/styles/app/index.styl +++ b/styles/app/index.styl @@ -117,7 +117,10 @@ li:hover .task-meta-controls .hover-show float:none margin:0px auto - td#avatar + td + padding-right: 2em + + td.avatar vertical-align: top text-align: center @@ -136,10 +139,9 @@ li:hover .task-meta-controls .hover-show .weapon-6 left:-20px td#bars - padding: 10px 0 0 10px + padding-top: 10px #bars - width: 100% .progress position: relative height: 25px diff --git a/views/app/index.html b/views/app/index.html index f9bd1f0309..66c9b504de 100644 --- a/views/app/index.html +++ b/views/app/index.html @@ -8,10 +8,15 @@ {#if _loggedIn} - User ID
- Copy this ID for use in third party applications. -
{_user.id}

+

API

+ Copy these for use in third party applications. +
User ID
+
{_user.id}
+
API Token
+
{_user.preferences.api_token}
+ +

Gender

+ {else} Login / Register With Facebook @@ -102,6 +108,16 @@ + +
+ {#if _view.addPartyError} +
{_view.addPartyError}
+ {/} + + +
+
+ {#if _flash.error}