Merge branch 'develop' into apiv2

Conflicts:
	src/server/apiv2.coffee
This commit is contained in:
Tyler Renelle
2013-07-19 14:59:09 -04:00
47 changed files with 2236 additions and 1195 deletions
+6 -4
View File
@@ -222,10 +222,12 @@ module.exports.app = (appExports, model, app) ->
app.on 'render', (ctx) ->
#restoreRefs(model)
setupSortable(model)
setupTooltips(model)
setupTour(model)
initStickyHeader(model) unless model.get('_mobileDevice')
unless model.get('_mobileDevice')
setupTooltips(model)
initStickyHeader(model)
setupSortable(model)
setupTour(model)
$('.datepicker').datepicker({autoclose:true, todayBtn:true})
.on 'changeDate', (ev) ->
#for some reason selecting a date doesn't fire a change event on the field, meaning our changes aren't saved
+83
View File
@@ -0,0 +1,83 @@
_ = require 'lodash'
helpers = require 'habitrpg-shared/script/helpers'
module.exports.app = (appExports, model) ->
browser = require './browser'
user = model.at '_user'
$('#profile-challenges-tab-link').on 'show', (e) ->
_.each model.get('groups'), (g) ->
_.each g.challenges, (chal) ->
_.each ['habit','daily','todo'], (type) ->
_.each chal["#{type}s"], (task) ->
_.each chal.users, (member) ->
if (history = member?["#{type}s"]?[task.id]?.history) and !!history
data = google.visualization.arrayToDataTable _.map(history, (h)-> [h.date,h.value])
options =
backgroundColor: { fill:'transparent' }
width: 150
height: 50
chartArea: width: '80%', height: '80%'
axisTitlePosition: 'none'
legend: position: 'bottom'
hAxis: gridlines: color: 'transparent' # since you can't seem to *remove* gridlines...
vAxis: gridlines: color: 'transparent'
chart = new google.visualization.LineChart $(".challenge-#{chal.id}-member-#{member.id}-history-#{task.id}")[0]
chart.draw(data, options)
appExports.challengeCreate = (e,el) ->
[type, gid] = [$(el).attr('data-type'), $(el).attr('data-gid')]
model.set '_challenge.new',
name: ''
habits: []
dailys: []
todos: []
rewards: []
id: model.id()
uid: user.get('id')
user: helpers.username(model.get('_user.auth'), model.get('_user.profile.name'))
group: {type, id:gid}
timestamp: +new Date
appExports.challengeSave = ->
gid = model.get('_challenge.new.group.id')
model.unshift "groups.#{gid}.challenges", model.get('_challenge.new'), ->
browser.growlNotification('Challenge Created','success')
challengeDiscard()
appExports.toggleChallengeEdit = (e, el) ->
path = "_editing.challenges.#{$(el).attr('data-id')}"
model.set path, !model.get(path)
appExports.challengeDiscard = challengeDiscard = -> model.del '_challenge.new'
appExports.challengeSubscribe = (e) ->
chal = e.get()
# Add challenge name as a tag for user
tags = user.get('tags')
unless tags and _.find(tags,{id: chal.id})
model.push '_user.tags', {id: chal.id, name: chal.name, challenge: true}
tags = {}; tags[chal.id] = true
# Add all challenge's tasks to user's tasks
userChallenges = user.get('challenges')
user.unshift('challenges', chal.id) unless userChallenges and (userChallenges.indexOf(chal.id) != -1)
_.each ['habit', 'daily', 'todo', 'reward'], (type) ->
_.each chal["#{type}s"], (task) ->
task.tags = tags
task.challenge = chal.id
task.group = {id: chal.group.id, type: chal.group.type}
model.push("_#{type}List", task)
true
appExports.challengeUnsubscribe = (e) ->
chal = e.get()
i = user.get('challenges')?.indexOf chal.id
user.remove("challenges.#{i}") if i? and i != -1
_.each ['habit', 'daily', 'todo', 'reward'], (type) ->
_.each chal["#{type}s"], (task) ->
model.remove "_#{type}List", _.findIndex(model.get("_#{type}List",{id:task.id}))
model.del "_user.tasks.#{task.id}"
true
+1 -3
View File
@@ -14,9 +14,7 @@ module.exports.app = (appExports, model) ->
model.set '_newTag', ''
appExports.toggleEditingTags = ->
before = model.get('_editingTags')
model.set '_editingTags', !before, ->
location.reload() if before is true #when they're done, refresh the page
model.set '_editingTags', !model.get('_editingTags')
appExports.clearFilters = ->
user.set 'filters', {}
+178
View File
@@ -0,0 +1,178 @@
_ = require('lodash')
helpers = require('habitrpg-shared/script/helpers')
module.exports.app = (appExports, model, app) ->
browser = require './browser'
_currentTime = model.at '_currentTime'
_currentTime.setNull +new Date
# Every 60 seconds, reset the current time so that the chat can update relative times
setInterval (->_currentTime.set +new Date), 60000
user = model.at('_user')
appExports.groupCreate = (e,el) ->
type = $(el).attr('data-type')
newGroup =
name: model.get("_new.group.name")
description: model.get("_new.group.description")
leader: user.get('id')
members: [user.get('id')]
type: type
# parties - free
if type is 'party'
return model.add 'groups', newGroup, ->location.reload()
# guilds - 4G
unless user.get('balance') >= 1
return $('#more-gems-modal').modal 'show'
if confirm "Create Guild for 4 Gems?"
newGroup.privacy = (model.get("_new.group.privacy") || 'public') if type is 'guild'
newGroup.balance = 1 # they spent $ to open the guild, it goes into their guild bank
model.add 'groups', newGroup, ->
user.incr 'balance', -1, ->location.reload()
appExports.toggleGroupEdit = (e, el) ->
path = "_editing.groups.#{$(el).attr('data-gid')}"
model.set path, !model.get(path)
appExports.toggleLeaderMessageEdit = (e, el) ->
path = "_editing.leaderMessage.#{$(el).attr('data-gid')}"
model.set path, !model.get(path)
appExports.groupAddWebsite = (e, el) ->
test = e.get()
e.at().unshift 'websites', model.get('_newGroupWebsite')
model.del '_newGroupWebsite'
appExports.groupInvite = (e,el) ->
uid = model.get('_groupInvitee').replace(/[\s"]/g, '')
model.set '_groupInvitee', ''
return if _.isEmpty(uid)
model.query('users').publicInfo([uid]).fetch (err, profiles) ->
throw err if err
profile = profiles.at(0).get()
return model.set("_groupError", "User with id #{uid} not found.") unless profile
model.query('groups').withMember(uid).fetch (err, g) ->
throw err if err
group = e.get(); groups = g.get()
{type, name} = group; gid = group.id
groupError = (msg) -> model.set("_groupError", msg)
invite = ->
$.bootstrapGrowl "Invitation Sent."
switch type
when 'guild' then model.push "users.#{uid}.invitations.guilds", {id:gid, name}, ->location.reload()
when 'party' then model.set "users.#{uid}.invitations.party", {id:gid, name}, ->location.reload()
switch type
when 'guild'
if profile.invitations?.guilds and _.find(profile.invitations.guilds, {id:gid})
return groupError("User already invited to that group")
else if uid in group.members
return groupError("User already in that group")
else invite()
when 'party'
if profile.invitations?.party
return groupError("User already pending invitation.")
else if _.find(groups, {type:'party'})
return groupError("User already in a party.")
else invite()
joinGroup = (gid) ->
model.push("groups.#{gid}.members", user.get('id'), ->location.reload())
appExports.joinGroup = (e, el) -> joinGroup e.get('id')
appExports.acceptInvitation = (e,el) ->
gid = e.get('id')
if $(el).attr('data-type') is 'party'
user.set 'invitations.party', null, ->joinGroup(gid)
else
e.at().remove ->joinGroup(gid)
appExports.rejectInvitation = (e, el) ->
clear = -> browser.resetDom(model)
if e.at().path().indexOf('party') != -1
model.del e.at().path(), clear
else e.at().remove clear
appExports.groupLeave = (e,el) ->
if confirm("Leave this group, are you sure?") is true
uid = user.get('id')
group = model.at "groups.#{$(el).attr('data-id')}"
index = group.get('members').indexOf(uid)
if index != -1
group.remove 'members', index, 1, ->
updated = group.get()
# last member out, delete the party
if _.isEmpty(updated.members) and (updated.type is 'party')
group.del ->location.reload()
# assign new leader, so the party is editable #TODO allow old leader to assign new leader, this is just random
else if (updated.leader is uid)
group.set "leader", updated.members[0], ->location.reload()
else location.reload()
###
Chat Functionality
###
model.on 'unshift', '_party.chat', -> $('.chat-message').tooltip()
model.on 'unshift', '_habitrpg.chat', -> $('.chat-message').tooltip()
appExports.sendChat = (e,el) ->
text = model.get '_chatMessage'
# Check for non-whitespace characters
return unless /\S/.test text
group = e.at()
# get rid of duplicate member ids - this is a weird place to put it, but works for now
members = group.get('members'); uniqMembers = _.uniq(members)
group.set('members', uniqMembers) if !_.isEqual(uniqMembers, members)
chat = group.at('chat')
model.set('_chatMessage', '')
message =
id: model.id()
uuid: user.get('id')
contributor: user.get('backer.contributor')
npc: user.get('backer.npc')
text: text
user: helpers.username(model.get('_user.auth'), model.get('_user.profile.name'))
timestamp: +new Date
# FIXME - sometimes racer will send many duplicates via chat.unshift. I think because it can't make connection, keeps
# trying, but all attempts go through. Unfortunately we can't do chat.set without potentially clobbering other chatters
messages = chat.get() or []
messages.unshift(message)
messages.splice(200)
chat.set messages
type = $(el).attr('data-type')
model.set '_user.party.lastMessageSeen', chat.get()[0].id if group.get('type') is 'party'
appExports.chatKeyup = (e, el, next) ->
return next() unless e.keyCode is 13
appExports.sendChat(e, el)
appExports.deleteChatMessage = (e) ->
if confirm("Delete chat message?") is true
e.at().remove() #requires the {#with}
app.on 'render', (ctx) ->
$('#party-tab-link').on 'shown', (e) ->
messages = model.get('_party.chat')
return false unless messages?.length > 0
model.set '_user.party.lastMessageSeen', messages[0].id
appExports.gotoPartyChat = ->
model.set '_gamePane', true, ->
$('#party-tab-link').tab('show')
appExports.assignGroupLeader = (e, el) ->
newLeader = model.get('_new.groupLeader')
if newLeader and (confirm("Assign new leader, you sure?") is true)
e.at().set('leader', newLeader, ->browser.resetDom(model)) if newLeader
+53 -18
View File
@@ -30,12 +30,18 @@ algos = require 'habitrpg-shared/script/algos'
setupSubscriptions = (page, model, params, next, cb) ->
uuid = model.get('_userId') or model.session.userId # see http://goo.gl/TPYIt
selfQ = model.query('users').withId(uuid) #keep this for later
partyQ = model.query('parties').withMember(uuid)
partyQ.fetch (err, party) ->
# Note: due to https://github.com/codeparty/racer/issues/57, this has to come at the very beginning. The more limited
# the returned fields in motifs, the sooner they must come in fetch / subscribes.
publicGroupsQuery = model.query('groups').publicGroups()
myGroupsQuery = model.query('groups').withMember(uuid)
model.fetch publicGroupsQuery, myGroupsQuery, (err, publicGroups, groups) ->
return next(err) if err
finished = (descriptors, paths) ->
# Add public "Tavern" guild in
descriptors.unshift('groups.habitrpg'); paths.unshift('_habitRPG')
# Subscribe to each descriptor
model.subscribe.apply model, descriptors.concat ->
[err, refs] = [arguments[0], arguments]
return next(err) if err
@@ -45,20 +51,40 @@ setupSubscriptions = (page, model, params, next, cb) ->
return page.redirect('/logout') #delete model.session.userId
return cb()
# (1) Solo player
return finished([selfQ, 'tavern'], ['_user', '_tavern']) unless party.get()
# Get public groups first, order most-to-least # subscribers
model.set '_publicGroups', _.sortBy(publicGroups.get(), (g) -> -_.size(g.members))
groupsObj = groups.get()
# (1) Solo player
return finished([selfQ], ['_user']) if _.isEmpty(groupsObj)
## (2) Party or Guild has members, fetch those users too
# Subscribe to the groups themselves. We separate them by _party, _guilds, and _habitRPG (the "global" guild).
groupsInfo = _.reduce groupsObj, ((m,g)->
if g.type is 'guild' then m.guildIds.push(g.id) else m.partyId = g.id
m.members = m.members.concat(g.members)
m
), {guildIds:[], partyId:null, members:[]}
# Fetch, not subscribe. There's nothing dynamic we need from members, just the the Group (below) which includes chat, challenges, etc
model.query('users').publicInfo(groupsInfo.members).fetch (err, members) ->
return next(err) if err
# we need _members as an object in the view, so we can iterate over _party.members as :id, and access _members[:id] for the info
mObj = members.get()
model.set "_members", _.object(_.pluck(mObj,'id'), mObj)
model.set "_membersArray", mObj
## (2) Party has members, subscribe to those users too
if m = party.get('members')
# Fetch instead of subscribe. There's nothing dynamic we need from members just yet, they'll update _party instead.
# This may change in the future.
model.query('users').party(m).fetch (err, members) ->
return next(err) if err
model.ref '_partyMembers', members
return finished([partyQ, selfQ, 'tavern'], ['_party', '_user', '_tavern'])
else
# Note - selfQ *must* come after membersQ in subscribe, otherwise _user will only get the fields restricted by party-members in store.coffee. Strang bug, but easy to get around
return finished([partyQ, selfQ, 'tavern'], ['_party', '_user', '_tavern'])
descriptors = [selfQ]; paths = ['_user']
if groupsInfo.partyId
descriptors.unshift model.query('groups').withIds(groupsInfo.partyId)
paths.unshift '_party'
unless _.isEmpty(groupsInfo.guildIds)
descriptors.unshift model.query('groups').withIds(groupsInfo.guildIds)
paths.unshift '_guilds'
finished descriptors, paths
# ========== ROUTES ==========
@@ -78,15 +104,13 @@ get '/', (page, model, params, next) ->
# ========== CONTROLLER FUNCTIONS ==========
ready (model) ->
exports.removeAt = (e) -> e.at().remove() # used for things like remove website, chat, etc
user = model.at('_user')
misc.fixCorruptUser(model) # https://github.com/lefnire/habitrpg/issues/634
browser = require './browser'
require('./tasks').app(exports, model)
require('./items').app(exports, model)
require('./party').app(exports, model, app)
require('./groups').app(exports, model, app)
require('./profile').app(exports, model)
require('./pets').app(exports, model)
require('../server/private').app(exports, model)
@@ -94,6 +118,14 @@ ready (model) ->
browser.app(exports, model, app)
require('./unlock').app(exports, model)
require('./filters').app(exports, model)
require('./challenges').app(exports, model)
# used for things like remove website, chat, etc
exports.removeAt = (e, el) ->
if (confirmMessage = $(el).attr 'data-confirm')?
return unless confirm(confirmMessage) is true
e.at().remove()
browser.resetDom(model) if $(el).attr('data-refresh')
###
Cron
@@ -102,7 +134,10 @@ ready (model) ->
# 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}
# for new user, just set lastCron - no need to reset dom.
# remember that the properties are set from uObj & paths AFTER the return of this callback
return if _.isEmpty(paths) or (paths['lastCron'] and _.size(paths) is 1)
# for everyone else, we need to reset dom - too many changes have been made and won't it breaks dom listeners.
if lostHp = delete paths['stats.hp'] # we'll set this manually so we can get a cool animation
setTimeout ->
browser.resetDom(model)
+63 -11
View File
@@ -18,9 +18,24 @@ module.exports.batchTxn = batchTxn = (model, cb, options) ->
# 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), {}
user.set "update__", setOps
user.set "update__", setOps, options?.done
ret
#TODO put this in habitrpg-shared
###
We can't always use refLists, but we often still need to get a positional path by id: eg, users.1234.tasks.5678.value
For arrays (which use indexes, not id-paths), here's a helper function so we can run indexedPath('users',:user.id,'tasks',:task.id,'value)
###
indexedPath = ->
_.reduce arguments, (m,v) =>
return v if !m #first iteration
return "#{m}.#{v}" if _.isString v #string paths
return "#{m}." + _.findIndex(@model.get(m),v)
, ''
taskInChallenge = (task) ->
return undefined unless task?.challenge
@model.at indexedPath.call(@, "groups.#{task.group.id}.challenges", {id:task.challenge}, "#{task.type}s", {id:task.id})
###
algos.score wrapper for habitrpg-helpers to work in Derby. We need to do model.set() instead of simply setting the
@@ -29,8 +44,8 @@ module.exports.batchTxn = batchTxn = (model, cb, options) ->
perform the updates while tracking paths, then all the values at those paths
###
module.exports.score = (model, taskId, direction, allowUndo=false) ->
#return setTimeout( (-> score(taskId, direction)), 500) if model._txnQueue.length > 0
batchTxn model, (uObj, paths) ->
drop = undefined
delta = batchTxn model, (uObj, paths) ->
tObj = uObj.tasks[taskId]
# Stuff for undo
@@ -44,10 +59,32 @@ module.exports.score = (model, taskId, direction, allowUndo=false) ->
delta = algos.score(uObj, tObj, direction, {paths})
model.set('_streakBonus', uObj._tmp.streakBonus) if uObj._tmp?.streakBonus
if uObj._tmp?.drop and $?
model.set '_drop', uObj._tmp.drop
drop = uObj._tmp?.drop
# Update challenge statistics
# FIXME put this in it's own batchTxn, make batchTxn model.at() ref aware (not just _user)
# FIXME use reflists for users & challenges
if (chalTask = taskInChallenge.call({model}, tObj)) and chalTask?.get()
model._dontPersist = false
chalTask.incr "value", delta
chal = model.at indexedPath.call({model}, "groups.#{tObj.group.id}.challenges", {id:tObj.challenge})
chalUser = -> indexedPath.call({model}, chal.path(), 'users', {id:uObj.id})
cu = model.at chalUser()
unless cu?.get()
chal.push "users", {id: uObj.id, name: helpers.username(uObj.auth, uObj.profile?.name)}
cu = model.at chalUser()
else
cu.set 'name', helpers.username(uObj.auth, uObj.profile?.name) # update their name incase it changed
cu.set "#{tObj.type}s.#{tObj.id}",
value: tObj.value
history: tObj.history
model._dontPersist = true
, done:->
if drop and $?
model.set '_drop', drop
$('#item-dropped-modal').modal 'show'
delta
delta
###
Make sure model.get() returns all properties, see https://github.com/codeparty/racer/issues/116
@@ -76,14 +113,15 @@ module.exports.fixCorruptUser = (model) ->
user.del("tasks.#{key}")
delete tasks[key]
true
resetDom = false
batchTxn model, (uObj, paths, batch) ->
## fix https://github.com/lefnire/habitrpg/issues/1086
uniqPets = _.uniq(uObj.items.pets)
batch.set('items.pets', uniqPets) if !_.isEqual(uniqPets, uObj.items.pets)
console.log {uniqPets, count:_.size(uniqPets)}
if uObj.invitations?.guilds
uniqInvites = _.uniq(uObj.invitations.guilds)
batch.set('invitations.guilds', uniqInvites) if !_.isEqual(uniqInvites, uObj.invitations.guilds)
## Task List Cleanup
['habit','daily','todo','reward'].forEach (type) ->
@@ -91,7 +129,7 @@ module.exports.fixCorruptUser = (model) ->
# 1. remove duplicates
# 2. restore missing zombie tasks back into list
idList = uObj["#{type}Ids"]
taskIds = _.pluck( _.where(tasks, {type:type}), 'id')
taskIds = _.pluck( _.where(tasks, {type}), 'id')
union = _.union idList, taskIds
# 2. remove empty (grey) tasks
@@ -128,12 +166,14 @@ module.exports.viewHelpers = (view) ->
view.fn 'int',
get: (num) -> num
set: (num) -> [parseInt(num)]
view.fn 'indexedPath', indexedPath
#iCal
view.fn "encodeiCalLink", helpers.encodeiCalLink
#User
view.fn "gems", (balance) -> return balance/0.25
view.fn "gems", (balance) -> balance * 4
view.fn "username", helpers.username
view.fn "tnl", algos.tnl
view.fn 'equipped', helpers.equipped
@@ -162,3 +202,15 @@ module.exports.viewHelpers = (view) ->
#Tags
view.fn 'noTags', helpers.noTags
view.fn 'appliedTags', helpers.appliedTags
#Challenges
view.fn 'taskInChallenge', (task) ->
taskInChallenge.call(@,task)?.get()
view.fn 'taskAttrFromChallenge', (task, attr) ->
taskInChallenge.call(@,task)?.get(attr)
view.fn 'brokenChallengeLink', (task) ->
task?.challenge and !(taskInChallenge.call(@,task)?.get())
view.fn 'challengeMemberScore', (member, tType, tid) ->
Math.round(member["#{tType}s"]?[tid]?.value)
-147
View File
@@ -1,147 +0,0 @@
_ = require('lodash')
helpers = require('habitrpg-shared/script/helpers')
module.exports.app = (appExports, model, app) ->
browser = require './browser'
_currentTime = model.at '_currentTime'
_currentTime.setNull +new Date()
# Every 60 seconds, reset the current time so that the chat
# can update relative times
setInterval ->
_currentTime.set +new Date()
, 60000
user = model.at('_user')
model.on 'set', '_user.party.invitation', (after, before) ->
if !before? and after? # they just got invited
partyQ = model.query('parties').withId(after)
partyQ.fetch (err, party) ->
return next(err) if err
model.ref '_party', party
browser.resetDom(model)
appExports.partyCreate = ->
newParty = model.get("_newParty")
id = model.add 'parties', { name: newParty, leader: user.get('id'), members: [user.get('id')], invites:[] }
user.set 'party', {current: id, invitation: null, leader: true}, ->
window.location.reload true
appExports.partyInvite = ->
id = model.get('_newPartyMember').replace(/[\s"]/g, '')
return if _.isEmpty(id)
model.query('users').party([id]).fetch (err, users) ->
throw err if err
u = users.at(0).get()
if !u?
model.set "_partyError", "User with id #{id} not found."
return
else if u.party.current? or u.party.invitation?
model.set "_partyError", "User already in a party or pending invitation."
return
else
$.bootstrapGrowl "Invitation Sent."
model.set "users.#{id}.party.invitation", model.get('_party.id'), -> window.location.reload()
#model.set '_newPartyMember', ''
#partySubscribe model
appExports.partyAccept = ->
partyId = user.get('party.invitation')
user.set 'party.invitation', null
user.set 'party.current', partyId
model.at("parties.#{partyId}.members").push user.get('id'), -> window.location.reload()
# model.query('parties').withId(partyId).fetch (err, p) ->
# members = p.get('members')
# members.push user.get('id')
# p.set 'members', members, ->
# window.location.reload true
# partySubscribe model, ->
# p = model.at('_party')
# p.push 'members', user.get('id')
appExports.partyReject = ->
user.set 'party.invitation', null
browser.resetDom(model)
appExports.partyLeave = ->
id = user.set 'party.current', null
party = model.at '_party'
members = party.get('members')
index = members.indexOf(user.get('id'))
party.remove 'members', index, 1, ->
if members.length is 1 # # last member out, kill the party
model.del "parties.#{id}", (-> window.location.reload true)
else
window.location.reload true
###
Chat Functionality
###
sendChat = (path, input) ->
chat = model.at path
text = model.get input
# Check for non-whitespace characters
return unless /\S/.test text
model.set(input, '')
message =
id: model.id()
uuid: user.get('id')
contributor: user.get('backer.contributor')
npc: user.get('backer.npc')
text: text
user: helpers.username(model.get('_user.auth'), model.get('_user.profile.name'))
timestamp: +new Date
# FIXME - sometimes racer will send many duplicates via chat.unshift. I think because it can't make connection, keeps
# trying, but all attempts go through. Unfortunately we can't do chat.set without potentially clobbering other chatters,
# and we can't make chat an object without using refLists. hack solution for now is to unshift, and if there are dupes
# after we set to unique
chat.unshift message, ->
messages = chat.get() || []
count = messages.length
messages =_.uniq messages, true, ((m) -> m?.id) # get rid of dupes
#There were a bunch of duplicates, let's clean it up
if messages.length != count
messages.splice(200)
chat.set messages
else
chat.remove(200)
model.on 'unshift', '_party.chat', -> $('.chat-message').tooltip()
model.on 'unshift', '_tavern.chat.messages', -> $('.chat-message').tooltip()
appExports.partySendChat = ->
sendChat('_party.chat', '_chatMessage')
model.set '_user.party.lastMessageSeen', model.get('_party.chat')[0].id
appExports.tavernSendChat = ->
sendChat('_tavern.chat.messages', '_tavernMessage')
appExports.partyMessageKeyup = (e, el, next) ->
return next() unless e.keyCode is 13
appExports.partySendChat()
appExports.tavernMessageKeyup = (e, el, next) ->
return next() unless e.keyCode is 13
appExports.tavernSendChat()
appExports.deleteChatMessage = (e) ->
if confirm("Delete chat message?") is true
e.at().remove() #requires the {#with}
app.on 'render', (ctx) ->
$('#party-tab-link').on 'shown', (e) ->
messages = model.get('_party.chat')
return false unless messages?.length > 0
model.set '_user.party.lastMessageSeen', messages[0].id
appExports.gotoPartyChat = ->
model.set '_gamePane', true, ->
$('#party-tab-link').tab('show')
+5 -6
View File
@@ -25,12 +25,11 @@ module.exports.app = (appExports, model) ->
return alert "You don't own that egg yet, complete more tasks!" if eggIdx is -1
return alert "You already have that pet, hatch a different combo." if myPets and myPets.indexOf("#{egg.name}-#{hatchingPotionName}") != -1
user.push 'items.pets', egg.name + '-' + hatchingPotionName
eggs.splice eggIdx, 1
myHatchingPotion.splice hatchingPotionIdx, 1
user.set 'items.eggs', eggs
user.set 'items.hatchingPotions', myHatchingPotion
user.push 'items.pets', egg.name + '-' + hatchingPotionName, ->
eggs.splice eggIdx, 1
myHatchingPotion.splice hatchingPotionIdx, 1
user.set 'items.eggs', eggs
user.set 'items.hatchingPotions', myHatchingPotion
alert 'Your egg hatched! Visit your stable to equip your pet.'
+28 -44
View File
@@ -18,8 +18,9 @@ module.exports.app = (appExports, model) ->
# Don't add a blank task; 20/02/13 Added a check for undefined value, more at issue #463 -lancemanfv
return if /^(\s)*$/.test(text) || text == undefined
activeFilters = _.reduce user.get('filters'), ((memo,v,k) -> memo[k]=v if v;memo), {}
newTask = {id: model.id(), type: type, text: text, notes: '', value: 0, tags: activeFilters}
newTask = {id: model.id(), type, text, notes: '', value: 0}
newTask.tags = _.reduce user.get('filters'), ((memo,v,k) -> memo[k]=v if v; memo), {}
switch type
when 'habit'
newTask = _.defaults {up: true, down: true}, newTask
@@ -29,34 +30,14 @@ module.exports.app = (appExports, model) ->
newTask = _.defaults {repeat:{su:true,m:true,t:true,w:true,th:true,f:true,s:true}, completed: false }, newTask
when 'todo'
newTask = _.defaults {completed: false }, newTask
model.unshift "_#{type}List", newTask
e.at().unshift newTask # e.at() in this case is the list, which was scoped here using {#with @list}...{/}
newModel.set ''
appExports.del = (e) ->
# Derby extends model.at to support creation from DOM nodes
task = e.at()
id = task.get('id')
history = task.get('history')
if history and history.length > 2
# prevent delete-and-recreate hack on red tasks
if task.get('value') < 0
if confirm("Are you sure? Deleting this task will hurt you (to prevent deleting, then re-creating red tasks).") is true
task.set('type','habit') # hack to make sure it hits HP, instead of performing "undo checkbox"
misc.score(model, id, 'down', true)
else
return # Cancel. Don't delete, don't hurt user
# prevent accidently deleting long-standing tasks
else
return unless confirm("Are you sure you want to delete this task?") is true
#TODO bug where I have to delete from _users.tasks AND _{type}List,
# fix when query subscriptions implemented properly
return unless confirm("Are you sure you want to delete this task?") is true
$('[rel=tooltip]').tooltip('hide')
user.del('tasks.'+id)
task.remove()
user.del "tasks.#{e.get('id')}"
e.at().remove()
appExports.clearCompleted = (e, el) ->
@@ -74,31 +55,34 @@ module.exports.app = (appExports, model) ->
task.set('repeat.' + $(el).attr('data-day'), true)
appExports.toggleTaskEdit = (e, el) ->
hideId = $(el).attr('data-hide-id')
toggleId = $(el).attr('data-toggle-id')
$(document.getElementById(hideId)).addClass('visuallyhidden')
$(document.getElementById(toggleId)).toggleClass('visuallyhidden')
id = e.get('id')
[editPath, chartPath] = ["_tasks.editing.#{id}", "_page.charts.#{id}"]
model.set editPath, !(model.get editPath)
model.set chartPath, false
appExports.toggleChart = (e, el) ->
hideSelector = $(el).attr('data-hide-id')
chartSelector = $(el).attr('data-toggle-id')
historyPath = $(el).attr('data-history-path')
$(document.getElementById(hideSelector)).hide()
$(document.getElementById(chartSelector)).toggle()
id = $(el).attr('data-id')
[historyPath, togglePath] = ['','']
switch id
when 'exp'
[togglePath, historyPath] = ['_page.charts.exp', '_user.history.exp']
when 'todos'
[togglePath, historyPath] = ['_page.charts.todos', '_user.history.todos']
else
[togglePath, historyPath] = ["_page.charts.#{id}", "_user.tasks.#{id}.history"]
model.set "_tasks.editing.#{id}", false
history = model.get(historyPath)
model.set togglePath, !(model.get togglePath)
matrix = [['Date', 'Score']]
for obj in model.get(historyPath)
date = +new Date(obj.date)
readableDate = moment(date).format('MM/DD')
matrix.push [ readableDate, obj.value ]
_.each history, (obj) -> matrix.push([ moment(obj.date).format('MM/DD/YY'), obj.value ])
data = google.visualization.arrayToDataTable matrix
options = {
options =
title: 'History'
backgroundColor: { fill:'transparent' }
}
chart = new google.visualization.LineChart(document.getElementById( chartSelector ))
chart = new google.visualization.LineChart $(".#{id}-chart")[0]
chart.draw(data, options)
appExports.todosShowRemaining = -> model.set '_showCompleted', false
+3 -3
View File
@@ -75,9 +75,9 @@ module.exports.app = (appExports, model) ->
user.on 'set', 'items.*', (after, before) ->
return if user.get('achievements.ultimateGear')
items = user.get('items')
if parseInt(items.weapon) == 6 and parseInt(items.armor) == 5 and parseInt(items.head) == 5 and parseInt(items.shield) == 5
dontPersist = model._dontPersist; model._dontPersist = false
user.set 'achievements.ultimateGear', true, (-> model._dontPersist = dontPersist)
if parseInt(items.weapon) >= 6 and parseInt(items.armor) >= 5 and parseInt(items.head) >= 5 and parseInt(items.shield) >= 5
dontPersist = model._dontPersist; model._dontPersist = false
user.set 'achievements.ultimateGear', true, (->model._dontPersist = dontPersist)
$('#max-gear-achievement-modal').modal('show')
user.on 'set', 'tasks.*.streak', (id, after, before) ->
+80 -35
View File
@@ -7,11 +7,16 @@ helpers = require 'habitrpg-shared/script/helpers'
validator = require 'derby-auth/node_modules/validator'
check = validator.check
sanitize = validator.sanitize
misc = require '../app/misc'
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) ->
task.type ?= 'habit'
tid = user.add "tasks", task
user.push "#{task.type}Ids", tid
# ---------- /api/v1 API ------------
# Every url added beneath router is prefaced by /api/v1
@@ -42,7 +47,7 @@ auth = (req, res, next) ->
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)
return res.json 401, NO_USER_FOUND if _.isEmpty(req.userObj)
req._isServer = true
next()
@@ -92,6 +97,56 @@ router.put '/user', auth, (req, res) ->
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
###
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
###
@@ -163,17 +218,17 @@ updateTasks = (tasks, user, model) ->
if task.id
if task.del
user.del "tasks.#{task.id}"
if task.type # TODO we should enforce they pass in type, so we can properly remove from idList
i = model.get("_#{task.type}List").indexOf(task.id)
model.remove("_#{task.type}List", i, 1) # doens't work when task.type isn't passed up
# Delete from id list, only if type is passed up
# TODO we should enforce they pass in type, so we can properly remove from idList
if task.type and ~(i = user.get("#{task.type}Ids").indexOf task.id)
user.remove("#{task.type}Ids", i, 1)
task = deleted: true
else
user.set "tasks.#{task.id}", task
else
type = task.type || 'habit'
model.ref '_user', user
model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
model.at("_#{type}List").push task
addTask(user, task)
tasks[idx] = task
return tasks
@@ -187,31 +242,19 @@ router.post '/user/tasks', auth, (req, res) ->
###
router.post '/user/task', auth, validateTask, (req, res) ->
task = req.task
type = task.type
model = req.getModel()
model.ref '_user', req.user
model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
model.at("_#{type}List").push task
addTask req.user, task
res.json 201, task
###
GET /user/tasks
###
router.get '/user/tasks', auth, (req, res) ->
user = req.userObj
return res.json 400, NO_USER_FOUND if !user || _.isEmpty(user)
return res.json 400, NO_USER_FOUND if _.isEmpty(req.userObj)
model = req.getModel()
model.ref '_user', req.user
tasks = []
types = ['habit','todo','daily','reward']
if /^(habit|todo|daily|reward)$/.test req.query.type
types = [req.query.type]
for type in types
model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
tasks = tasks.concat model.get("_#{type}List")
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
@@ -230,9 +273,7 @@ scoreTask = (req, res, next) ->
model = req.getModel()
{user, userObj} = req
model.ref('_user', user)
existingTask = model.at "_user.tasks.#{taskId}"
existingTask = user.at "tasks.#{taskId}"
# TODO add service & icon to task
# If task exists, set it's compltion
if existingTask.get()
@@ -253,12 +294,16 @@ scoreTask = (req, res, next) ->
when 'daily', 'todo'
task.completed = direction is 'up'
model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
model.at("_#{type}List").push task
addTask user, task
#FIXME
delta = misc.score(model, taskId, direction)
result = model.get '_user.stats'
# 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
-1
View File
@@ -101,7 +101,6 @@ router.post '/', auth, (req, res) ->
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.
-3
View File
@@ -23,9 +23,6 @@ racer.set('bundleTimeout', 40000)
# racer.use(racer.logPlugin)
# derby.use(derby.logPlugin)
# Infinite stack trace
Error.stackTraceLimit = Infinity if process.env.NODE_ENV is 'development'
## SERVER CONFIGURATION ##
expressApp = express()
+53 -48
View File
@@ -5,10 +5,15 @@ Setup read / write access
@param store
###
publicAccess = ->
accept = arguments[arguments.length-2]
#return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@)
return accept(false) if derbyAuth.bustedSession(@)
accept(true)
module.exports.customAccessControl = (store) ->
userAccess(store)
partySystem(store)
tavernSystem(store)
groupSystem(store)
REST(store)
###
@@ -43,7 +48,7 @@ userAccess = (store) ->
return accept(false) # we can only manually set this stuff in the database
# public access to users.*.party.invitation (TODO, lock down a bit more)
if attrPath is 'party.invitation'
if attrPath.indexOf('invitations.') is 0
return accept(true)
# Same session (user.id = this.session.userId)
@@ -84,63 +89,63 @@ REST = (store) ->
###
Party permissions
Party & Guild Permissions
###
partySystem = (store) ->
store.query.expose "users", "party", (ids) ->
groupSystem = (store) ->
###
Public User Info
###
store.query.expose "users", "publicInfo", (ids) ->
@where("id").within(ids)
.only('stats',
'items',
'party',
'invitations',
'profile',
'achievements',
'backer',
'preferences',
'auth.local.username',
'auth.facebook.displayName')
store.queryAccess "users", "publicInfo", publicAccess
store.queryAccess "users", "party", (ids, accept, err) ->
# return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@)
return accept(false) if derbyAuth.bustedSession(@)
accept(true) # no harm in public user stats
###
Read / Write groups, so they can create new groups
###
store.readPathAccess "groups.*", publicAccess
store.writeAccess "*", "groups.*", publicAccess
store.query.expose "parties", "withId", (id) ->
@where("id").equals(id).findOne()
###
Public HabitRPG Guild
###
store.readPathAccess 'groups.habitrpg', publicAccess
store.writeAccess "*", "groups.habitrpg.chat.*", publicAccess
store.writeAccess "*", "groups.habitrpg.challenges.*", publicAccess
store.queryAccess "parties", "withId", (id, accept, err) ->
# return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@)
return accept(false) if derbyAuth.bustedSession(@)
accept(true)
###
Find group which has member by id
###
store.query.expose "groups", "withMember", (id, type) ->
q = @where('members').contains([id]).only(['id', 'type', 'name', 'description', 'members' , 'privacy'])
q = q.where('type').equals(type) if type?
store.queryAccess 'groups', 'withMember', publicAccess
store.readPathAccess "parties.*", ->
accept = arguments[arguments.length-2]
accept(true)
store.writeAccess "*", "parties.*", ->
accept = arguments[arguments.length-2]
err = arguments[arguments.length - 1]
# return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@)
return accept(false) if derbyAuth.bustedSession(@)
accept(true)
store.query.expose "parties", "withMember", (id) ->
@where('members').contains([id]).findOne()
store.queryAccess 'parties', 'withMember', (id, accept, err) ->
return accept(false) if derbyAuth.bustedSession(@)
accept(true)
###
LFG / tavern system
###
tavernSystem = (store) ->
store.readPathAccess 'tavern', ->
accept = arguments[arguments.length-2]
return accept(false) if derbyAuth.bustedSession(@)
accept(true)
store.writeAccess "*", "tavern.*", ->
accept = arguments[arguments.length-2]
return accept(false) if derbyAuth.bustedSession(@)
accept(true)
###
Public Groups Info
###
store.query.expose "groups", "publicGroups", ->
@where('privacy').equals('public')
.where('type').equals('guild')
.only(['id', 'type', 'name', 'description', 'members' , 'privacy'])
store.queryAccess "groups", "publicGroups", publicAccess
###
Fetch group info (ie, they just got invited)
###
store.query.expose "groups", "withIds", (ids) ->
return unless ids #FIXME this is sometimes null when ids is array (guilds)
if typeof ids is 'string'
@where("id").equals(ids).findOne() # find a single group
else
@where("id").within(ids) # find multiple groups
store.queryAccess "groups", "withIds", publicAccess