Merge branch 'parties'

Conflicts:
	src/app/schema.coffee
This commit is contained in:
Tyler Renelle
2013-02-04 17:44:22 -05:00
14 changed files with 459 additions and 189 deletions
+13
View File
@@ -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;
})
+2 -2
View File
@@ -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",
+20 -4
View File
@@ -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: "<div class='item-store-popover'>
<img src='/img/BrowserQuest/chest.png' />
#{content.items.unlockedMessage.content} <a href='#' onClick=\"$('ul.items').popover('hide');return false;\">[Close]</a>
</div>"
<img src='/img/BrowserQuest/chest.png' />
#{message} <a href='#' onClick=\"$('ul.items').popover('hide');return false;\">[Close]</a>
</div>"
$('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: "<div class='party-system-popover'>
<img src='/img/BrowserQuest/favicon.png' />
#{message} <a href='#' onClick=\"$('#add-party-button').popover('hide');return false;\">[Close]</a>
</div>"
$('#add-party-button').popover 'show'
# Setup listeners which trigger notifications
user.on 'set', 'stats.hp', (captures, args) ->
num = captures - args
-3
View File
@@ -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}
+22 -1
View File
@@ -54,4 +54,25 @@ module.exports.viewHelpers = (view) ->
a < b
view.fn "tokens", (money) ->
return money/0.25
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'
+59 -43
View File
@@ -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')
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()
+87 -22
View File
@@ -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
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 = {}
}
+93 -81
View File
@@ -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
+4 -4
View File
@@ -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 ##
+65 -2
View File
@@ -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) ->
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
+18 -15
View File
@@ -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)
+24 -1
View File
@@ -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)
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
+5 -3
View File
@@ -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
+47 -8
View File
@@ -8,10 +8,15 @@
<modalDialogs:>
{#if _loggedIn}
<app:myModal modalId="settings-modal" header="Settings">
<strong>User ID</strong><br/>
<small>Copy this ID for use in third party applications.</small>
<pre class=prettyprint>{_user.id}</pre><br/>
<h4>API</h4>
<small>Copy these for use in third party applications.</small>
<h6>User ID</h6>
<pre class=prettyprint>{_user.id}</pre>
<h6>API Token</h6>
<pre class=prettyprint>{_user.preferences.api_token}</pre>
<hr/>
<h4>Gender</h4>
<label class="radio">
<input type="radio" name="genderRadios" value="m" x-bind="click:setMale" checked="{equal(_user.preferences.gender,'m')}">
@@ -56,6 +61,7 @@
<button data-dismiss="modal" class="btn btn-success">Ok</button>
</@footer>
</app:myModal>
{else}
<app:myModal modalId="login-modal" header="Login / Register">
<a href="/auth/facebook"><img src='/img/facebook-login-register.jpeg' alt="Login / Register With Facebook"/></a>
@@ -102,6 +108,16 @@
</@footer>
</app:myModal>
<app:myModal modalId="add-party-modal" header="Add Party Member">
<form x-bind="submit: addParty">
{#if _view.addPartyError}
<div class='alert alert-danger'>{_view.addPartyError}</div>
{/}
<input type="text" class="input-medium search-query" value="{_newPartyMember}">
<input type="submit" class="btn" value="Add" />
</form>
</app:myModal>
<alerts:>
{#if _flash.error}
<ul class="alert alert-error">
@@ -126,7 +142,7 @@
<a href="#" class="btn btn-small btn-info" data-target="#login-modal" data-toggle="modal">Login / Register</a>
{else}
<div class="btn-group">
<button class="btn btn-small">{_view.loginName}</button>
<button class="btn btn-small">{username(_user.auth)}</button>
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
</button>
@@ -142,17 +158,21 @@
<div class='container-fluid'>
<div class='row-fluid'>
<div id=character class='span4'>
<div id=character class='{#if _party}span9{else}span5{/}'>
<table>
<tr>
<td id="avatar">
<!-- Avatar -->
<td class="avatar main-avatar">
<div class='avatar-sprites'>
<img class='weapon weapon-{_user.items.weapon}' src="/img/BrowserQuest/habitrpg_mods/weapon{_user.items.weapon}.png" />
<img class='armor armor-{_user.items.armor}' src="/img/BrowserQuest/habitrpg_mods/{_user._armor}" />
<img class='armor armor-{_user.items.armor}' src="/img/BrowserQuest/habitrpg_mods/{currentArmor(_user.preferences.gender, _user.items.armor, _user.preferences.armorSet)}" />
</div>
<div id="lvl"><span class="badge badge-info">Lvl {_user.stats.lvl}</span></div>
</td>
<td id="bars">
<!-- Progress Bars -->
<td id="bars" style="width:{#if _party}70%{else}90%{/};">
<div class="progress progress-danger" rel=tooltip data-placement=bottom title="Health">
<div class="bar" style="width: {percent(_user.stats.hp, 50)}%;"></div>
<span class="progress-text"><i class=icon-heart></i> {round(_user.stats.hp)} / 50</span>
@@ -168,7 +188,23 @@
</span>
</div>
</td>
<!-- Party -->
{#if _user.flags.partyEnabled}
{#each _party as :member}
<td class="avatar party-avatar" rel="tooltip" title="{username(:member.auth)}" data-placement="bottom" >
<div class='avatar-sprites'>
<img class='weapon weapon-{:member.items.weapon}' src="/img/BrowserQuest/habitrpg_mods/weapon{:member.items.weapon}.png" />
<img class='armor armor-{:member.items.armor}' src="/img/BrowserQuest/habitrpg_mods/{currentArmor(:member.preferences.gender, :member.items.armor, :member.preferences.armorSet)}" />
</div>
<div id="lvl"><span class="badge badge-info">Lvl {:member.stats.lvl}</span></div>
</td>
{/}
<td><a class="btn" id="add-party-button" data-target="#add-party-modal" data-toggle="modal"><i class="icon-user"></i></a></td>
{/}
</tr>
</table>
<div id="exp-chart" style="display:none;"></div>
</div>
@@ -272,6 +308,9 @@
<!-- Footer -->
<footer class=footer>
<div class=container>
<!--<div class='pull-right'>
<button class='btn' x-bind="click:emulateNextDay">Emulate Next Day</button>
</div>-->
<div>
<ul>
<li>Copyright &copy; 2012 OCDevel LLC</li>