try
This commit is contained in:
@@ -1,74 +0,0 @@
|
||||
XP = 15
|
||||
HP = 2
|
||||
|
||||
priorityValue = module.exports.priorityValue = (priority='!') ->
|
||||
switch priority
|
||||
when '!' then 1
|
||||
when '!!' then 1.5
|
||||
when '!!!' then 2
|
||||
else 1
|
||||
|
||||
module.exports.tnl = (level) ->
|
||||
if level >= 100
|
||||
value = 0
|
||||
else
|
||||
value = Math.round(((Math.pow(level,2)*0.25)+(10 * level) + 139.75)/10)*10 # round to nearest 10
|
||||
return value
|
||||
|
||||
###
|
||||
Calculates Exp modificaiton based on level and weapon strength
|
||||
{value} task.value for exp gain
|
||||
{weaponStrength) weapon strength
|
||||
{level} current user level
|
||||
{priority} user-defined priority multiplier
|
||||
###
|
||||
module.exports.expModifier = (value, weaponStr, level, priority='!') ->
|
||||
str = (level-1) / 2 # ultimately get this from user
|
||||
totalStr = (str + weaponStr) / 100
|
||||
strMod = 1 + totalStr
|
||||
exp = value * XP * strMod * priorityValue(priority)
|
||||
return Math.round(exp)
|
||||
|
||||
###
|
||||
Calculates HP modification based on level and armor defence
|
||||
{value} task.value for hp loss
|
||||
{armorDefense} defense from armor
|
||||
{helmDefense} defense from helm
|
||||
{level} current user level
|
||||
{priority} user-defined priority multiplier
|
||||
###
|
||||
module.exports.hpModifier = (value, armorDef, helmDef, shieldDef, level, priority='!') ->
|
||||
def = (level-1) / 2 # ultimately get this from user?
|
||||
totalDef = (def + armorDef + helmDef + shieldDef) / 100 #ultimate get this from user
|
||||
defMod = 1 - totalDef
|
||||
hp = value * HP * defMod * priorityValue(priority)
|
||||
return Math.round(hp * 10)/10 # round to 1dp
|
||||
|
||||
###
|
||||
Future use
|
||||
{priority} user-defined priority multiplier
|
||||
###
|
||||
module.exports.gpModifier = (value, modifier, priority='!', streak, model) ->
|
||||
val = value * modifier * priorityValue(priority)
|
||||
if streak and model
|
||||
streakBonus = streak / 100 + 1 # eg, 1-day streak is 1.1, 2-day is 1.2, etc
|
||||
afterStreak = val * streakBonus
|
||||
model.set('_streakBonus', afterStreak - val) if (val > 0) # can we do this without model? just global emit?
|
||||
return afterStreak
|
||||
else
|
||||
return val
|
||||
|
||||
###
|
||||
Calculates the next task.value based on direction
|
||||
Uses a capped inverse log y=.95^x, y>= -5
|
||||
{currentValue} the current value of the task
|
||||
{direction} up or down
|
||||
###
|
||||
module.exports.taskDeltaFormula = (currentValue, direction) ->
|
||||
if currentValue < -47.27 then currentValue = -47.27
|
||||
else if currentValue > 21.27 then currentValue = 21.27
|
||||
delta = Math.pow(0.9747,currentValue)
|
||||
return delta if direction is 'up'
|
||||
return -delta
|
||||
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
_ = require 'underscore'
|
||||
moment = require 'moment'
|
||||
|
||||
###
|
||||
Loads JavaScript files from public/vendor/*
|
||||
Use require() to min / concatinate for faster page load
|
||||
###
|
||||
loadJavaScripts = (model) ->
|
||||
|
||||
# Turns out you can't have expressions in browserify require() statements
|
||||
#vendor = '../../public/vendor'
|
||||
#require "#{vendor}/jquery-ui-1.10.2/jquery-1.9.1"
|
||||
|
||||
###
|
||||
Internal Scripts
|
||||
###
|
||||
require "../../public/vendor/jquery-ui-1.10.2/jquery-1.9.1"
|
||||
require "../../public/vendor/jquery.cookie.min"
|
||||
require "../../public/vendor/bootstrap/js/bootstrap.min"
|
||||
require "../../public/vendor/jquery.bootstrap-growl.min"
|
||||
require "../../public/vendor/datepicker/js/bootstrap-datepicker"
|
||||
require "../../public/vendor/bootstrap-tour/bootstrap-tour"
|
||||
|
||||
unless (model.get('_mobileDevice') is true)
|
||||
require "../../public/vendor/jquery-ui-1.10.2/ui/jquery.ui.core"
|
||||
require "../../public/vendor/jquery-ui-1.10.2/ui/jquery.ui.widget"
|
||||
require "../../public/vendor/jquery-ui-1.10.2/ui/jquery.ui.mouse"
|
||||
require "../../public/vendor/jquery-ui-1.10.2/ui/jquery.ui.sortable"
|
||||
require "../../public/vendor/sticky"
|
||||
|
||||
# note: external script loading is handled in app.on('render') near the bottom of this file (see https://groups.google.com/forum/?fromgroups=#!topic/derbyjs/x8FwdTLEuXo)
|
||||
|
||||
###
|
||||
Setup jQuery UI Sortable
|
||||
###
|
||||
setupSortable = (model) ->
|
||||
unless (model.get('_mobileDevice') is true) #don't do sortable on mobile
|
||||
_.each ['habit', 'daily', 'todo', 'reward'], (type) ->
|
||||
$("ul.#{type}s").sortable
|
||||
dropOnEmpty: false
|
||||
cursor: "move"
|
||||
items: "li"
|
||||
scroll: true
|
||||
axis: 'y'
|
||||
update: (e, ui) ->
|
||||
item = ui.item[0]
|
||||
domId = item.id
|
||||
id = item.getAttribute 'data-id'
|
||||
to = $("ul.#{type}s").children().index(item)
|
||||
# Use the Derby ignore option to suppress the normal move event
|
||||
# binding, since jQuery UI will move the element in the DOM.
|
||||
# Also, note that refList index arguments can either be an index
|
||||
# or the item's id property
|
||||
model.at("_#{type}List").pass(ignore: domId).move {id}, to
|
||||
|
||||
setupTooltips = module.exports.setupTooltips = ->
|
||||
$('[rel=tooltip]').tooltip()
|
||||
$('[rel=popover]').popover()
|
||||
$('.popover-auto-show').popover('show')
|
||||
|
||||
$('.priority-multiplier-help').popover
|
||||
title: "How difficult is this task?"
|
||||
trigger: "hover"
|
||||
content: "This multiplies its point value. Use sparingly, rely instead on our organic value-adjustment algorithms. But some tasks are grossly more valuable (Write Thesis vs Floss Teeth). Click for more info."
|
||||
|
||||
setupTour = (model) ->
|
||||
tourSteps = [
|
||||
{
|
||||
element: ".main-herobox"
|
||||
title: "Welcome to HabitRPG"
|
||||
content: "Welcome to HabitRPG, a habit-tracker which treats your goals like a Role Playing Game."
|
||||
}
|
||||
{
|
||||
element: "#bars"
|
||||
title: "Achieve goals and level up"
|
||||
content: "As you accomplish goals, you level up. If you fail your goals, you lose hit points. Lose all your HP and you die."
|
||||
}
|
||||
{
|
||||
element: "ul.habits"
|
||||
title: "Habits"
|
||||
content: "Habits are goals that you constantly track."
|
||||
placement: "bottom"
|
||||
}
|
||||
{
|
||||
element: "ul.dailys"
|
||||
title: "Dailies"
|
||||
content: "Dailies are goals that you want to complete once a day."
|
||||
placement: "bottom"
|
||||
}
|
||||
{
|
||||
element: "ul.todos"
|
||||
title: "Todos"
|
||||
content: "Todos are one-off goals which need to be completed eventually."
|
||||
placement: "bottom"
|
||||
}
|
||||
{
|
||||
element: "ul.rewards"
|
||||
title: "Rewards"
|
||||
content: "As you complete goals, you earn gold to buy rewards. Buy them liberally - rewards are integral in forming good habits."
|
||||
placement: "bottom"
|
||||
}
|
||||
{
|
||||
element: "ul.habits li:first-child"
|
||||
title: "Hover over comments"
|
||||
content: "Different task-types have special properties. Hover over each task's comment for more information. When you're ready to get started, delete the existing tasks and add your own."
|
||||
placement: "right"
|
||||
}
|
||||
]
|
||||
|
||||
$('.main-herobox').popover('destroy') #remove previous popovers
|
||||
tour = new Tour()
|
||||
_.each tourSteps, (step) ->
|
||||
tour.addStep _.defaults step, {html:true}
|
||||
tour._current = 0 if isNaN(tour._current) #bootstrap-tour bug
|
||||
tour.start()
|
||||
|
||||
|
||||
# jquery sticky header on scroll, no need for position fixed
|
||||
initStickyHeader = (model) ->
|
||||
$('.header-wrap').sticky({topSpacing:0})
|
||||
|
||||
###
|
||||
Sets up "+1 Exp", "Level Up", etc notifications
|
||||
###
|
||||
setupGrowlNotifications = (model) ->
|
||||
return unless jQuery? # Only run this in the browser
|
||||
user = model.at '_user'
|
||||
|
||||
statsNotification = (html, type) ->
|
||||
#don't show notifications if user dead
|
||||
return if user.get('stats.lvl') == 0
|
||||
$.bootstrapGrowl html,
|
||||
ele: '#notification-area',
|
||||
type: type # (null, 'info', 'error', 'success', 'gp', 'xp', 'hp', 'lvl','death')
|
||||
top_offset: 20
|
||||
align: 'right' # ('left', 'right', or 'center')
|
||||
width: 250 # (integer, or 'auto')
|
||||
delay: 3000
|
||||
allow_dismiss: true
|
||||
stackup_spacing: 10 # spacing between consecutive stacecked growls.
|
||||
|
||||
# Setup listeners which trigger notifications
|
||||
user.on 'set', 'stats.hp', (captures, args) ->
|
||||
num = captures - args
|
||||
rounded = Math.abs(num.toFixed(1))
|
||||
if num < 0
|
||||
statsNotification "<i class='icon-heart'></i> - #{rounded} HP", 'hp' # lost hp from purchase
|
||||
else if num > 0
|
||||
statsNotification "<i class='icon-heart'></i> + #{rounded} HP", 'hp' # gained hp from potion/level?
|
||||
|
||||
user.on 'set', 'stats.exp', (captures, args, isLocal, silent=false) ->
|
||||
# unless silent
|
||||
num = captures - args
|
||||
rounded = Math.abs(num.toFixed(1))
|
||||
if num < 0 and num > -50 # TODO fix hackey negative notification supress
|
||||
statsNotification "<i class='icon-star'></i> - #{rounded} XP", 'xp'
|
||||
else if num > 0
|
||||
statsNotification "<i class='icon-star'></i> + #{rounded} XP", 'xp'
|
||||
|
||||
###
|
||||
Show "+ 5 {gold_coin} 3 {silver_coin}"
|
||||
###
|
||||
showCoins = (money) ->
|
||||
absolute = Math.abs(money)
|
||||
gold = Math.floor(absolute)
|
||||
silver = Math.floor((absolute-gold)*100)
|
||||
if gold and silver > 0
|
||||
return "#{gold} <i class='icon-gold'></i> #{silver} <i class='icon-silver'></i>"
|
||||
else if gold > 0
|
||||
return "#{gold} <i class='icon-gold'></i>"
|
||||
else if silver > 0
|
||||
return "#{silver} <i class='icon-silver'></i>"
|
||||
|
||||
user.on 'set', 'stats.gp', (captures, args) ->
|
||||
money = captures - args
|
||||
return unless !!money # why is this happening? gotta find where stats.gp is being set from (-)habit
|
||||
sign = if money < 0 then '-' else '+'
|
||||
statsNotification "#{sign} #{showCoins(money)}", 'gp'
|
||||
|
||||
# Append Bonus
|
||||
bonus = model.get('_streakBonus')
|
||||
if (money > 0) and !!bonus
|
||||
bonus = 0.01 if bonus < 0.01
|
||||
statsNotification "+ #{showCoins(bonus)} Streak Bonus!"
|
||||
model.del('_streakBonus')
|
||||
|
||||
user.on 'set', 'items.*', (item, after, before) ->
|
||||
if item in ['armor','weapon','shield','head'] and parseInt(after) < parseInt(before)
|
||||
item = 'helm' if item is 'head' # don't want to day "lost a head"
|
||||
statsNotification "<i class='icon-death'></i> Respawn!", "death"
|
||||
|
||||
user.on 'set', 'stats.lvl', (captures, args) ->
|
||||
if captures > args
|
||||
statsNotification '<i class="icon-chevron-up"></i> Level Up!', 'lvl'
|
||||
|
||||
module.exports.resetDom = (model) ->
|
||||
DERBY.app.dom.clear()
|
||||
DERBY.app.view.render(model, DERBY.app.view._lastRender.ns, DERBY.app.view._lastRender.context);
|
||||
|
||||
# Note, Google Analyatics giving beef if in this file. Moved back to index.html. It's ok, it's async - really the
|
||||
# syncronous requires up top are what benefit the most from this file.
|
||||
|
||||
googleAnalytics = (model) ->
|
||||
if model.flags.nodeEnv is 'production'
|
||||
window._gaq = [["_setAccount", "UA-33510635-1"], ["_setDomainName", "habitrpg.com"], ["_trackPageview"]]
|
||||
$.getScript ((if "https:" is document.location.protocol then "https://ssl" else "http://www")) + ".google-analytics.com/ga.js"
|
||||
|
||||
amazonAffiliate = (model) ->
|
||||
if model.get('_loggedIn') and (model.get('_user.flags.ads') != 'hide')
|
||||
$.getScript('//wms.assoc-amazon.com/20070822/US/js/link-enhancer-common.js?tag=ha0d2-20').fail ->
|
||||
$('body').append('<img src="//wms.assoc-amazon.com/20070822/US/img/noscript.gif?tag=ha0d2-20" alt="" />')
|
||||
|
||||
googleCharts = ->
|
||||
$.getScript "//www.google.com/jsapi", ->
|
||||
# Specifying callback in options param is vital! Otherwise you get blank screen, see http://stackoverflow.com/a/12200566/362790
|
||||
google.load "visualization", "1", {packages:["corechart"], callback: ->}
|
||||
|
||||
module.exports.app = (appExports, model, app) ->
|
||||
loadJavaScripts(model)
|
||||
setupGrowlNotifications(model) unless model.get('_mobileDevice')
|
||||
|
||||
app.on 'render', (ctx) ->
|
||||
#restoreRefs(model)
|
||||
setupSortable(model)
|
||||
setupTooltips(model)
|
||||
setupTour(model)
|
||||
initStickyHeader(model) unless model.get('_mobileDevice')
|
||||
$('.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
|
||||
model.at(ev.target).set 'date', moment(ev.date).format('MM/DD/YYYY')
|
||||
|
||||
###
|
||||
External Scripts
|
||||
JS files not needed right away (google charts) or entirely optional (analytics)
|
||||
Each file getsload asyncronously via $.getScript, so it doesn't bog page-load
|
||||
These need to be handled in app.on('render'), see https://groups.google.com/forum/?fromgroups=#!topic/derbyjs/x8FwdTLEuXo
|
||||
###
|
||||
$.getScript('//checkout.stripe.com/v2/checkout.js')
|
||||
unless (model.get('_mobileDevice') is true)
|
||||
$.getScript("//s7.addthis.com/js/250/addthis_widget.js#pubid=lefnire")
|
||||
googleCharts()
|
||||
|
||||
googleAnalytics(model)
|
||||
amazonAffiliate(model)
|
||||
@@ -1,202 +0,0 @@
|
||||
browser = require './browser'
|
||||
items = require './items'
|
||||
algos = require './algos'
|
||||
|
||||
moment = require 'moment'
|
||||
_ = require 'underscore'
|
||||
lodash = require 'lodash'
|
||||
derby = require 'derby'
|
||||
|
||||
module.exports.app = (appExports, model) ->
|
||||
user = model.at '_user'
|
||||
|
||||
appExports.revive = ->
|
||||
# Reset stats
|
||||
user.set 'stats.hp', 50
|
||||
user.set 'stats.exp', 0
|
||||
user.set 'stats.gp', 0
|
||||
user.incr 'stats.lvl', -1 if user.get('stats.lvl') > 1
|
||||
|
||||
## Lose a random item
|
||||
loseThisItem = false
|
||||
owned = user.get('items')
|
||||
# unless they're already at 0-everything
|
||||
if parseInt(owned.armor)>0 or parseInt(owned.head)>0 or parseInt(owned.shield)>0 or parseInt(owned.weapon)>0
|
||||
console.log 'test'
|
||||
# find a random item to lose
|
||||
until loseThisItem
|
||||
#candidate = {0:'items.armor', 1:'items.head', 2:'items.shield', 3:'items.weapon', 4:'stats.gp'}[Math.random()*5|0]
|
||||
candidate = {0:'armor', 1:'head', 2:'shield', 3:'weapon'}[Math.random()*4|0]
|
||||
loseThisItem = candidate if owned[candidate] > 0
|
||||
user.set "items.#{loseThisItem}", 0
|
||||
|
||||
items.updateStore(model)
|
||||
|
||||
appExports.reset = (e, el) ->
|
||||
batch = new BatchUpdate(model)
|
||||
batch.startTransaction()
|
||||
taskTypes = ['habit', 'daily', 'todo', 'reward']
|
||||
batch.set 'tasks', {}
|
||||
_.each taskTypes, (type) -> batch.set "#{type}Ids", []
|
||||
batch.set 'balance', 1 if user.get('balance') < 1 #only if they haven't manually bought gems
|
||||
|
||||
# Reset stats
|
||||
batch.set 'stats.hp', 50
|
||||
batch.set 'stats.lvl', 1
|
||||
batch.set 'stats.gp', 0
|
||||
batch.set 'stats.exp', 0
|
||||
# Reset items
|
||||
batch.set 'items.armor', 0
|
||||
batch.set 'items.weapon', 0
|
||||
batch.set 'items.head', 0
|
||||
batch.set 'items.shield', 0
|
||||
|
||||
items.updateStore(model)
|
||||
batch.commit()
|
||||
browser.resetDom(model)
|
||||
|
||||
appExports.closeNewStuff = (e, el) ->
|
||||
user.set('flags.newStuff', 'hide')
|
||||
|
||||
appExports.customizeGender = (e, el) ->
|
||||
user.set 'preferences.gender', $(el).attr('data-value')
|
||||
|
||||
appExports.customizeHair = (e, el) ->
|
||||
user.set 'preferences.hair', $(el).attr('data-value')
|
||||
|
||||
appExports.customizeSkin = (e, el) ->
|
||||
user.set 'preferences.skin', $(el).attr('data-value')
|
||||
|
||||
appExports.customizeArmorSet = (e, el) ->
|
||||
user.set 'preferences.armorSet', $(el).attr('data-value')
|
||||
|
||||
appExports.restoreSave = (e, el) ->
|
||||
batch = new BatchUpdate(model)
|
||||
batch.startTransaction()
|
||||
$('#restore-form input').each ->
|
||||
batch.set $(this).attr('data-for'), parseInt($(this).val() || 1)
|
||||
batch.commit()
|
||||
|
||||
appExports.toggleHeader = (e, el) ->
|
||||
user.set 'preferences.hideHeader', !user.get('preferences.hideHeader')
|
||||
|
||||
appExports.deleteAccount = (e, el) ->
|
||||
model.del "users.#{user.get('id')}", ->
|
||||
window.location.href = "/logout"
|
||||
|
||||
userSchema =
|
||||
# _id
|
||||
stats: { gp: 0, exp: 0, lvl: 1, hp: 50 }
|
||||
party: { current: null, invitation: null }
|
||||
items: { weapon: 0, armor: 0, head: 0, shield: 0 }
|
||||
preferences: { gender: 'm', skin: 'white', hair: 'blond', armorSet: 'v1', dayStart:0, showHelm: true }
|
||||
habitIds: []
|
||||
dailyIds: []
|
||||
todoIds: []
|
||||
rewardIds: []
|
||||
apiToken: null # set in newUserObject below
|
||||
lastCron: 'new' #this will be replaced with `+new Date` on first run
|
||||
balance: 0
|
||||
tasks: {}
|
||||
flags:
|
||||
partyEnabled: false
|
||||
itemsEnabled: false
|
||||
tags: []
|
||||
# ads: 'show' # added on registration
|
||||
|
||||
module.exports.newUserObject = ->
|
||||
# deep clone, else further new users get duplicate objects
|
||||
newUser = lodash.cloneDeep userSchema
|
||||
newUser.apiToken = derby.uuid()
|
||||
|
||||
repeat = {m:true,t:true,w:true,th:true,f:true,s:true,su:true}
|
||||
defaultTasks = [
|
||||
{type: 'habit', text: '1h Productive Work', notes: '-- Habits: Constantly Track --\nFor some habits, it only makes sense to *gain* points (like this one).', value: 0, up: true, down: false }
|
||||
{type: 'habit', text: 'Eat Junk Food', notes: 'For others, it only makes sense to *lose* points', value: 0, up: false, down: true}
|
||||
{type: 'habit', text: 'Take The Stairs', notes: 'For the rest, both + and - make sense (stairs = gain, elevator = lose)', value: 0, up: true, down: true}
|
||||
|
||||
{type: 'daily', text: '1h Personal Project', notes: '-- Dailies: Complete Once a Day --\nAt the end of each day, non-completed Dailies dock you points.', value: 0, completed: false, repeat: repeat }
|
||||
{type: 'daily', text: 'Exercise', notes: "If you are doing well, they turn green and are less valuable (experience, gold) and less damaging (HP). This means you can ease up on them for a bit.", value: 3, completed: false, repeat: repeat }
|
||||
{type: 'daily', text: '45m Reading', notes: 'But if you are doing poorly, they turn red. The worse you do, the more valuable (exp, gold) and more damaging (HP) these goals become. This encourages you to focus on your shortcomings, the reds.', value: -10, completed: false, repeat: repeat }
|
||||
|
||||
{type: 'todo', text: 'Call Mom', notes: "-- Todos: Complete Eventually --\nNon-completed Todos won't hurt you, but they will become more valuable over time. This will encourage you to wrap up stale Todos.", value: -3, completed: false }
|
||||
|
||||
{type: 'reward', text: '1 Episode of Game of Thrones', notes: '-- Rewards: Treat Yourself! --\nAs you complete goals, you earn gold to buy rewards. Buy them liberally - rewards are integral in forming good habits.', value: 20 }
|
||||
{type: 'reward', text: 'Cake', notes: 'But only buy if you have enough gold - you lose HP otherwise.', value: 10 }
|
||||
]
|
||||
|
||||
defaultTags = [
|
||||
{name: 'morning'}
|
||||
{name: 'afternoon'}
|
||||
{name: 'evening'}
|
||||
]
|
||||
|
||||
for task in defaultTasks
|
||||
guid = task.id = derby.uuid()
|
||||
newUser.tasks[guid] = task
|
||||
switch task.type
|
||||
when 'habit' then newUser.habitIds.push guid
|
||||
when 'daily' then newUser.dailyIds.push guid
|
||||
when 'todo' then newUser.todoIds.push guid
|
||||
when 'reward' then newUser.rewardIds.push guid
|
||||
|
||||
for tag in defaultTags
|
||||
tag.id = derby.uuid()
|
||||
newUser.tags.push tag
|
||||
|
||||
return newUser
|
||||
|
||||
module.exports.BatchUpdate = BatchUpdate = (model) ->
|
||||
user = model.at("_user")
|
||||
transactionInProgress = false
|
||||
obj = null
|
||||
updates = {}
|
||||
|
||||
{
|
||||
user: user
|
||||
|
||||
obj: ->
|
||||
obj ?= model.get 'users.'+user.get('id')
|
||||
return obj
|
||||
|
||||
startTransaction: ->
|
||||
# start a batch transaction - nothing between now and @commit() will be set immediately
|
||||
transactionInProgress = true
|
||||
model._dontPersist = true
|
||||
@obj()
|
||||
|
||||
###
|
||||
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
|
||||
# pass true if we have levelled to supress xp notification
|
||||
user.set "update__", updates
|
||||
transactionInProgress = false
|
||||
updates = {}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
moment = require 'moment'
|
||||
algos = require './algos'
|
||||
|
||||
module.exports.app = (appExports, model) ->
|
||||
user = model.at('_user')
|
||||
|
||||
appExports.emulateNextDay = ->
|
||||
yesterday = +moment().subtract('days', 1).toDate()
|
||||
user.set 'lastCron', yesterday
|
||||
window.location.reload()
|
||||
|
||||
appExports.emulateTenDays = ->
|
||||
yesterday = +moment().subtract('days', 10).toDate()
|
||||
user.set 'lastCron', yesterday
|
||||
window.location.reload()
|
||||
|
||||
appExports.cheat = ->
|
||||
user.incr 'stats.exp', algos.tnl(user.get('stats.lvl'))
|
||||
user.incr 'stats.gp', 1000
|
||||
@@ -1,41 +0,0 @@
|
||||
_ = require 'underscore'
|
||||
browser = require './browser'
|
||||
|
||||
module.exports.app = (appExports, model) ->
|
||||
user = model.at('_user')
|
||||
|
||||
appExports.toggleFilterByTag = (e, el) ->
|
||||
tagId = $(el).attr('data-tag-id')
|
||||
path = 'filters.' + tagId
|
||||
user.set path, !(user.get path)
|
||||
|
||||
appExports.filtersNewTag = ->
|
||||
user.setNull 'tags', []
|
||||
user.push 'tags', {id: model.id(), name: model.get("_newTag")}
|
||||
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
|
||||
|
||||
appExports.clearFilters = ->
|
||||
user.set 'filters', {}
|
||||
|
||||
appExports.filtersDeleteTag = (e, el) ->
|
||||
tags = user.get('tags')
|
||||
tag = e.at "_user.tags." + $(el).attr('data-index')
|
||||
tagId = tag.get('id')
|
||||
|
||||
#something got corrupted, let's clear the corrupt tags
|
||||
unless tagId
|
||||
user.set 'tags', _.filter( tags, ((t)-> t?.id) )
|
||||
user.set 'filters', {}
|
||||
return
|
||||
|
||||
model.del "_user.filters.#{tagId}"
|
||||
tag.remove()
|
||||
|
||||
# remove tag from all tasks
|
||||
_.each user.get("tasks"), (task) -> user.del "tasks.#{task.id}.tags.#{tagId}"
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
moment = require 'moment'
|
||||
_ = require 'underscore'
|
||||
relative = require 'relative-date'
|
||||
algos = require './algos'
|
||||
items = require('./items').items
|
||||
|
||||
# Absolute diff between two dates
|
||||
daysBetween = (yesterday, now, dayStart) ->
|
||||
#sanity-check reset-time (is it 24h time?)
|
||||
dayStart = 0 unless (dayStart? and (dayStart = parseInt(dayStart)) and dayStart >= 0 and dayStart <= 24)
|
||||
Math.abs moment(yesterday).startOf('day').add('h', dayStart).diff(moment(now), 'days')
|
||||
|
||||
dayMapping = dayMapping = {0:'su',1:'m',2:'t',3:'w',4:'th',5:'f',6:'s',7:'su'}
|
||||
|
||||
# http://stackoverflow.com/questions/2532218/pick-random-property-from-a-javascript-object
|
||||
# obj: object
|
||||
# returns random property (the value)
|
||||
randomVal = (obj) ->
|
||||
result = undefined
|
||||
count = 0
|
||||
for key, val of obj
|
||||
result = val if Math.random() < (1 / ++count)
|
||||
result
|
||||
|
||||
removeWhitespace = (str) ->
|
||||
return '' unless str
|
||||
str.replace /\s/g, ''
|
||||
|
||||
username = (auth, override) ->
|
||||
#some people define custom profile name in Avatar -> Profile
|
||||
return override if override
|
||||
|
||||
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'
|
||||
|
||||
viewHelpers = (view) ->
|
||||
view.fn "percent", (x, y) ->
|
||||
x=1 if x==0
|
||||
Math.round(x/y*100)
|
||||
|
||||
view.fn "round", Math.round
|
||||
view.fn "floor", Math.floor
|
||||
view.fn "ceil", Math.ceil
|
||||
view.fn "lt", (a, b) -> a < b
|
||||
view.fn 'gt', (a, b) -> a > b
|
||||
view.fn "mod", (a, b) -> parseInt(a) % parseInt(b) == 0
|
||||
view.fn 'removeWhitespace', removeWhitespace
|
||||
view.fn "notEqual", (a, b) -> (a != b)
|
||||
view.fn "and", -> _.reduce arguments, (cumm, curr) -> cumm && curr
|
||||
view.fn "or", -> _.reduce arguments, (cumm, curr) -> cumm || curr
|
||||
view.fn "truarr", (num) -> num-1
|
||||
view.fn 'count', (arr) -> arr?.length or 0
|
||||
|
||||
view.fn "gems", (gp) -> return gp/0.25
|
||||
|
||||
view.fn "encodeiCalLink", (uid, apiToken) ->
|
||||
loc = window?.location.host or process.env.BASE_URL
|
||||
encodeURIComponent "http://#{loc}/v1/users/#{uid}/calendar.ics?apiToken=#{apiToken}"
|
||||
|
||||
|
||||
###
|
||||
User
|
||||
###
|
||||
view.fn "username", (auth, override) -> username(auth, override)
|
||||
view.fn "tnl", algos.tnl
|
||||
|
||||
###
|
||||
Items
|
||||
###
|
||||
view.fn 'equipped', (type, item=0, preferences={gender:'m', armorSet:'v1'}, backerTier=0) ->
|
||||
{gender, armorSet} = preferences
|
||||
item = parseInt(item)
|
||||
backerTier = parseInt(backerTier)
|
||||
|
||||
switch type
|
||||
when'armor'
|
||||
if item > 5
|
||||
return 'armor_6' if backerTier >= 45
|
||||
item = 5 # set them back if they're trying to cheat
|
||||
if gender is 'f'
|
||||
return if (item is 0) then "f_armor_#{item}_#{armorSet}" else "f_armor_#{item}"
|
||||
else
|
||||
return "m_armor_#{item}"
|
||||
|
||||
when 'head'
|
||||
if item > 5
|
||||
return 'head_6' if backerTier >= 45
|
||||
item = 5
|
||||
if gender is 'f'
|
||||
return if (item > 1) then "f_head_#{item}_#{armorSet}" else "f_head_#{item}"
|
||||
else
|
||||
return "m_head_#{item}"
|
||||
|
||||
when 'shield'
|
||||
if item > 5
|
||||
return 'shield_6' if backerTier >= 45
|
||||
item = 5
|
||||
return "#{preferences.gender}_shield_#{item}"
|
||||
|
||||
when 'weapon'
|
||||
if item > 6
|
||||
return 'weapon_7' if backerTier >= 70
|
||||
item = 6
|
||||
return "#{preferences.gender}_weapon_#{item}"
|
||||
|
||||
view.fn "gold", (num) ->
|
||||
if num
|
||||
return (num).toFixed(1).split('.')[0]
|
||||
else
|
||||
return "0"
|
||||
|
||||
view.fn "silver", (num) ->
|
||||
if num
|
||||
(num).toFixed(2).split('.')[1]
|
||||
else
|
||||
return "00"
|
||||
|
||||
###
|
||||
Tasks
|
||||
###
|
||||
view.fn 'taskClasses', (task, filters, dayStart, lastCron, showCompleted=false) ->
|
||||
return unless task
|
||||
{type, completed, value, repeat} = task
|
||||
|
||||
# completed / remaining toggle
|
||||
return 'hidden' if (type is 'todo') and (completed != showCompleted)
|
||||
|
||||
for filter, enabled of filters
|
||||
if enabled and not task.tags?[filter]
|
||||
# All the other classes don't matter
|
||||
return 'hidden'
|
||||
|
||||
classes = type
|
||||
|
||||
now = moment().day()
|
||||
|
||||
# calculate the current contextual day (e.g. if it's 12 AM Fri and the user's custom day start is 4 AM, then we should still act like it's Thursday)
|
||||
dayStart = 0 unless (dayStart? and (dayStart = parseInt(dayStart)) and dayStart >= 0 and dayStart <= 24)
|
||||
hourDiff = Math.abs moment(lastCron).startOf('day').add('h', dayStart).diff(moment(now), 'hours')
|
||||
dayStamp = moment(now).add('h', hourDiff)
|
||||
day = dayStamp.day()
|
||||
|
||||
# show as completed if completed (naturally) or not required for today
|
||||
if type in ['todo', 'daily']
|
||||
if completed or (repeat and (repeat[dayMapping[day]] == false))
|
||||
classes += " completed"
|
||||
else
|
||||
classes += " uncompleted"
|
||||
else if type is 'habit'
|
||||
classes += ' habit-wide' if task.down and task.up
|
||||
|
||||
if value < -20
|
||||
classes += ' color-worst'
|
||||
else if value < -10
|
||||
classes += ' color-worse'
|
||||
else if value < -1
|
||||
classes += ' color-bad'
|
||||
else if value < 1
|
||||
classes += ' color-neutral'
|
||||
else if value < 5
|
||||
classes += ' color-good'
|
||||
else if value < 10
|
||||
classes += ' color-better'
|
||||
else
|
||||
classes += ' color-best'
|
||||
return classes
|
||||
|
||||
view.fn 'ownsPet', (pet, userPets) -> !!userPets && userPets.indexOf(pet) != -1
|
||||
|
||||
view.fn 'friendlyTimestamp', (timestamp) -> moment(timestamp).format('MM/DD h:mm:ss a')
|
||||
|
||||
view.fn 'newChatMessages', (messages, lastMessageSeen) ->
|
||||
return false unless messages?.length > 0
|
||||
messages?[0] and (messages[0].id != lastMessageSeen)
|
||||
|
||||
view.fn 'indexOf', (str1, str2) ->
|
||||
return false unless str1 && str2
|
||||
str1.indexOf(str2) != -1
|
||||
|
||||
view.fn 'relativeDate', relative
|
||||
|
||||
view.fn 'noTags', (tags) ->
|
||||
_.isEmpty(tags) or _.isEmpty(_.filter( tags, (t) -> t ) )
|
||||
|
||||
view.fn 'appliedTags', (userTags, taskTags) ->
|
||||
arr = []
|
||||
_.each userTags, (t) ->
|
||||
return unless t?
|
||||
arr.push(t.name) if taskTags?[t.id]
|
||||
arr.join(', ')
|
||||
|
||||
view.fn 'userStr', (level) ->
|
||||
str = (level-1) / 2
|
||||
view.fn 'totalStr', (level, weapon=0) ->
|
||||
str = (level-1) / 2
|
||||
totalStr = (str + items.weapon[weapon].strength)
|
||||
view.fn 'userDef', (level) ->
|
||||
def = (level-1) / 2
|
||||
view.fn 'totalDef', (level, armor=0, helm=0, shield=0) ->
|
||||
def = (level-1) / 2
|
||||
totalDef = (def + items.armor[armor].defense + items.head[helm].defense + items.shield[shield].defense)
|
||||
view.fn 'itemText', (type, item=0) -> items[type][parseInt(item)].text
|
||||
view.fn 'itemStat', (type, item=0) -> if type is 'weapon' then items[type][parseInt(item)].strength else items[type][parseInt(item)].defense
|
||||
|
||||
|
||||
# view.fn 'activeFilters', (filters) ->
|
||||
# debugger
|
||||
# (_.find filters, (f) -> f)?
|
||||
|
||||
|
||||
|
||||
module.exports = { viewHelpers, removeWhitespace, randomVal, daysBetween, dayMapping, username }
|
||||
@@ -1,7 +0,0 @@
|
||||
i18n = require 'derby-i18n'
|
||||
|
||||
i18n.plurals.add 'he', (n) -> n
|
||||
i18n.plurals.add 'bg', (n) -> n
|
||||
i18n.plurals.add 'nl', (n) -> n
|
||||
|
||||
module.exports = i18n
|
||||
@@ -1,137 +0,0 @@
|
||||
derby = require 'derby'
|
||||
|
||||
# Include library components
|
||||
derby.use require('derby-ui-boot'), {styles: []}
|
||||
derby.use require '../../ui'
|
||||
derby.use require 'derby-auth/components'
|
||||
|
||||
# Init app & reference its functions
|
||||
app = derby.createApp module
|
||||
{get, view, ready} = app
|
||||
|
||||
# Translations
|
||||
i18n = require './i18n'
|
||||
i18n.localize app,
|
||||
availableLocales: ['en', 'he', 'bg', 'nl']
|
||||
defaultLocale: 'en'
|
||||
urlScheme: false
|
||||
checkHeader: true
|
||||
|
||||
helpers = require './helpers'
|
||||
helpers.viewHelpers view
|
||||
|
||||
_ = require('underscore')
|
||||
|
||||
###
|
||||
Cleanup task-corruption (null tasks, rogue/invisible tasks, etc)
|
||||
Obviously none of this should be happening, but we'll stop-gap until we can find & fix
|
||||
Gotta love refLists! see https://github.com/lefnire/habitrpg/issues/803 & https://github.com/lefnire/habitrpg/issues/6343
|
||||
###
|
||||
cleanupCorruptTasks = (model) ->
|
||||
user = model.at('_user')
|
||||
tasks = user.get('tasks')
|
||||
|
||||
## Remove corrupted tasks
|
||||
_.each tasks, (task, key) ->
|
||||
unless task?.id? and task?.type?
|
||||
user.del("tasks.#{key}")
|
||||
delete tasks[key]
|
||||
|
||||
batch = null
|
||||
|
||||
## Task List Cleanup
|
||||
_.each ['habit','daily','todo','reward'], (type) ->
|
||||
|
||||
# 1. remove duplicates
|
||||
# 2. restore missing zombie tasks back into list
|
||||
idList = user.get("#{type}Ids")
|
||||
taskIds = _.pluck( _.where(tasks, {type:type}), 'id')
|
||||
union = _.union idList, taskIds
|
||||
|
||||
# 2. remove empty (grey) tasks
|
||||
preened = _.filter union, (id) -> id and _.contains(taskIds, id)
|
||||
|
||||
# There were indeed issues found, set the new list
|
||||
if !_.isEqual(idList, preened)
|
||||
unless batch?
|
||||
batch = new require('./character').BatchUpdate(model)
|
||||
batch.startTransaction()
|
||||
batch.set("#{type}Ids", preened)
|
||||
console.error user.get('id') + "'s #{type}s were corrupt."
|
||||
|
||||
batch.commit() if batch?
|
||||
|
||||
###
|
||||
Subscribe to the user, the users's party (meta info like party name, member ids, etc), and the party's members. 3 subscriptions.
|
||||
###
|
||||
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) ->
|
||||
return next(err) if err
|
||||
|
||||
finished = (descriptors, paths) ->
|
||||
model.subscribe.apply model, descriptors.concat ->
|
||||
[err, refs] = [arguments[0], arguments]
|
||||
return next(err) if err
|
||||
_.each paths, (path, idx) -> model.ref path, refs[idx+1]
|
||||
unless model.get('_user')
|
||||
console.error "User not found - this shouldn't be happening!"
|
||||
return page.redirect('/logout') #delete model.session.userId
|
||||
return cb()
|
||||
|
||||
# (1) Solo player
|
||||
return finished([selfQ, 'tavern'], ['_user', '_tavern']) unless party.get()
|
||||
|
||||
## (2) Party has members, subscribe to those users too
|
||||
membersQ = model.query('users').party(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.
|
||||
membersQ.fetch (err, members) ->
|
||||
return next(err) if err
|
||||
model.ref '_partyMembers', members
|
||||
|
||||
# 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'])
|
||||
|
||||
# ========== ROUTES ==========
|
||||
|
||||
get '/', (page, model, params, next) ->
|
||||
return page.redirect '/' if page.params?.query?.play?
|
||||
|
||||
# removed force-ssl (handled in nginx), see git for code
|
||||
setupSubscriptions page, model, params, next, ->
|
||||
cleanupCorruptTasks(model) # https://github.com/lefnire/habitrpg/issues/634
|
||||
require('./items').server(model)
|
||||
#refLists
|
||||
_.each ['habit', 'daily', 'todo', 'reward'], (type) ->
|
||||
model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
|
||||
page.render()
|
||||
|
||||
|
||||
# ========== CONTROLLER FUNCTIONS ==========
|
||||
|
||||
ready (model) ->
|
||||
user = model.at('_user')
|
||||
model.setNull '_user.apiToken', derby.uuid()
|
||||
|
||||
#set cron immediately
|
||||
lastCron = user.get('lastCron')
|
||||
user.set('lastCron', +new Date) if (!lastCron? or lastCron == 'new')
|
||||
|
||||
require('./scoring').cron(model)
|
||||
|
||||
require('./character').app(exports, model)
|
||||
require('./tasks').app(exports, model)
|
||||
require('./items').app(exports, model)
|
||||
require('./party').app(exports, model, app)
|
||||
require('./profile').app(exports, model)
|
||||
require('./pets').app(exports, model)
|
||||
require('../server/private').app(exports, model)
|
||||
require('./debug').app(exports, model) if model.flags.nodeEnv != 'production'
|
||||
require('./browser').app(exports, model, app)
|
||||
require('./unlock').app(exports, model)
|
||||
require('./filters').app(exports, model)
|
||||
@@ -1,151 +0,0 @@
|
||||
_ = require 'underscore'
|
||||
|
||||
items = module.exports.items =
|
||||
weapon: [
|
||||
{index: 0, text: "Training Sword", classes: "weapon_0", notes:'Training weapon.', strength: 0, value:0}
|
||||
{index: 1, text: "Sword", classes:'weapon_1', notes:'Increases experience gain by 3%.', strength: 3, value:20}
|
||||
{index: 2, text: "Axe", classes:'weapon_2', notes:'Increases experience gain by 6%.', strength: 6, value:30}
|
||||
{index: 3, text: "Morningstar", classes:'weapon_3', notes:'Increases experience gain by 9%.', strength: 9, value:45}
|
||||
{index: 4, text: "Blue Sword", classes:'weapon_4', notes:'Increases experience gain by 12%.', strength: 12, value:65}
|
||||
{index: 5, text: "Red Sword", classes:'weapon_5', notes:'Increases experience gain by 15%.', strength: 15, value:90}
|
||||
{index: 6, text: "Golden Sword", classes:'weapon_6', notes:'Increases experience gain by 18%.', strength: 18, value:120}
|
||||
{index: 7, text: "Dark Souls Blade", classes:'weapon_7', notes:'Increases experience gain by 21%.', strength: 21, value:150}
|
||||
]
|
||||
armor: [
|
||||
{index: 0, text: "Cloth Armor", classes: 'armor_0', notes:'Training armor.', defense: 0, value:0}
|
||||
{index: 1, text: "Leather Armor", classes: 'armor_1', notes:'Decreases HP loss by 4%.', defense: 4, value:30}
|
||||
{index: 2, text: "Chain Mail", classes: 'armor_2', notes:'Decreases HP loss by 6%.', defense: 6, value:45}
|
||||
{index: 3, text: "Plate Mail", classes: 'armor_3', notes:'Decreases HP loss by 7%.', defense: 7, value:65}
|
||||
{index: 4, text: "Red Armor", classes: 'armor_4', notes:'Decreases HP loss by 8%.', defense: 8, value:90}
|
||||
{index: 5, text: "Golden Armor", classes: 'armor_5', notes:'Decreases HP loss by 10%.', defense: 10, value:120}
|
||||
{index: 6, text: "Shade Armor", classes: 'armor_6', notes:'Decreases HP loss by 12%.', defense: 12, value:150}
|
||||
]
|
||||
head: [
|
||||
{index: 0, text: "No Helm", classes: 'head_0', notes:'Training helm.', defense: 0, value:0}
|
||||
{index: 1, text: "Leather Helm", classes: 'head_1', notes:'Decreases HP loss by 2%.', defense: 2, value:15}
|
||||
{index: 2, text: "Chain Coif", classes: 'head_2', notes:'Decreases HP loss by 3%.', defense: 3, value:25}
|
||||
{index: 3, text: "Plate Helm", classes: 'head_3', notes:'Decreases HP loss by 4%.', defense: 4, value:45}
|
||||
{index: 4, text: "Red Helm", classes: 'head_4', notes:'Decreases HP loss by 5%.', defense: 5, value:60}
|
||||
{index: 5, text: "Golden Helm", classes: 'head_5', notes:'Decreases HP loss by 6%.', defense: 6, value:80}
|
||||
{index: 6, text: "Shade Helm", classes: 'head_6', notes:'Decreases HP loss by 7%.', defense: 7, value:100}
|
||||
]
|
||||
shield: [
|
||||
{index: 0, text: "No Shield", classes: 'shield_0', notes:'No Shield.', defense: 0, value:0}
|
||||
{index: 1, text: "Wooden Shield", classes: 'shield_1', notes:'Decreases HP loss by 3%', defense: 3, value:20}
|
||||
{index: 2, text: "Buckler", classes: 'shield_2', notes:'Decreases HP loss by 4%.', defense: 4, value:35}
|
||||
{index: 3, text: "Enforced Shield", classes: 'shield_3', notes:'Decreases HP loss by 5%.', defense: 5, value:55}
|
||||
{index: 4, text: "Red Shield", classes: 'shield_4', notes:'Decreases HP loss by 7%.', defense: 7, value:70}
|
||||
{index: 5, text: "Golden Shield", classes: 'shield_5', notes:'Decreases HP loss by 8%.', defense: 8, value:90}
|
||||
{index: 6, text: "Tormented Skull", classes: 'shield_6', notes:'Decreases HP loss by 9%.', defense: 9, value:120}
|
||||
]
|
||||
potion: {type: 'potion', text: "Potion", notes: "Recover 15 HP", value: 25, classes: 'potion'}
|
||||
reroll: {type: 'reroll', text: "Re-Roll", classes: 'reroll', notes: "Resets your task values back to 0 (yellow). Useful when everything's red and it's hard to stay alive.", value:0 }
|
||||
|
||||
pets: [
|
||||
{text: 'Wolf', name: 'Wolf', value: 3}
|
||||
{text: 'Tiger Cub', name: 'TigerCub', value: 3}
|
||||
#{text: 'Polar Bear Cub', name: 'PolarBearCub', value: 3} #commented out because there are no polarbear modifiers yet, special drop?
|
||||
{text: 'Panda Cub', name: 'PandaCub', value: 3}
|
||||
{text: 'Lion Cub', name: 'LionCub', value: 3}
|
||||
{text: 'Fox', name: 'Fox', value: 3}
|
||||
{text: 'Flying Pig', name: 'FlyingPig', value: 3}
|
||||
{text: 'Dragon', name: 'Dragon', value: 3}
|
||||
{text: 'Cactus', name: 'Cactus', value: 3}
|
||||
{text: 'Bear Cub', name: 'BearCub', value: 3}
|
||||
]
|
||||
|
||||
hatchingPotions: [
|
||||
{text: 'Base', name: 'Base', notes: "Hatches your pet in it's base form.", value: 1}
|
||||
{text: 'White', name: 'White', notes: 'Turns your animal into a White pet.', value: 2}
|
||||
{text: 'Desert', name: 'Desert', notes: 'Turns your animal into a Desert pet.', value: 2}
|
||||
{text: 'Red', name: 'Red', notes: 'Turns your animal into a Red pet.', value: 3}
|
||||
{text: 'Shade', name: 'Shade', notes: 'Turns your animal into a Shade pet.', value: 3}
|
||||
{text: 'Skeleton', name: 'Skeleton', notes: 'Turns your animal into a Skeleton.', value: 3}
|
||||
{text: 'Zombie', name: 'Zombie', notes: 'Turns your animal into a Zombie.', value: 4}
|
||||
{text: 'Cotton Candy Pink', name: 'CottonCandyPink', notes: 'Turns your animal into a Cotton Candy Pink pet.', value: 4}
|
||||
{text: 'Cotton Candy Blue', name: 'CottonCandyBlue', notes: 'Turns your animal into a Cotton Candy Blue pet.', value: 4}
|
||||
{text: 'Golden', name: 'Golden', notes: 'Turns your animal into a Golden pet.', value: 5}
|
||||
]
|
||||
|
||||
# add "type" to each item, so we can reference that as "weapon" or "armor" in the html
|
||||
_.each ['weapon', 'armor', 'head', 'shield'], (key) ->
|
||||
_.each items[key], (item) -> item.type = key
|
||||
|
||||
_.each items.pets, (pet) -> pet.notes = 'Find a hatching potion to pour on this egg, and one day it will hatch into a loyal pet.'
|
||||
_.each items.hatchingPotions, (hatchingPotion) -> hatchingPotion.notes = "Pour this on an egg, and it will hatch as a #{hatchingPotion.text} pet."
|
||||
|
||||
###
|
||||
server exports
|
||||
###
|
||||
module.exports.server = (model) ->
|
||||
model.set '_items', items
|
||||
updateStore(model)
|
||||
|
||||
###
|
||||
app exports
|
||||
###
|
||||
module.exports.app = (appExports, model) ->
|
||||
user = model.at '_user'
|
||||
|
||||
appExports.buyItem = (e, el, next) ->
|
||||
user = model.at '_user'
|
||||
#TODO: this should be working but it's not. so instead, i'm passing all needed values as data-attrs
|
||||
# item = model.at(e.target)
|
||||
|
||||
gp = user.get 'stats.gp'
|
||||
[type, value, index] = [ $(el).attr('data-type'), $(el).attr('data-value'), $(el).attr('data-index') ]
|
||||
|
||||
return if gp < value
|
||||
# make sure deduction doesn't happen unless purchase was successful, see https://github.com/lefnire/habitrpg/issues/233
|
||||
deductGP = -> user.set 'stats.gp', gp - value
|
||||
if type == 'weapon'
|
||||
user.set 'items.weapon', index, deductGP
|
||||
updateStore model
|
||||
else if type == 'armor'
|
||||
user.set 'items.armor', index, deductGP
|
||||
updateStore model
|
||||
else if type == 'head'
|
||||
user.set 'items.head', index, deductGP
|
||||
updateStore model
|
||||
else if type == 'shield'
|
||||
user.set 'items.shield', index, deductGP
|
||||
updateStore model
|
||||
else if type == 'potion'
|
||||
hp = user.get 'stats.hp'
|
||||
hp += 15
|
||||
hp = 50 if hp > 50
|
||||
user.set 'stats.hp', hp, deductGP
|
||||
|
||||
|
||||
appExports.activateRewardsTab = ->
|
||||
model.set '_activeTabRewards', true
|
||||
model.set '_activeTabPets', false
|
||||
appExports.activatePetsTab = ->
|
||||
model.set '_activeTabPets', true
|
||||
model.set '_activeTabRewards', false
|
||||
|
||||
###
|
||||
update store
|
||||
###
|
||||
module.exports.updateStore = updateStore = (model) ->
|
||||
model.setNull '_items.next', {}
|
||||
user = model.at('_user')
|
||||
equipped = user.get('items')
|
||||
|
||||
_.each ['weapon', 'armor', 'shield', 'head'], (type) ->
|
||||
i = parseInt(equipped?[type] || 0) + 1
|
||||
showNext = true
|
||||
if i is items[type].length - 1
|
||||
if (type in ['armor', 'shield', 'head'])
|
||||
showNext = user.get('backer.tier') >= 45 # backer armor
|
||||
else
|
||||
showNext = user.get('backer.tier') >= 70 # backer weapon
|
||||
else if i is items[type].length
|
||||
showNext = false
|
||||
|
||||
model.set "_items.next.#{type}", if showNext then items[type][i] else {hide:true}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
_ = require('underscore')
|
||||
helpers = require './helpers'
|
||||
|
||||
module.exports.app = (appExports, model, app) ->
|
||||
character = require './character'
|
||||
browser = require './browser'
|
||||
helpers = require './helpers'
|
||||
|
||||
_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) -> 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')
|
||||
@@ -1,74 +0,0 @@
|
||||
_ = require 'underscore'
|
||||
{ randomVal } = require './helpers'
|
||||
{ pets, hatchingPotions } = require('./items').items
|
||||
|
||||
###
|
||||
app exports
|
||||
###
|
||||
module.exports.app = (appExports, model) ->
|
||||
user = model.at '_user'
|
||||
|
||||
appExports.chooseEgg = (e, el) ->
|
||||
model.ref '_hatchEgg', e.at()
|
||||
|
||||
appExports.hatchEgg = (e, el) ->
|
||||
hatchingPotionName = $(el).children('select').val()
|
||||
myHatchingPotion = user.get 'items.hatchingPotions'
|
||||
egg = model.get '_hatchEgg'
|
||||
eggs = user.get 'items.eggs'
|
||||
myPets = user.get 'items.pets'
|
||||
|
||||
hatchingPotionIdx = myHatchingPotion.indexOf hatchingPotionName
|
||||
eggIdx = eggs.indexOf egg
|
||||
|
||||
return alert "You don't own that hatching potion yet, complete more tasks!" if hatchingPotionIdx is -1
|
||||
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
|
||||
|
||||
alert 'Your egg hatched! Visit your stable to equip your pet.'
|
||||
|
||||
#FIXME Bug: this removes from the array properly in the browser, but on refresh is has removed all items from the arrays
|
||||
# user.remove 'items.hatchingPotions', hatchingPotionIdx, 1
|
||||
# user.remove 'items.eggs', eggIdx, 1
|
||||
|
||||
appExports.choosePet = (e, el, next) ->
|
||||
petStr = $(el).attr('data-pet')
|
||||
|
||||
return next() if user.get('items.pets').indexOf(petStr) == -1
|
||||
# If user's pet is already active, deselect it
|
||||
return user.set 'items.currentPet', {} if user.get('items.currentPet.str') is petStr
|
||||
|
||||
[name, modifier] = petStr.split('-')
|
||||
pet = _.findWhere pets, name: name
|
||||
pet.modifier = modifier
|
||||
pet.str = petStr
|
||||
user.set 'items.currentPet', pet
|
||||
|
||||
appExports.buyHatchingPotion = (e, el) ->
|
||||
name = $(el).attr 'data-hatchingPotion'
|
||||
newHatchingPotion = _.findWhere hatchingPotions, name: name
|
||||
gems = user.get('balance') * 4
|
||||
if gems >= newHatchingPotion.value
|
||||
if confirm "Buy this hatching potion with #{newHatchingPotion.value} of your #{gems} Gems?"
|
||||
user.push 'items.hatchingPotions', newHatchingPotion.name
|
||||
user.set 'balance', (gems - newHatchingPotion.value) / 4
|
||||
else
|
||||
$('#more-gems-modal').modal 'show'
|
||||
|
||||
appExports.buyEgg = (e, el) ->
|
||||
name = $(el).attr 'data-egg'
|
||||
newEgg = _.findWhere pets, name: name
|
||||
gems = user.get('balance') * 4
|
||||
if gems >= newEgg.value
|
||||
if confirm "Buy this egg with #{newEgg.value} of your #{gems} Gems?"
|
||||
user.push 'items.eggs', newEgg
|
||||
user.set 'balance', (gems - newEgg.value) / 4
|
||||
else
|
||||
$('#more-gems-modal').modal 'show'
|
||||
@@ -1,38 +0,0 @@
|
||||
character = require './character'
|
||||
browser = require './browser'
|
||||
helpers = require './helpers'
|
||||
|
||||
module.exports.app = (appExports, model) ->
|
||||
user = model.at('_user')
|
||||
|
||||
appExports.profileAddWebsite = (e, el) ->
|
||||
newWebsite = model.get('_newProfileWebsite')
|
||||
return if /^(\s)*$/.test(newWebsite)
|
||||
user.unshift 'profile.websites', newWebsite
|
||||
model.set '_newProfileWebsite', ''
|
||||
|
||||
appExports.profileEdit = (e, el) -> model.set '_profileEditing', true
|
||||
appExports.profileSave = (e, el) -> model.set '_profileEditing', false
|
||||
appExports.profileRemoveWebsite = (e, el) ->
|
||||
sites = user.get 'profile.websites'
|
||||
i = sites.indexOf $(el).attr('data-website')
|
||||
sites.splice(i,1)
|
||||
user.set 'profile.websites', sites
|
||||
|
||||
|
||||
toggleGamePane = ->
|
||||
model.set '_gamePane', !model.get('_gamePane'), ->
|
||||
browser.setupTooltips()
|
||||
|
||||
appExports.clickAvatar = (e, el) ->
|
||||
uid = $(el).attr('data-uid')
|
||||
if uid is model.get('_userId') # clicked self
|
||||
toggleGamePane()
|
||||
else
|
||||
$("#avatar-modal-#{uid}").modal('show')
|
||||
|
||||
appExports.toggleGamePane = -> toggleGamePane()
|
||||
|
||||
appExports.toggleResting = ->
|
||||
model.set '_user.flags.rest', !model.get('_user.flags.rest')
|
||||
|
||||
@@ -1,357 +0,0 @@
|
||||
moment = require 'moment'
|
||||
_ = require 'underscore'
|
||||
{ randomVal } = helpers = require './helpers'
|
||||
browser = require './browser'
|
||||
character = require './character'
|
||||
items = require './items'
|
||||
{ pets, hatchingPotions } = items.items
|
||||
algos = require './algos'
|
||||
|
||||
MODIFIER = algos.MODIFIER # each new level, armor, weapon add 2% modifier (this mechanism will change)
|
||||
|
||||
###
|
||||
Drop System
|
||||
###
|
||||
randomDrop = (model, delta, priority, streak=0) ->
|
||||
user = model.at('_user')
|
||||
|
||||
# limit drops to 2 / day
|
||||
user.setNull 'items.lastDrop',
|
||||
date: +moment().subtract('d',1) # trick - set it to yesterday on first run, that way they can get drops today
|
||||
count: 0
|
||||
reachedDropLimit = (helpers.daysBetween(user.get('items.lastDrop.date'), +new Date) is 0) and user.get('items.lastDrop.count') >= 2
|
||||
return if reachedDropLimit
|
||||
|
||||
# % chance of getting a pet or meat
|
||||
chanceMultiplier = Math.abs(delta)
|
||||
chanceMultiplier *= algos.priorityValue(priority) # multiply chance by reddness
|
||||
chanceMultiplier += streak # streak bonus
|
||||
console.log chanceMultiplier
|
||||
|
||||
if user.get('flags.dropsEnabled') and Math.random() < (.05 * chanceMultiplier)
|
||||
# current breakdown - 3% (adjustable) chance on drop
|
||||
# If they got a drop: 50% chance of egg, 50% Hatching Potion. If hatchingPotion, broken down further even further
|
||||
rarity = Math.random()
|
||||
|
||||
# Egg, 40% chance
|
||||
if rarity > .6
|
||||
drop = randomVal(pets)
|
||||
user.push 'items.eggs', drop
|
||||
drop.type = 'Egg'
|
||||
drop.dialog = "You've found a #{drop.text} Egg! #{drop.notes}"
|
||||
|
||||
# Hatching Potion, 60% chance - break down by rarity even more. FIXME this may not be the best method, so revisit
|
||||
else
|
||||
acceptableDrops = []
|
||||
|
||||
# Tier 5 (Blue Moon Rare)
|
||||
if rarity < .1
|
||||
acceptableDrops = ['Base', 'White', 'Desert', 'Red', 'Shade', 'Skeleton', 'Zombie', 'CottonCandyPink', 'CottonCandyBlue', 'Golden']
|
||||
|
||||
# Tier 4 (Very Rare)
|
||||
else if rarity < .2
|
||||
acceptableDrops = ['Base', 'White', 'Desert', 'Red', 'Shade', 'Skeleton', 'Zombie', 'CottonCandyPink', 'CottonCandyBlue']
|
||||
|
||||
# Tier 3 (Rare)
|
||||
else if rarity < .3
|
||||
acceptableDrops = ['Base', 'White', 'Desert', 'Red', 'Shade', 'Skeleton']
|
||||
|
||||
# Tier 2 (Scarce)
|
||||
else if rarity < .4
|
||||
acceptableDrops = ['Base', 'White', 'Desert']
|
||||
# Tier 1 (Common)
|
||||
else
|
||||
acceptableDrops = ['Base']
|
||||
|
||||
acceptableDrops = _.filter(hatchingPotions, (hatchingPotion) -> hatchingPotion.name in acceptableDrops)
|
||||
drop = randomVal acceptableDrops
|
||||
user.push 'items.hatchingPotions', drop.name
|
||||
drop.type = 'HatchingPotion'
|
||||
drop.dialog = "You've found a #{drop.text} Hatching Potion! #{drop.notes}"
|
||||
|
||||
model.set '_drop', drop
|
||||
$('#item-dropped-modal').modal 'show'
|
||||
|
||||
user.set 'items.lastDrop.date', +new Date
|
||||
user.incr 'items.lastDrop.count'
|
||||
|
||||
# {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 = (model, taskId, direction, times, batch, cron) ->
|
||||
user = model.at '_user'
|
||||
|
||||
commit = false
|
||||
unless batch?
|
||||
commit = true
|
||||
batch = new character.BatchUpdate(model)
|
||||
batch.startTransaction()
|
||||
obj = batch.obj()
|
||||
|
||||
{gp, hp, exp, lvl} = obj.stats
|
||||
|
||||
taskPath = "tasks.#{taskId}"
|
||||
taskObj = obj.tasks[taskId]
|
||||
{type, value, streak} = taskObj
|
||||
priority = taskObj.priority or '!'
|
||||
|
||||
# If they're trying to purhcase a too-expensive reward, confirm they want to take a hit for it
|
||||
if taskObj.value > obj.stats.gp and taskObj.type is 'reward'
|
||||
r = confirm "Not enough GP to purchase this reward, buy anyway and lose HP? (Punishment for taking a reward you didn't earn)."
|
||||
unless r
|
||||
batch.commit()
|
||||
return
|
||||
|
||||
delta = 0
|
||||
times ?= 1
|
||||
calculateDelta = (adjustvalue=true) ->
|
||||
# If multiple days have passed, multiply times days missed
|
||||
_.times times, (n) ->
|
||||
# Each iteration calculate the delta (nextDelta), which is then accumulated in delta
|
||||
# (aka, the total delta). This weirdness won't be necessary when calculating mathematically
|
||||
# rather than iteratively
|
||||
nextDelta = algos.taskDeltaFormula(value, direction)
|
||||
value += nextDelta if adjustvalue
|
||||
delta += nextDelta
|
||||
|
||||
addPoints = ->
|
||||
level = user.get('stats.lvl')
|
||||
weaponStrength = items.items.weapon[user.get('items.weapon')].strength
|
||||
exp += algos.expModifier(delta,weaponStrength,level, priority) / 2 # / 2 hack for now bcause people leveling too fast
|
||||
if streak
|
||||
gp += algos.gpModifier(delta, 1, priority, streak, model)
|
||||
else
|
||||
gp += algos.gpModifier(delta, 1, priority)
|
||||
|
||||
subtractPoints = ->
|
||||
level = user.get('stats.lvl')
|
||||
armorDefense = items.items.armor[user.get('items.armor')].defense
|
||||
helmDefense = items.items.head[user.get('items.head')].defense
|
||||
shieldDefense = items.items.shield[user.get('items.shield')].defense
|
||||
hp += algos.hpModifier(delta,armorDefense,helmDefense,shieldDefense,level, priority)
|
||||
|
||||
switch type
|
||||
when 'habit'
|
||||
calculateDelta()
|
||||
# Add habit value to habit-history (if different)
|
||||
if (delta > 0) then addPoints() else subtractPoints()
|
||||
taskObj.history ?= []
|
||||
if taskObj.value != value
|
||||
historyEntry = { date: +new Date, value: value }
|
||||
taskObj.history.push historyEntry
|
||||
batch.set "#{taskPath}.history", taskObj.history
|
||||
|
||||
when 'daily'
|
||||
if cron? # cron
|
||||
calculateDelta()
|
||||
subtractPoints()
|
||||
batch.set "#{taskPath}.streak", 0
|
||||
else
|
||||
calculateDelta(false)
|
||||
if delta != 0
|
||||
addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes
|
||||
if direction is 'up'
|
||||
streak = if streak then streak + 1 else 1
|
||||
else
|
||||
streak = if streak then streak - 1 else 0
|
||||
batch.set "#{taskPath}.streak", streak
|
||||
taskObj.streak = streak
|
||||
|
||||
|
||||
when 'todo'
|
||||
if cron? #cron
|
||||
calculateDelta()
|
||||
#don't touch stats on cron
|
||||
else
|
||||
calculateDelta()
|
||||
addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes
|
||||
|
||||
when 'reward'
|
||||
# Don't adjust values for rewards
|
||||
calculateDelta(false)
|
||||
# purchase item
|
||||
gp -= Math.abs(taskObj.value)
|
||||
num = parseFloat(taskObj.value).toFixed(2)
|
||||
# if too expensive, reduce health & zero gp
|
||||
if gp < 0
|
||||
hp += gp # hp - gp difference
|
||||
gp = 0
|
||||
|
||||
taskObj.value = value
|
||||
batch.set "#{taskPath}.value", taskObj.value
|
||||
origStats = _.clone obj.stats
|
||||
updateStats model, { hp, exp, gp }, batch
|
||||
|
||||
# Commit
|
||||
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()
|
||||
|
||||
# Drop system
|
||||
randomDrop(model, delta, priority, streak) if direction is 'up'
|
||||
|
||||
return delta
|
||||
|
||||
###
|
||||
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 = (model, newStats, batch) ->
|
||||
user = model.at '_user'
|
||||
obj = batch.obj()
|
||||
|
||||
# if user is dead, dont do anything
|
||||
return if obj.stats.hp <= 0
|
||||
|
||||
if newStats.hp?
|
||||
# Game Over
|
||||
if newStats.hp <= 0
|
||||
obj.stats.hp = 0 # signifies dead
|
||||
return
|
||||
else
|
||||
obj.stats.hp = newStats.hp
|
||||
|
||||
if newStats.exp?
|
||||
tnl = algos.tnl(obj.stats.lvl)
|
||||
#silent = false
|
||||
# if we're at level 100, turn xp to gold
|
||||
if obj.stats.lvl >= 100
|
||||
newStats.gp += newStats.exp / 15
|
||||
newStats.exp = 0
|
||||
obj.stats.lvl = 100
|
||||
else
|
||||
# level up & carry-over exp
|
||||
if newStats.exp >= tnl
|
||||
#silent = true # push through the negative xp silently
|
||||
user.set('stats.exp', newStats.exp) # push normal + notification
|
||||
while newStats.exp >= tnl and obj.stats.lvl < 100 # keep levelling up
|
||||
newStats.exp -= tnl
|
||||
obj.stats.lvl++
|
||||
tnl = algos.tnl(obj.stats.lvl)
|
||||
if obj.stats.lvl== 100
|
||||
newStats.exp = 0
|
||||
obj.stats.hp = 50
|
||||
|
||||
obj.stats.exp = newStats.exp
|
||||
#if silent
|
||||
#console.log("pushing silent :" + obj.stats.exp)
|
||||
#user.pass(true).set('stats.exp', obj.stats.exp)
|
||||
|
||||
# Set flags when they unlock features
|
||||
# NOTE we have to first model.set() the flag to true, then AFTER that obj.flags.flag = true
|
||||
# The reason is model.on() listeners still track object references, so if obj.flags.flags = true and then we
|
||||
# model.set(), the second argument of .on() listeners will be true (in otherwords, before/after tests will fail)
|
||||
if !obj.flags.customizationsNotification and (obj.stats.exp > 10 or obj.stats.lvl > 1)
|
||||
batch.set 'flags.customizationsNotification', true
|
||||
obj.flags.customizationsNotification = true
|
||||
if !obj.flags.itemsEnabled and obj.stats.lvl >= 2
|
||||
# Set to object, then also send to browser right away to get model.on() subscription notification
|
||||
batch.set 'flags.itemsEnabled', true
|
||||
obj.flags.itemsEnabled = true
|
||||
if !obj.flags.partyEnabled and obj.stats.lvl >= 3
|
||||
batch.set 'flags.partyEnabled', true
|
||||
obj.flags.partyEnabled = true
|
||||
if !obj.flags.dropsEnabled and obj.stats.lvl >= 4
|
||||
batch.set 'flags.dropsEnabled', true
|
||||
obj.flags.dropsEnabled = true
|
||||
|
||||
if newStats.gp?
|
||||
#FIXME what was I doing here? I can't remember, gp isn't defined
|
||||
gp = 0.0 if (!gp? or gp<0)
|
||||
obj.stats.gp = newStats.gp
|
||||
|
||||
###
|
||||
At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
|
||||
For incomplete Dailys, deduct experience
|
||||
###
|
||||
cron = (model) ->
|
||||
user = model.at '_user'
|
||||
today = +new Date
|
||||
daysPassed = helpers.daysBetween(user.get('lastCron'), today, user.get('preferences.dayStart'))
|
||||
if daysPassed > 0
|
||||
|
||||
# User is resting at the inn. Used to be we un-checked each daily without performing calculation (see commits before fb29e35)
|
||||
# but to prevent abusing the inn (http://goo.gl/GDb9x) we now do *not* calculate dailies, and simply set lastCron to today
|
||||
if user.get('flags.rest') is true
|
||||
return user.set('lastCron', today)
|
||||
|
||||
batch = new character.BatchUpdate(model)
|
||||
batch.startTransaction()
|
||||
obj = batch.obj()
|
||||
batch.set 'lastCron', today
|
||||
|
||||
hpBefore = obj.stats.hp #we'll use this later so we can animate hp loss
|
||||
# Tally each task
|
||||
todoTally = 0
|
||||
_.each obj.tasks, (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 model, id, 'down', daysFailed, batch, true
|
||||
if type == 'daily'
|
||||
if completed #set OHV for completed dailies
|
||||
newValue = taskObj.value + algos.taskDeltaFormula(taskObj.value,'up')
|
||||
batch.set "tasks.#{taskObj.id}.value", newValue
|
||||
|
||||
taskObj.history ?= []
|
||||
taskObj.history.push { date: +new Date, value: taskObj.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
|
||||
else if type is 'habit' # slowly reset 'onlies' value to 0
|
||||
if taskObj.up==false or taskObj.down==false
|
||||
if Math.abs(taskObj.value) < 0.1
|
||||
batch.set "tasks.#{taskObj.id}.value", 0
|
||||
else
|
||||
batch.set "tasks.#{taskObj.id}.value", taskObj.value / 2
|
||||
|
||||
# Finished tallying
|
||||
obj.history ?= {}; obj.history.todos ?= []; obj.history.exp ?= []
|
||||
obj.history.todos.push { date: today, value: todoTally }
|
||||
# tally experience
|
||||
expTally = obj.stats.exp
|
||||
lvl = 0 #iterator
|
||||
while lvl < (obj.stats.lvl-1)
|
||||
lvl++
|
||||
expTally += algos.tnl(lvl)
|
||||
obj.history.exp.push { date: today, value: expTally }
|
||||
|
||||
# Set the new user specs, and animate HP loss
|
||||
[hpAfter, obj.stats.hp] = [obj.stats.hp, hpBefore]
|
||||
batch.setStats()
|
||||
batch.set('history', obj.history)
|
||||
batch.commit()
|
||||
browser.resetDom(model)
|
||||
setTimeout (-> user.set 'stats.hp', hpAfter), 1000 # animate hp loss
|
||||
|
||||
|
||||
module.exports = {
|
||||
score: score
|
||||
cron: cron
|
||||
|
||||
# testing stuff
|
||||
expModifier: algos.expModifier
|
||||
hpModifier: algos.hpModifier
|
||||
taskDeltaFormula: algos.taskDeltaFormula
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
scoring = require './scoring'
|
||||
helpers = require './helpers'
|
||||
_ = require 'underscore'
|
||||
moment = require 'moment'
|
||||
character = require './character'
|
||||
|
||||
module.exports.app = (appExports, model) ->
|
||||
user = model.at('_user')
|
||||
|
||||
appExports.addTask = (e, el) ->
|
||||
type = $(el).attr('data-task-type')
|
||||
newModel = model.at('_new' + type.charAt(0).toUpperCase() + type.slice(1))
|
||||
text = newModel.get()
|
||||
# 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}
|
||||
switch type
|
||||
when 'habit'
|
||||
newTask = _.defaults {up: true, down: true}, newTask
|
||||
when 'reward'
|
||||
newTask = _.defaults {value: 20}, newTask
|
||||
when 'daily'
|
||||
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
|
||||
newModel.set ''
|
||||
|
||||
appExports.del = (e, el) ->
|
||||
# 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
|
||||
result = confirm("Are you sure? Deleting this task will hurt you (to prevent deleting, then re-creating red tasks).")
|
||||
if result != true
|
||||
return # Cancel. Don't delete, don't hurt user
|
||||
else
|
||||
task.set('type','habit') # hack to make sure it hits HP, instead of performing "undo checkbox"
|
||||
scoring.score(model, id, direction:'down')
|
||||
|
||||
# prevent accidently deleting long-standing tasks
|
||||
else
|
||||
result = confirm("Are you sure you want to delete this task?")
|
||||
return if result != true
|
||||
|
||||
#TODO bug where I have to delete from _users.tasks AND _{type}List,
|
||||
# fix when query subscriptions implemented properly
|
||||
$('[rel=tooltip]').tooltip('hide')
|
||||
|
||||
user.del('tasks.'+id)
|
||||
task.remove()
|
||||
|
||||
|
||||
appExports.clearCompleted = (e, el) ->
|
||||
completedIds = _.pluck( _.where(model.get('_todoList'), {completed:true}), 'id')
|
||||
todoIds = user.get('todoIds')
|
||||
|
||||
_.each completedIds, (id) -> user.del "tasks.#{id}"
|
||||
user.set 'todoIds', _.difference(todoIds, completedIds)
|
||||
|
||||
appExports.toggleDay = (e, el) ->
|
||||
task = model.at(e.target)
|
||||
if /active/.test($(el).attr('class')) # previous state, not current
|
||||
task.set('repeat.' + $(el).attr('data-day'), false)
|
||||
else
|
||||
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')
|
||||
|
||||
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()
|
||||
|
||||
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 ]
|
||||
data = google.visualization.arrayToDataTable matrix
|
||||
|
||||
options = {
|
||||
title: 'History'
|
||||
backgroundColor: { fill:'transparent' }
|
||||
}
|
||||
|
||||
chart = new google.visualization.LineChart(document.getElementById( chartSelector ))
|
||||
chart.draw(data, options)
|
||||
|
||||
appExports.todosShowRemaining = -> model.set '_showCompleted', false
|
||||
appExports.todosShowCompleted = -> model.set '_showCompleted', true
|
||||
|
||||
setUndo = (stats, task) ->
|
||||
previousUndo = model.get('_undo')
|
||||
clearTimeout(previousUndo.timeoutId) if previousUndo?.timeoutId
|
||||
timeoutId = setTimeout (-> model.del('_undo')), 10000
|
||||
model.set '_undo', {stats:stats, task:task, timeoutId: timeoutId}
|
||||
|
||||
|
||||
###
|
||||
Call scoring functions for habits & rewards (todos & dailies handled below)
|
||||
###
|
||||
appExports.score = (e, el) ->
|
||||
task= model.at $(el).parents('li')[0]
|
||||
taskObj = task.get()
|
||||
direction = $(el).attr('data-direction')
|
||||
|
||||
# set previous state for undo
|
||||
setUndo _.clone(user.get('stats')), _.clone(taskObj)
|
||||
|
||||
scoring.score(model, taskObj.id, direction)
|
||||
|
||||
###
|
||||
This is how we handle appExports.score for todos & dailies. Due to Derby's special handling of `checked={:task.completd}`,
|
||||
the above function doesn't work so we need a listener here
|
||||
###
|
||||
user.on 'set', 'tasks.*.completed', (i, completed, previous, isLocal, passed) ->
|
||||
return if passed? && passed.cron # Don't do this stuff on cron
|
||||
direction = if completed then 'up' else 'down'
|
||||
|
||||
# set previous state for undo
|
||||
taskObj = _.clone user.get("tasks.#{i}")
|
||||
taskObj.completed = previous
|
||||
setUndo _.clone(user.get('stats')), taskObj
|
||||
|
||||
scoring.score(model, i, direction)
|
||||
|
||||
###
|
||||
Undo
|
||||
###
|
||||
appExports.undo = () ->
|
||||
undo = model.get '_undo'
|
||||
clearTimeout(undo.timeoutId) if undo?.timeoutId
|
||||
batch = character.BatchUpdate(model)
|
||||
batch.startTransaction()
|
||||
model.del '_undo'
|
||||
_.each undo.stats, (val, key) -> batch.set "stats.#{key}", val
|
||||
taskPath = "tasks.#{undo.task.id}"
|
||||
_.each undo.task, (val, key) ->
|
||||
return if key in ['id', 'type'] # strange bugs in this world: https://workflowy.com/shared/a53582ea-43d6-bcce-c719-e134f9bf71fd/
|
||||
if key is 'completed'
|
||||
user.pass({cron:true}).set("#{taskPath}.completed",val)
|
||||
else
|
||||
batch.set "#{taskPath}.#{key}", val
|
||||
batch.commit()
|
||||
|
||||
appExports.tasksToggleAdvanced = (e, el) ->
|
||||
$(el).next('.advanced-option').toggleClass('visuallyhidden')
|
||||
|
||||
appExports.tasksSaveAndClose = ->
|
||||
# When they update their notes, re-establish tooltip & popover
|
||||
$('[rel=tooltip]').tooltip()
|
||||
$('[rel=popover]').popover()
|
||||
|
||||
appExports.tasksSetPriority = (e, el) ->
|
||||
dataId = $(el).parent('[data-id]').attr('data-id')
|
||||
#"_user.tasks.#{dataId}"
|
||||
model.at(e.target).set 'priority', $(el).attr('data-priority')
|
||||
@@ -1,95 +0,0 @@
|
||||
_ = require 'underscore'
|
||||
{ randomVal } = require './helpers'
|
||||
{ pets, hatchingPotions } = require('./items').items
|
||||
|
||||
###
|
||||
Listeners to enabled flags, set notifications to the user when they've unlocked features
|
||||
###
|
||||
|
||||
module.exports.app = (appExports, model) ->
|
||||
user = model.at('_user')
|
||||
|
||||
alreadyShown = (before, after) -> !(!before and after is true)
|
||||
|
||||
showPopover = (selector, title, html, placement='bottom') ->
|
||||
$(selector).popover('destroy')
|
||||
html += " <a href='#' onClick=\"$('#{selector}').popover('hide');return false;\">[Close]</a>"
|
||||
$(selector).popover({
|
||||
title: title
|
||||
placement: placement
|
||||
trigger: 'manual'
|
||||
html: true
|
||||
content: html
|
||||
}).popover 'show'
|
||||
|
||||
|
||||
user.on 'set', 'flags.customizationsNotification', (after, before) ->
|
||||
return if alreadyShown(before,after)
|
||||
$('.main-herobox').popover('destroy') #remove previous popovers
|
||||
html = "Click your avatar to customize your appearance."
|
||||
showPopover '.main-herobox', 'Customize Your Avatar', html, 'bottom'
|
||||
|
||||
user.on 'set', 'flags.itemsEnabled', (after, before) ->
|
||||
return if alreadyShown(before,after)
|
||||
html = """
|
||||
<img src='/vendor/BrowserQuest/client/img/1/chest.png' />
|
||||
Congratulations, you have unlocked the Item Store! You can now buy weapons, armor, potions, etc. Read each item's comment for more information.
|
||||
"""
|
||||
showPopover 'div.rewards', 'Item Store Unlocked', html, 'left'
|
||||
|
||||
user.on 'set', 'flags.petsEnabled', (after, before) ->
|
||||
return if alreadyShown(before,after)
|
||||
html = """
|
||||
<img src='/img/sprites/wolf_border.png' style='width:30px;height:30px;float:left;padding-right:5px' />
|
||||
You have unlocked Pets! You can now buy pets with Gems (note, you replenish Gems with real-life money - so chose your pets wisely!)
|
||||
"""
|
||||
showPopover '#rewardsTabs', 'Pets Unlocked', html, 'left'
|
||||
|
||||
user.on 'set', 'flags.partyEnabled', (after, before) ->
|
||||
return if user.get('party.current') or alreadyShown(before,after)
|
||||
html = """
|
||||
Be social, join a party and play Habit with your friends! You'll be better at your habits with accountability partners. Click User -> Options -> Party, and follow the instructions. LFG anyone?
|
||||
"""
|
||||
showPopover '.user-menu', 'Party System', html, 'bottom'
|
||||
|
||||
user.on 'set', 'flags.dropsEnabled', (after, before) ->
|
||||
return if alreadyShown(before,after)
|
||||
|
||||
egg = randomVal pets
|
||||
|
||||
dontPersist = model._dontPersist
|
||||
|
||||
model._dontPersist = false
|
||||
user.push 'items.eggs', egg
|
||||
model._dontPersist = dontPersist
|
||||
|
||||
$('#drops-enabled-modal').modal 'show'
|
||||
|
||||
user.on 'push', 'items.pets', (after, before) ->
|
||||
return if user.get('achievements.beastMaster')
|
||||
if before >= 90 # evidently before is the count?
|
||||
dontPersist = model._dontPersist; model._dontPersist = false
|
||||
user.set 'achievements.beastMaster', true, (-> model._dontPersist = dontPersist)
|
||||
$('#beastmaster-achievement-modal').modal('show')
|
||||
|
||||
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)
|
||||
$('#max-gear-achievement-modal').modal('show')
|
||||
|
||||
user.on 'set', 'tasks.*.streak', (id, after, before) ->
|
||||
if after > 0
|
||||
|
||||
# 21-day streak, as per the old philosophy of doign a thing 21-days in a row makes a habit
|
||||
if (after % 21) is 0
|
||||
dontPersist = model._dontPersist; model._dontPersist = false
|
||||
user.incr 'achievements.streak', 1, (-> model._dontPersist = dontPersist)
|
||||
$('#streak-achievement-modal').modal('show')
|
||||
|
||||
# they're undoing a task at the 21 mark, take back their badge
|
||||
else if (before - after is 1) and (before % 21 is 0)
|
||||
dontPersist = model._dontPersist; model._dontPersist = false
|
||||
user.incr 'achievements.streak', -1, (-> model._dontPersist = dontPersist)
|
||||
@@ -0,0 +1,28 @@
|
||||
/* Load nconf and define default configuration values if config.json or ENV vars are not found*/
|
||||
|
||||
|
||||
var conf = require("nconf");
|
||||
var path = require("path");
|
||||
|
||||
conf.argv()
|
||||
.env()
|
||||
//.file('defaults', path.join(path.resolve(__dirname, '../config.json.example')))
|
||||
.file('user', path.join(path.resolve(__dirname, '../config.json')));
|
||||
|
||||
/*
|
||||
var agent;
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// Follow these instructions for profiling / debugging leaks
|
||||
// * https://developers.google.com/chrome-developer-tools/docs/heap-profiling
|
||||
// * https://developers.google.com/chrome-developer-tools/docs/memory-analysis-101
|
||||
agent = require('webkit-devtools-agent');
|
||||
console.log("To debug memory leaks:" +
|
||||
"\n\t(1) Run `kill -SIGUSR2 " + process.pid + "`" +
|
||||
"\n\t(2) open http://c4milo.github.com/node-webkit-agent/21.0.1180.57/inspector.html?host=localhost:1337&page=0");
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
if (conf.get('NODE_ENV') === "development") {
|
||||
Error.stackTraceLimit = Infinity;
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
var _ = require('lodash');
|
||||
var validator = require('validator');
|
||||
var check = validator.check;
|
||||
var sanitize = validator.sanitize;
|
||||
var passport = require('passport');
|
||||
var helpers = require('habitrpg-shared/script/helpers');
|
||||
var async = require('async');
|
||||
var utils = require('../utils');
|
||||
var nconf = require('nconf');
|
||||
var User = require('../models/user').model;
|
||||
|
||||
var api = module.exports;
|
||||
|
||||
var NO_TOKEN_OR_UID = { err: "You must include a token and uid (user id) in your request"};
|
||||
var NO_USER_FOUND = {err: "No user found."};
|
||||
|
||||
/*
|
||||
beforeEach auth interceptor
|
||||
*/
|
||||
|
||||
api.auth = function(req, res, next) {
|
||||
var token, uid;
|
||||
uid = req.headers['x-api-user'];
|
||||
token = req.headers['x-api-key'];
|
||||
if (!(uid && token)) {
|
||||
return res.json(401, NO_TOKEN_OR_UID);
|
||||
}
|
||||
return User.findOne({
|
||||
_id: uid,
|
||||
apiToken: token
|
||||
}, function(err, user) {
|
||||
if (err) {
|
||||
return res.json(500, {
|
||||
err: err
|
||||
});
|
||||
}
|
||||
if (_.isEmpty(user)) {
|
||||
return res.json(401, NO_USER_FOUND);
|
||||
}
|
||||
|
||||
res.locals.wasModified = req.query._v ? +user._v !== +req.query._v : true;
|
||||
res.locals.user = user;
|
||||
req.session.userId = user._id;
|
||||
return next();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
api.registerUser = function(req, res, next) {
|
||||
var confirmPassword, e, email, password, username, _ref;
|
||||
_ref = req.body, email = _ref.email, username = _ref.username, password = _ref.password, confirmPassword = _ref.confirmPassword;
|
||||
if (!(username && password && email)) {
|
||||
return res.json(401, {err: ":username, :email, :password, :confirmPassword required"});
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
return res.json(401, {err: ":password and :confirmPassword don't match"});
|
||||
}
|
||||
try {
|
||||
validator.check(email).isEmail();
|
||||
} catch (err) {
|
||||
return res.json(401, {err: err.message});
|
||||
}
|
||||
async.waterfall([
|
||||
function(cb) {
|
||||
User.findOne({'auth.local.email': email}, cb);
|
||||
},
|
||||
function(found, cb) {
|
||||
if (found) {
|
||||
return cb("Email already taken");
|
||||
}
|
||||
User.findOne({'auth.local.username': username}, cb);
|
||||
}, function(found, cb) {
|
||||
var newUser, salt, user;
|
||||
if (found) {
|
||||
return cb("Username already taken");
|
||||
}
|
||||
newUser = helpers.newUser(true);
|
||||
salt = utils.makeSalt();
|
||||
newUser.auth = {
|
||||
local: {
|
||||
username: username,
|
||||
email: email,
|
||||
salt: salt
|
||||
},
|
||||
timestamps: {created: +new Date(), loggedIn: +new Date()}
|
||||
};
|
||||
newUser.auth.local.hashed_password = utils.encryptPassword(password, salt);
|
||||
user = new User(newUser);
|
||||
user.save(cb);
|
||||
}
|
||||
], function(err, saved) {
|
||||
if (err) {
|
||||
return res.json(401, {err: err});
|
||||
}
|
||||
res.json(200, saved);
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
Register new user with uname / password
|
||||
*/
|
||||
|
||||
|
||||
api.loginLocal = function(req, res, next) {
|
||||
var username = req.body.username;
|
||||
var password = req.body.password;
|
||||
if (!(username && password)) return res.json(401, {err:'Missing :username or :password in request body, please provide both'});
|
||||
User.findOne({'auth.local.username': username}, function(err, user){
|
||||
if (err) return res.json(500,{err:err});
|
||||
if (!user) return res.json(401, {err:"Username '" + username + "' not found. Usernames are case-sensitive, click 'Forgot Password' if you can't remember the capitalization."});
|
||||
// We needed the whole user object first so we can get his salt to encrypt password comparison
|
||||
User.findOne({
|
||||
'auth.local.username': username,
|
||||
'auth.local.hashed_password': utils.encryptPassword(password, user.auth.local.salt)
|
||||
}, function(err, user){
|
||||
if (err) return res.json(500,{err:err});
|
||||
if (!user) return res.json(401,{err:'Incorrect password'});
|
||||
res.json({id: user._id,token: user.apiToken});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
POST /user/auth/facebook
|
||||
*/
|
||||
|
||||
|
||||
api.loginFacebook = function(req, res, next) {
|
||||
var email, facebook_id, name, _ref;
|
||||
_ref = req.body, facebook_id = _ref.facebook_id, email = _ref.email, name = _ref.name;
|
||||
if (!facebook_id) {
|
||||
return res.json(401, {
|
||||
err: 'No facebook id provided'
|
||||
});
|
||||
}
|
||||
return User.findOne({
|
||||
'auth.local.facebook.id': facebook_id
|
||||
}, function(err, user) {
|
||||
if (err) {
|
||||
return res.json(401, {
|
||||
err: err
|
||||
});
|
||||
}
|
||||
if (user) {
|
||||
return res.json(200, {
|
||||
id: user.id,
|
||||
token: user.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."
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
api.resetPassword = function(req, res, next){
|
||||
var email = req.body.email,
|
||||
salt = utils.makeSalt(),
|
||||
newPassword = utils.makeSalt(), // use a salt as the new password too (they'll change it later)
|
||||
hashed_password = utils.encryptPassword(newPassword, salt);
|
||||
|
||||
User.findOne({'auth.local.email':email}, function(err, user){
|
||||
if (err) return res.json(500,{err:err});
|
||||
if (!user) return res.send(500, {err:"Couldn't find a user registered for email " + email});
|
||||
user.auth.local.salt = salt;
|
||||
user.auth.local.hashed_password = hashed_password;
|
||||
utils.sendEmail({
|
||||
from: "HabitRPG <admin@habitrpg.com>",
|
||||
to: email,
|
||||
subject: "Password Reset for HabitRPG",
|
||||
text: "Password for " + user.auth.local.username + " has been reset to " + newPassword + ". Log in at " + nconf.get('BASE_URL'),
|
||||
html: "Password for <strong>" + user.auth.local.username + "</strong> has been reset to <strong>" + newPassword + "</strong>. Log in at " + nconf.get('BASE_URL')
|
||||
});
|
||||
user.save();
|
||||
return res.send('New password sent to '+ email);
|
||||
});
|
||||
};
|
||||
|
||||
api.changePassword = function(req, res, next) {
|
||||
var user = res.locals.user,
|
||||
oldPassword = req.body.oldPassword,
|
||||
newPassword = req.body.newPassword,
|
||||
confirmNewPassword = req.body.confirmNewPassword;
|
||||
|
||||
if (newPassword != confirmNewPassword)
|
||||
return res.json(500, {err: "Password & Confirm don't match"});
|
||||
|
||||
var salt = user.auth.local.salt,
|
||||
hashed_old_password = utils.encryptPassword(oldPassword, salt),
|
||||
hashed_new_password = utils.encryptPassword(newPassword, salt);
|
||||
|
||||
if (hashed_old_password !== user.auth.local.hashed_password)
|
||||
return res.json(500, {err:"Old password doesn't match"});
|
||||
|
||||
user.auth.local.hashed_password = hashed_new_password;
|
||||
user.save(function(err, saved){
|
||||
if (err) res.json(500,{err:err});
|
||||
res.send(200);
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Registers a new user. Only accepting username/password registrations, no Facebook
|
||||
*/
|
||||
|
||||
api.setupPassport = function(router) {
|
||||
|
||||
router.get('/logout', function(req, res) {
|
||||
req.logout();
|
||||
delete req.session.userId;
|
||||
res.redirect('/');
|
||||
})
|
||||
|
||||
// GET /auth/facebook
|
||||
// Use passport.authenticate() as route middleware to authenticate the
|
||||
// request. The first step in Facebook authentication will involve
|
||||
// redirecting the user to facebook.com. After authorization, Facebook will
|
||||
// redirect the user back to this application at /auth/facebook/callback
|
||||
router.get('/auth/facebook',
|
||||
passport.authenticate('facebook'),
|
||||
function(req, res){
|
||||
// The request will be redirected to Facebook for authentication, so this
|
||||
// function will not be called.
|
||||
});
|
||||
|
||||
// GET /auth/facebook/callback
|
||||
// Use passport.authenticate() as route middleware to authenticate the
|
||||
// request. If authentication fails, the user will be redirected back to the
|
||||
// login page. Otherwise, the primary route function function will be called,
|
||||
// which, in this example, will redirect the user to the home page.
|
||||
router.get('/auth/facebook/callback',
|
||||
passport.authenticate('facebook', { failureRedirect: '/login' }),
|
||||
function(req, res) {
|
||||
//res.redirect('/');
|
||||
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
User.findOne({'auth.facebook.id':req.user.id}, cb)
|
||||
},
|
||||
function(user, cb){
|
||||
if (user) return cb(null, user);
|
||||
var newUser = helpers.newUser(true);
|
||||
newUser.auth = {
|
||||
facebook: req.user,
|
||||
timestamps: {created: +new Date(), loggedIn: +new Date()}
|
||||
};
|
||||
user = new User(newUser);
|
||||
user.save(cb);
|
||||
|
||||
|
||||
}
|
||||
], function(err, saved){
|
||||
if (err) return res.redirect('/static/front?err=' + err);
|
||||
req.session.userId = saved._id;
|
||||
res.redirect('/static/front?_id='+saved._id+'&apiToken='+saved.apiToken);
|
||||
})
|
||||
});
|
||||
|
||||
// Simple route middleware to ensure user is authenticated.
|
||||
// Use this route middleware on any resource that needs to be protected. If
|
||||
// the request is authenticated (typically via a persistent login session),
|
||||
// the request will proceed. Otherwise, the user will be redirected to the
|
||||
// login page.
|
||||
// function ensureAuthenticated(req, res, next) {
|
||||
// if (req.isAuthenticated()) { return next(); }
|
||||
// res.redirect('/login')
|
||||
// }
|
||||
};
|
||||
@@ -0,0 +1,341 @@
|
||||
// @see ../routes for routing
|
||||
|
||||
var _ = require('lodash');
|
||||
var nconf = require('nconf');
|
||||
var async = require('async');
|
||||
var algos = require('habitrpg-shared/script/algos');
|
||||
var helpers = require('habitrpg-shared/script/helpers');
|
||||
var items = require('habitrpg-shared/script/items');
|
||||
var User = require('./../models/user').model;
|
||||
var Group = require('./../models/group').model;
|
||||
var Challenge = require('./../models/challenge').model;
|
||||
var api = module.exports;
|
||||
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Challenges
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
api.list = function(req, res) {
|
||||
var user = res.locals.user;
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
// Get all available groups I belong to
|
||||
Group.find({members: {$in: [user._id]}}).select('_id').exec(cb);
|
||||
},
|
||||
function(gids, cb){
|
||||
// and their challenges
|
||||
Challenge.find({
|
||||
$or:[
|
||||
{leader: user._id},
|
||||
{members:{$in:[user._id]}}, // all challenges I belong to (is this necessary? thought is a left a group, but not its challenge)
|
||||
{group:{$in:gids}}, // all challenges in my groups
|
||||
{group: 'habitrpg'} // public group
|
||||
]
|
||||
})
|
||||
.select('name description group members prize')
|
||||
.populate('group', '_id name')
|
||||
.exec(cb);
|
||||
}
|
||||
], function(err, challenges){
|
||||
if (err) return res.json(500,{err:err});
|
||||
_.each(challenges, function(c){
|
||||
c._isMember = !!~c.members.indexOf(user._id);
|
||||
c.memberCount = _.size(c.members);
|
||||
c.members = undefined;
|
||||
})
|
||||
res.json(challenges);
|
||||
});
|
||||
}
|
||||
|
||||
// GET
|
||||
api.get = function(req, res) {
|
||||
// TODO use mapReduce() or aggregate() here to
|
||||
// 1) Find the sum of users.tasks.values within the challnege (eg, {'profile.name':'tyler', 'sum': 100})
|
||||
// 2) Sort by the sum
|
||||
// 3) Limit 30 (only show the 30 users currently in the lead)
|
||||
Challenge.findById(req.params.cid)
|
||||
.populate('members', 'profile.name _id')
|
||||
.exec(function(err, challenge){
|
||||
if(err) return res.json(500, {err:err});
|
||||
if (!challenge) return res.json(404, {err: 'Challenge ' + req.params.cid + ' not found'});
|
||||
res.json(challenge);
|
||||
})
|
||||
}
|
||||
|
||||
api.getMember = function(req, res) {
|
||||
var cid = req.params.cid, uid = req.params.uid;
|
||||
var elMatch = {$elemMatch:{'challenge.id':cid}};
|
||||
User.findById(uid)
|
||||
.select({'profile.name':1, habits:elMatch, dailys:elMatch, rewards:elMatch, todos:elMatch})
|
||||
.exec(function(err, member){
|
||||
if(err) return res.json(500, {err:err});
|
||||
if (!member) return res.json(404, {err: 'Member '+uid+' for challenge '+cid+' not found'});
|
||||
res.json(member);
|
||||
})
|
||||
}
|
||||
|
||||
// CREATE
|
||||
api.create = function(req, res){
|
||||
var user = res.locals.user;
|
||||
var group, chal;
|
||||
|
||||
// First, make sure they've selected a legit group, and store it for later
|
||||
var waterfall = [
|
||||
function(cb){
|
||||
Group.findById(req.body.group).exec(cb);
|
||||
},
|
||||
function(_group, cb){
|
||||
if (!_group) return cb("Group." + req.body.group + " not found");
|
||||
group = _group;
|
||||
cb(null);
|
||||
}
|
||||
];
|
||||
|
||||
// If they're adding a prize, do some validation
|
||||
if (+req.body.prize < 0) return res.json(401, {err: 'Challenge prize must be >= 0'});
|
||||
if (req.body.group=='habitrpg' && +req.body.prize < 1) return res.json(401, {err: 'Prize must be at least 1 Gem for public challenges.'});
|
||||
if (+req.body.prize > 0) {
|
||||
waterfall.push(function(cb){
|
||||
var groupBalance = ((group.balance && group.leader==user._id) ? group.balance : 0);
|
||||
if (req.body.prize > (user.balance*4 + groupBalance*4))
|
||||
return cb("Challenge.prize > (your gems + group balance). Purchase more gems or lower prize amount.s")
|
||||
|
||||
var net = req.body.prize/4; // I really should have stored user.balance as gems rather than dollars... stupid...
|
||||
|
||||
// user is group leader, and group has balance. Subtract from that first, then take the rest from user
|
||||
if (groupBalance > 0) {
|
||||
group.balance -= net;
|
||||
if (group.balance < 0) {
|
||||
net = Math.abs(group.balance);
|
||||
group.balance = 0;
|
||||
}
|
||||
}
|
||||
user.balance -= net;
|
||||
cb(null)
|
||||
});
|
||||
}
|
||||
|
||||
waterfall = waterfall.concat([
|
||||
function(cb) { // if we're dealing with prize above, arguemnts will be `group, numRows, cb` - else `cb`
|
||||
var chal = new Challenge(req.body); // FIXME sanitize
|
||||
chal.members.push(user._id);
|
||||
chal.save(cb)
|
||||
},
|
||||
function(_chal, num, cb){
|
||||
chal = _chal;
|
||||
group.challenges.push(chal._id);
|
||||
group.save(cb);
|
||||
},
|
||||
function(_group, num, cb) {
|
||||
// Auto-join creator to challenge (see members.push above)
|
||||
chal.syncToUser(user, cb);
|
||||
}
|
||||
]);
|
||||
async.waterfall(waterfall, function(err){
|
||||
if (err) return res.json(500, {err:err});
|
||||
res.json(chal);
|
||||
});
|
||||
}
|
||||
|
||||
// UPDATE
|
||||
api.update = function(req, res){
|
||||
var cid = req.params.cid;
|
||||
var user = res.locals.user;
|
||||
var before;
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
// We first need the original challenge data, since we're going to compare against new & decide to sync users
|
||||
Challenge.findById(cid, cb);
|
||||
},
|
||||
function(_before, cb) {
|
||||
if (!_before) return cb('Challenge ' + cid + ' not found');
|
||||
if (_before.leader != user._id) return cb("You don't have permissions to edit this challenge");
|
||||
// Update the challenge, since syncing will need the updated challenge. But store `before` we're going to do some
|
||||
// before-save / after-save comparison to determine if we need to sync to users
|
||||
before = _before;
|
||||
var attrs = _.pick(req.body, 'name shortName description habits dailys todos rewards date'.split(' '));
|
||||
Challenge.findByIdAndUpdate(cid, {$set:attrs}, cb);
|
||||
},
|
||||
function(saved, cb) {
|
||||
// after saving, we're done as far as the client's concerned. We kick of syncing (heavy task) in the background
|
||||
cb(null, saved);
|
||||
|
||||
// Compare whether any changes have been made to tasks. If so, we'll want to sync those changes to subscribers
|
||||
if (before.isOutdated(req.body)) {
|
||||
User.find({_id: {$in: saved.members}}, function(err, users){
|
||||
console.log('Challenge updated, sync to subscribers');
|
||||
if (err) throw err;
|
||||
_.each(users, function(user){
|
||||
saved.syncToUser(user);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
], function(err, saved){
|
||||
if(err) res.json(500, {err:err});
|
||||
res.json(saved);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by either delete() or selectWinner(). Will delete the challenge and set the "broken" property on all users' subscribed tasks
|
||||
* @param {cid} the challenge id
|
||||
* @param {broken} the object representing the broken status of the challenge. Eg:
|
||||
* {broken: 'CHALLENGE_DELETED', id: CHALLENGE_ID}
|
||||
* {broken: 'CHALLENGE_CLOSED', id: CHALLENGE_ID, winner: USER_NAME}
|
||||
*/
|
||||
function closeChal(cid, broken, cb) {
|
||||
var removed;
|
||||
async.waterfall([
|
||||
function(cb2){
|
||||
Challenge.findOneAndRemove({_id:cid}, cb2)
|
||||
},
|
||||
function(_removed, cb2) {
|
||||
removed = _removed;
|
||||
var pull = {'$pull':{}}; pull['$pull'][_removed._id] = 1;
|
||||
Group.findByIdAndUpdate(_removed.group, pull);
|
||||
User.find({_id:{$in: removed.members}}, cb2);
|
||||
},
|
||||
function(users, cb2) {
|
||||
var parallel = [];
|
||||
_.each(users, function(user){
|
||||
var tag = _.find(user.tags, {id:cid});
|
||||
if (tag) tag.challenge = undefined;
|
||||
_.each(user.tasks, function(task){
|
||||
if (task.challenge && task.challenge.id == removed._id) {
|
||||
_.merge(task.challenge, broken);
|
||||
}
|
||||
})
|
||||
parallel.push(function(cb3){
|
||||
user.save(cb3);
|
||||
})
|
||||
})
|
||||
async.parallel(parallel, cb2);
|
||||
}
|
||||
], cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete & close
|
||||
*/
|
||||
api['delete'] = function(req, res){
|
||||
var user = res.locals.user;
|
||||
var cid = req.params.cid;
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
Challenge.findById(cid, cb);
|
||||
},
|
||||
function(chal, cb){
|
||||
if (!chal) return cb('Challenge ' + cid + ' not found');
|
||||
if (chal.leader != user._id) return cb("You don't have permissions to edit this challenge");
|
||||
closeChal(req.params.cid, {broken: 'CHALLENGE_DELETED'}, cb);
|
||||
}
|
||||
], function(err){
|
||||
if (err) return res.json(500, {err: err});
|
||||
res.send(200);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Select Winner & Close
|
||||
*/
|
||||
api.selectWinner = function(req, res) {
|
||||
if (!req.query.uid) return res.json(401, {err: 'Must select a winner'});
|
||||
var user = res.locals.user;
|
||||
var cid = req.params.cid;
|
||||
var chal;
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
Challenge.findById(cid, cb);
|
||||
},
|
||||
function(_chal, cb){
|
||||
chal = _chal;
|
||||
if (!chal) return cb('Challenge ' + cid + ' not found');
|
||||
if (chal.leader != user._id) return cb("You don't have permissions to edit this challenge");
|
||||
User.findById(req.query.uid, cb)
|
||||
},
|
||||
function(winner, cb){
|
||||
if (!winner) return cb('Winner ' + req.query.uid + ' not found.');
|
||||
_.defaults(winner.achievements, {challenges: []});
|
||||
winner.achievements.challenges.push(chal.name);
|
||||
winner.balance += chal.prize/4;
|
||||
winner.save(cb);
|
||||
},
|
||||
function(saved, num, cb) {
|
||||
closeChal(cid, {broken: 'CHALLENGE_CLOSED', winner: saved.profile.name}, cb);
|
||||
}
|
||||
], function(err){
|
||||
if (err) return res.json(500, {err: err});
|
||||
res.send(200);
|
||||
})
|
||||
}
|
||||
|
||||
api.join = function(req, res){
|
||||
var user = res.locals.user;
|
||||
var cid = req.params.cid;
|
||||
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
Challenge.findByIdAndUpdate(cid, {$addToSet:{members:user._id}}, cb);
|
||||
},
|
||||
function(challenge, cb){
|
||||
if (!~user.challenges.indexOf(cid))
|
||||
user.challenges.unshift(cid);
|
||||
// Add all challenge's tasks to user's tasks
|
||||
challenge.syncToUser(user, function(err){
|
||||
if (err) return cb(err);
|
||||
cb(null, challenge); // we want the saved challenge in the return results, due to ng-resource
|
||||
});
|
||||
}
|
||||
], function(err, result){
|
||||
if(err) return res.json(500,{err:err});
|
||||
result._isMember = true;
|
||||
res.json(result);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
api.leave = function(req, res){
|
||||
var user = res.locals.user;
|
||||
var cid = req.params.cid;
|
||||
// whether or not to keep challenge's tasks. strictly default to true if "keep-all" isn't provided
|
||||
var keep = (/^remove-all/i).test(req.query.keep) ? 'remove-all' : 'keep-all';
|
||||
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
Challenge.findByIdAndUpdate(cid, {$pull:{members:user._id}}, cb);
|
||||
},
|
||||
function(chal, cb){
|
||||
var i = user.challenges.indexOf(cid)
|
||||
if (~i) user.challenges.splice(i,1);
|
||||
user.unlink({cid:chal._id, keep:keep}, function(err){
|
||||
if (err) return cb(err);
|
||||
cb(null, chal);
|
||||
})
|
||||
}
|
||||
], function(err, result){
|
||||
if(err) return res.json(500,{err:err});
|
||||
result._isMember = false;
|
||||
res.json(result);
|
||||
});
|
||||
}
|
||||
|
||||
api.unlink = function(req, res, next) {
|
||||
// they're scoring the task - commented out, we probably don't need it due to route ordering in api.js
|
||||
//var urlParts = req.originalUrl.split('/');
|
||||
//if (_.contains(['up','down'], urlParts[urlParts.length -1])) return next();
|
||||
|
||||
var user = res.locals.user;
|
||||
var tid = req.params.id;
|
||||
var cid = user.tasks[tid].challenge.id;
|
||||
if (!req.query.keep)
|
||||
return res.json(400, {err: 'Provide unlink method as ?keep=keep-all (keep, keep-all, remove, remove-all)'});
|
||||
user.unlink({cid:cid, keep:req.query.keep, tid:tid}, function(err, saved){
|
||||
if (err) return res.json(500,{err:err});
|
||||
res.send(200);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
var express = require('express');
|
||||
var router = new express.Router();
|
||||
var _ = require('lodash');
|
||||
var icalendar = require('icalendar');
|
||||
var api = require('./user');
|
||||
var auth = require('./auth');
|
||||
|
||||
/* ---------- Deprecated Paths ------------*/
|
||||
|
||||
|
||||
var deprecatedMessage = 'This API is no longer supported, see https://github.com/lefnire/habitrpg/wiki/API for new protocol';
|
||||
|
||||
router.get('/:uid/up/:score?', function(req, res) {
|
||||
return res.send(500, deprecatedMessage);
|
||||
});
|
||||
|
||||
router.get('/:uid/down/:score?', function(req, res) {
|
||||
return res.send(500, deprecatedMessage);
|
||||
});
|
||||
|
||||
router.post('/users/:uid/tasks/:taskId/:direction', function(req, res) {
|
||||
return res.send(500, deprecatedMessage);
|
||||
});
|
||||
|
||||
/* Redirect to new API*/
|
||||
|
||||
|
||||
var initDeprecated = function(req, res, next) {
|
||||
req.headers['x-api-user'] = req.params.uid;
|
||||
req.headers['x-api-key'] = req.body.apiToken;
|
||||
return next();
|
||||
};
|
||||
|
||||
router.post('/v1/users/:uid/tasks/:taskId/:direction', initDeprecated, auth.auth, api.scoreTask);
|
||||
|
||||
router.get('/v1/users/:uid/calendar.ics', function(req, res, next) {
|
||||
return next() //disable for now
|
||||
|
||||
var apiToken, model, query, uid;
|
||||
uid = req.params.uid;
|
||||
apiToken = req.query.apiToken;
|
||||
model = req.getModel();
|
||||
query = model.query('users').withIdAndToken(uid, apiToken);
|
||||
return query.fetch(function(err, result) {
|
||||
var formattedIcal, ical, tasks, tasksWithDates;
|
||||
if (err) {
|
||||
return res.send(500, err);
|
||||
}
|
||||
tasks = result.get('tasks');
|
||||
/* tasks = result[0].tasks*/
|
||||
|
||||
tasksWithDates = _.filter(tasks, function(task) {
|
||||
return !!task.date;
|
||||
});
|
||||
if (_.isEmpty(tasksWithDates)) {
|
||||
return res.send(500, "No events found");
|
||||
}
|
||||
ical = new icalendar.iCalendar();
|
||||
ical.addProperty('NAME', 'HabitRPG');
|
||||
_.each(tasksWithDates, function(task) {
|
||||
var d, event;
|
||||
event = new icalendar.VEvent(task.id);
|
||||
event.setSummary(task.text);
|
||||
d = new Date(task.date);
|
||||
d.date_only = true;
|
||||
event.setDate(d);
|
||||
ical.addComponent(event);
|
||||
return true;
|
||||
});
|
||||
res.type('text/calendar');
|
||||
formattedIcal = ical.toString().replace(/DTSTART\:/g, 'DTSTART;VALUE=DATE:');
|
||||
return res.send(200, formattedIcal);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,417 @@
|
||||
// @see ../routes for routing
|
||||
|
||||
var _ = require('lodash');
|
||||
var nconf = require('nconf');
|
||||
var async = require('async');
|
||||
var algos = require('habitrpg-shared/script/algos');
|
||||
var helpers = require('habitrpg-shared/script/helpers');
|
||||
var items = require('habitrpg-shared/script/items');
|
||||
var User = require('./../models/user').model;
|
||||
var Group = require('./../models/group').model;
|
||||
var api = module.exports;
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Groups
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
var itemFields = 'items.armor items.head items.shield items.weapon items.currentPet items.pets'; // TODO just send down count(items.pets) for better performance
|
||||
var partyFields = 'profile preferences stats achievements party backer contributor balance flags.rest auth.timestamps ' + itemFields;
|
||||
var nameFields = 'profile.name';
|
||||
var challengeFields = '_id name';
|
||||
var guildPopulate = {path: 'members', select: nameFields, options: {limit: 15} };
|
||||
/**
|
||||
* For parties, we want a lot of member details so we can show their avatars in the header. For guilds, we want very
|
||||
* limited fields - and only a sampling of the members, beacuse they can be in the thousands
|
||||
* @param type: 'party' or otherwise
|
||||
* @param q: the Mongoose query we're building up
|
||||
*/
|
||||
var populateQuery = function(type, q){
|
||||
if (type == 'party')
|
||||
q.populate('members', partyFields);
|
||||
else
|
||||
q.populate(guildPopulate);
|
||||
q.populate('invites', nameFields);
|
||||
q.populate('challenges', challengeFields);
|
||||
return q;
|
||||
}
|
||||
|
||||
|
||||
api.getMember = function(req, res) {
|
||||
User.findById(req.params.uid).select(partyFields).exec(function(err, user){
|
||||
if (err) return res.json(500,{err:err});
|
||||
if (!user) return res.json(400,{err:'User not found'});
|
||||
res.json(user);
|
||||
})
|
||||
}
|
||||
|
||||
api.updateMember = function(req, res) {
|
||||
var user = res.locals.user;
|
||||
if (!(user.contributor && user.contributor.admin)) return res.json(401, {err:"You don't have access to save this user"});
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
User.findById(req.params.uid, cb);
|
||||
},
|
||||
function(member, cb){
|
||||
if (!member) return res.json(404, {err: "User not found"});
|
||||
if (req.body.contributor.level > (member.contributor && member.contributor.level || 0)) {
|
||||
member.flags.contributor = true;
|
||||
member.balance += (req.body.contributor.level - (member.contributor.level || 0))*.5 // +2 gems per tier
|
||||
}
|
||||
_.merge(member, _.pick(req.body, 'contributor'));
|
||||
if (!member.items.pets) member.items.pets = [];
|
||||
var i = member.items.pets.indexOf('Dragon-Hydra');
|
||||
if (!~i && member.contributor.level >= 6) member.items.pets.push('Dragon-Hydra');
|
||||
member.save(cb);
|
||||
}
|
||||
], function(err, saved){
|
||||
if (err) return res.json(500,{err:err});
|
||||
res.json(204);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch groups list. This no longer returns party or tavern, as those can be requested indivdually
|
||||
* as /groups/party or /groups/tavern
|
||||
*/
|
||||
api.list = function(req, res) {
|
||||
var user = res.locals.user;
|
||||
var groupFields = 'name description memberCount balance leader';
|
||||
var sort = '-memberCount';
|
||||
var type = req.query.type || 'party,guilds,public,tavern';
|
||||
|
||||
async.parallel({
|
||||
|
||||
// unecessary given our ui-router setup
|
||||
party: function(cb){
|
||||
if (!~type.indexOf('party')) return cb(null, {});
|
||||
Group.findOne({type: 'party', members: {'$in': [user._id]}})
|
||||
.select(groupFields).exec(function(err, party){
|
||||
if (err) return cb(err);
|
||||
cb(null, (party === null ? [] : [party])); // return as an array for consistent ngResource use
|
||||
});
|
||||
},
|
||||
|
||||
guilds: function(cb) {
|
||||
if (!~type.indexOf('guilds')) return cb(null, []);
|
||||
Group.find({members: {'$in': [user._id]}, type:'guild'})
|
||||
.select(groupFields).sort(sort).exec(cb);
|
||||
},
|
||||
|
||||
'public': function(cb) {
|
||||
if (!~type.indexOf('public')) return cb(null, []);
|
||||
Group.find({privacy: 'public'})
|
||||
.select(groupFields + ' members')
|
||||
.sort(sort)
|
||||
.exec(function(err, groups){
|
||||
if (err) return cb(err);
|
||||
_.each(groups, function(g){
|
||||
// To save some client-side performance, don't send down the full members arr, just send down temp var _isMember
|
||||
if (~g.members.indexOf(user._id)) g._isMember = true;
|
||||
g.members = undefined;
|
||||
});
|
||||
cb(null, groups);
|
||||
});
|
||||
},
|
||||
|
||||
// unecessary given our ui-router setup
|
||||
tavern: function(cb) {
|
||||
if (!~type.indexOf('tavern')) return cb(null, {});
|
||||
Group.findById('habitrpg').select(groupFields).exec(function(err, tavern){
|
||||
if (err) return cb(err);
|
||||
cb(null, [tavern]); // return as an array for consistent ngResource use
|
||||
});
|
||||
}
|
||||
|
||||
}, function(err, results){
|
||||
if (err) return res.json(500, {err: err});
|
||||
// ngResource expects everything as arrays. We used to send it down as a structured object: {public:[], party:{}, guilds:[], tavern:{}}
|
||||
// but unfortunately ngResource top-level attrs are considered the ngModels in the list, so we had to do weird stuff and multiple
|
||||
// requests to get it to work properly. Instead, we're not depending on the client to do filtering / organization, and we're
|
||||
// just sending down a merged array. Revisit
|
||||
var arr = _.reduce(results, function(m,v){
|
||||
if (_.isEmpty(v)) return m;
|
||||
return m.concat(_.isArray(v) ? v : [v]);
|
||||
}, [])
|
||||
res.json(arr);
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* Get group
|
||||
* TODO: implement requesting fields ?fields=chat,members
|
||||
*/
|
||||
api.get = function(req, res) {
|
||||
var user = res.locals.user;
|
||||
var gid = req.params.gid;
|
||||
|
||||
var q = (gid == 'party') ? Group.findOne({type: 'party', members: {'$in': [user._id]}}) : Group.findById(gid);
|
||||
populateQuery(gid, q);
|
||||
q.exec(function(err, group){
|
||||
if (group && ((group.type == 'guild' && group.privacy == 'private') || (group.type == 'party'))) {
|
||||
if(!_.find(group.members, {_id: user._id}))
|
||||
return res.json(401, {err: "You don't have access to this group"});
|
||||
}
|
||||
res.json(group);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
api.create = function(req, res, next) {
|
||||
var group = new Group(req.body);
|
||||
var user = res.locals.user;
|
||||
|
||||
if(group.type === 'guild'){
|
||||
if(user.balance < 1) return res.json(401, {err: 'Not enough gems!'});
|
||||
|
||||
group.balance = 1;
|
||||
user.balance--;
|
||||
|
||||
user.save(function(err){
|
||||
if(err) return res.json(500,{err:err});
|
||||
group.save(function(err, saved){
|
||||
if (err) return res.json(500,{err:err});
|
||||
saved.populate('members', nameFields, function(err, populated){
|
||||
if (err) return res.json(500,{err:err});
|
||||
return res.json(populated);
|
||||
});
|
||||
});
|
||||
});
|
||||
}else{
|
||||
group.save(function(err, saved){
|
||||
if (err) return res.json(500,{err:err});
|
||||
saved.populate('members', nameFields, function(err, populated){
|
||||
if (err) return res.json(500,{err:err});
|
||||
return res.json(populated);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
api.update = function(req, res, next) {
|
||||
var group = res.locals.group;
|
||||
var user = res.locals.user;
|
||||
|
||||
if(group.leader !== user._id)
|
||||
return res.json(401, {err: "Only the group leader can update the group!"});
|
||||
|
||||
'name description logo websites logo leaderMessage leader'.split(' ').forEach(function(attr){
|
||||
group[attr] = req.body[attr];
|
||||
});
|
||||
|
||||
group.save(function(err, saved){
|
||||
if (err) return res.json(500,{err:err});
|
||||
|
||||
res.send(204);
|
||||
});
|
||||
}
|
||||
|
||||
api.attachGroup = function(req, res, next) {
|
||||
Group.findById(req.params.gid, function(err, group){
|
||||
if(err) return res.json(500, {err:err});
|
||||
res.locals.group = group;
|
||||
next();
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO make this it's own ngResource so we don't have to send down group data with each chat post
|
||||
*/
|
||||
api.postChat = function(req, res, next) {
|
||||
var user = res.locals.user
|
||||
var group = res.locals.group;
|
||||
var message = {
|
||||
id: helpers.uuid(),
|
||||
uuid: user._id,
|
||||
contributor: user.contributor && user.contributor.toObject(),
|
||||
backer: user.backer && user.backer.toObject(),
|
||||
text: req.query.message, // FIXME this should be body, but ngResource is funky
|
||||
user: user.profile.name,
|
||||
timestamp: +(new Date)
|
||||
};
|
||||
|
||||
var lastClientMsg = req.query.previousMsg;
|
||||
var chatUpdated = (lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg) ? true : false;
|
||||
|
||||
group.chat.unshift(message);
|
||||
group.chat.splice(200);
|
||||
|
||||
if (group.type === 'party') {
|
||||
user.party.lastMessageSeen = message.id;
|
||||
user.save();
|
||||
}
|
||||
|
||||
group.save(function(err, saved){
|
||||
if (err) return res.json(500, {err:err});
|
||||
|
||||
return chatUpdated ? res.json({chat: group.chat}) : res.json({message: saved.chat[0]});
|
||||
});
|
||||
}
|
||||
|
||||
api.deleteChatMessage = function(req, res){
|
||||
var user = res.locals.user
|
||||
var group = res.locals.group;
|
||||
var message = _.find(group.chat, {id: req.params.messageId});
|
||||
|
||||
if(!message) return res.json(404, {err: "Message not found!"});
|
||||
|
||||
if(user._id !== message.uuid && !(user.backer && user.contributor.admin))
|
||||
return res.json(401, {err: "Not authorized to delete this message!"})
|
||||
|
||||
var lastClientMsg = req.query.previousMsg;
|
||||
var chatUpdated = (lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg) ? true : false;
|
||||
|
||||
Group.update({_id:group._id}, {$pull:{chat:{id: req.params.messageId}}}, function(err){
|
||||
if(err) return res.json(500, {err: err});
|
||||
return chatUpdated ? res.json({chat: group.chat}) : res.send(204);
|
||||
});
|
||||
}
|
||||
|
||||
api.join = function(req, res) {
|
||||
var user = res.locals.user,
|
||||
group = res.locals.group;
|
||||
|
||||
if (group.type == 'party' && group._id == (user.invitations && user.invitations.party && user.invitations.party.id)) {
|
||||
user.invitations.party = undefined;
|
||||
user.save();
|
||||
}
|
||||
else if (group.type == 'guild' && user.invitations && user.invitations.guilds) {
|
||||
var i = _.findIndex(user.invitations.guilds, {id:group._id});
|
||||
if (~i) user.invitations.guilds.splice(i,1);
|
||||
user.save();
|
||||
}
|
||||
|
||||
if (!_.contains(group.members, user._id)){
|
||||
group.members.push(user._id);
|
||||
group.invites.splice(_.indexOf(group.invites, user._id), 1);
|
||||
}
|
||||
|
||||
async.series([
|
||||
function(cb){
|
||||
group.save(cb);
|
||||
},
|
||||
function(cb){
|
||||
populateQuery(group.type, Group.findById(group._id)).exec(cb);
|
||||
}
|
||||
], function(err, results){
|
||||
if (err) return res.json(500,{err:err});
|
||||
|
||||
// Return the group? Or not?
|
||||
res.json(results[1]);
|
||||
});
|
||||
}
|
||||
|
||||
api.leave = function(req, res, next) {
|
||||
var user = res.locals.user,
|
||||
group = res.locals.group;
|
||||
|
||||
Group.update({_id:group._id},{$pull:{members:user._id}}, function(err, saved){
|
||||
if (err) return res.json(500,{err:err});
|
||||
return res.send(204);
|
||||
});
|
||||
}
|
||||
|
||||
api.invite = function(req, res, next) {
|
||||
var group = res.locals.group;
|
||||
var uuid = req.query.uuid;
|
||||
var user = res.locals.user;
|
||||
|
||||
User.findById(uuid, function(err,invite){
|
||||
if (err) return res.json(500,{err:err});
|
||||
if (!invite)
|
||||
return res.json(400,{err:'User with id "' + uuid + '" not found'});
|
||||
if (group.type == 'guild') {
|
||||
if (_.contains(group.members,uuid))
|
||||
return res.json(400,{err: "User already in that group"});
|
||||
if (invite.invitations && invite.invitations.guilds && _.find(invite.invitations.guilds, {id:group._id}))
|
||||
return res.json(400, {err:"User already invited to that group"});
|
||||
sendInvite();
|
||||
} else if (group.type == 'party') {
|
||||
if (invite.invitations && !_.isEmpty(invite.invitations.party))
|
||||
return res.json(400,{err:"User already pending invitation."});
|
||||
Group.find({type:'party', members:{$in:[uuid]}}, function(err, groups){
|
||||
if (err) return res.json(500,{err:err});
|
||||
if (!_.isEmpty(groups))
|
||||
return res.json(400,{err:"User already in a party."})
|
||||
sendInvite();
|
||||
});
|
||||
}
|
||||
|
||||
function sendInvite (){
|
||||
if(group.type === 'guild'){
|
||||
invite.invitations.guilds.push({id: group._id, name: group.name});
|
||||
}else{
|
||||
//req.body.type in 'guild', 'party'
|
||||
invite.invitations.party = {id: group._id, name: group.name}
|
||||
}
|
||||
|
||||
group.invites.push(invite._id);
|
||||
|
||||
async.series([
|
||||
function(cb){
|
||||
invite.save(cb);
|
||||
},
|
||||
function(cb){
|
||||
group.save(cb);
|
||||
},
|
||||
function(cb){
|
||||
populateQuery(group.type, Group.findById(group._id)).exec(cb);
|
||||
}
|
||||
], function(err, results){
|
||||
if (err) return res.json(500,{err:err});
|
||||
|
||||
// Have to return whole group and its members for angular to show the invited user
|
||||
res.json(results[2]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
api.removeMember = function(req, res, next){
|
||||
var group = res.locals.group;
|
||||
var uuid = req.query.uuid;
|
||||
var user = res.locals.user;
|
||||
|
||||
if(group.leader !== user._id){
|
||||
return res.json(401, {err: "Only group leader can remove a member!"});
|
||||
}
|
||||
|
||||
if(_.contains(group.members, uuid)){
|
||||
Group.update({_id:group._id},{$pull:{members:uuid}}, function(err, saved){
|
||||
if (err) return res.json(500,{err:err});
|
||||
|
||||
// Sending an empty 204 because Group.update doesn't return the group
|
||||
// see http://mongoosejs.com/docs/api.html#model_Model.update
|
||||
return res.send(204);
|
||||
});
|
||||
}else if(_.contains(group.invites, uuid)){
|
||||
User.findById(uuid, function(err,invited){
|
||||
var invitations = invited.invitations;
|
||||
if(group.type === 'guild'){
|
||||
invitations.guilds.splice(_.indexOf(invitations.guilds, group._id), 1);
|
||||
}else{
|
||||
invitations.party = undefined;
|
||||
}
|
||||
|
||||
async.series([
|
||||
function(cb){
|
||||
invited.save(cb);
|
||||
},
|
||||
function(cb){
|
||||
Group.update({_id:group._id},{$pull:{invites:uuid}}, cb);
|
||||
}
|
||||
], function(err, results){
|
||||
if (err) return res.json(500,{err:err});
|
||||
|
||||
// Sending an empty 204 because Group.update doesn't return the group
|
||||
// see http://mongoosejs.com/docs/api.html#model_Model.update
|
||||
return res.send(204);
|
||||
});
|
||||
|
||||
});
|
||||
}else{
|
||||
return res.json(400, {err: "User not found among group's members!"});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
/* @see ./routes.coffee for routing*/
|
||||
|
||||
var url = require('url');
|
||||
var ipn = require('paypal-ipn');
|
||||
var _ = require('lodash');
|
||||
var nconf = require('nconf');
|
||||
var async = require('async');
|
||||
var algos = require('habitrpg-shared/script/algos');
|
||||
var helpers = require('habitrpg-shared/script/helpers');
|
||||
var items = require('habitrpg-shared/script/items');
|
||||
var validator = require('validator');
|
||||
var check = validator.check;
|
||||
var sanitize = validator.sanitize;
|
||||
var User = require('./../models/user').model;
|
||||
var Group = require('./../models/group').model;
|
||||
var Challenge = require('./../models/challenge').model;
|
||||
var api = module.exports;
|
||||
|
||||
// FIXME put this in a proper location
|
||||
api.marketBuy = function(req, res, next){
|
||||
var user = res.locals.user,
|
||||
type = req.query.type,
|
||||
item = req.body;
|
||||
|
||||
if (!_.contains(['hatchingPotion', 'egg'], req.query.type))
|
||||
return res.json(400, {err: "Type must be in 'hatchingPotion' or 'egg'"});
|
||||
var item;
|
||||
if (type == 'egg'){
|
||||
if (!user.items && !user.items.eggs) user.items.eggs = [];
|
||||
user.items.eggs.push(item);
|
||||
} else {
|
||||
if (!user.items && !user.items.hatchingPotions) user.items.hatchingPotions = [];
|
||||
user.items.hatchingPotions.push(item.name);
|
||||
}
|
||||
user.markModified('items'); // I still don't get when this is necessary and when not..
|
||||
user.balance -= (item.value/4);
|
||||
user.save(function(err, saved){
|
||||
if (err) return res.json(500, {err:err});
|
||||
res.json(saved);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Tasks
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
Local Methods
|
||||
---------------
|
||||
*/
|
||||
|
||||
/*
|
||||
Validate task
|
||||
*/
|
||||
api.verifyTaskExists = function(req, res, next) {
|
||||
// If we're updating, get the task from the user
|
||||
var task = res.locals.user.tasks[req.params.id];
|
||||
if (_.isEmpty(task)) return res.json(400, {err: "No task found."});
|
||||
res.locals.task = task;
|
||||
return next();
|
||||
};
|
||||
|
||||
function addTask(user, task) {
|
||||
task = helpers.taskDefaults(task);
|
||||
user[task.type+'s'].unshift(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
/*
|
||||
API Routes
|
||||
---------------
|
||||
*/
|
||||
|
||||
/**
|
||||
This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login
|
||||
Export it also so we can call it from deprecated.coffee
|
||||
*/
|
||||
api.scoreTask = function(req, res, next) {
|
||||
var id = req.params.id,
|
||||
direction = req.params.direction,
|
||||
user = res.locals.user,
|
||||
task;
|
||||
|
||||
// Send error responses for improper API call
|
||||
if (!id) return res.json(500, {err: ':id required'});
|
||||
if (direction !== 'up' && direction !== 'down') {
|
||||
if (direction == 'unlink') return next();
|
||||
return res.json(500, {err: ":direction must be 'up' or 'down'"});
|
||||
}
|
||||
// If exists already, score it
|
||||
if (task = user.tasks[id]) {
|
||||
// Set completed if type is daily or todo and task exists
|
||||
if (task.type === 'daily' || task.type === 'todo') {
|
||||
task.completed = direction === 'up';
|
||||
}
|
||||
} else {
|
||||
// If it doesn't exist, this is likely a 3rd party up/down - create a new one, then score it
|
||||
task = {
|
||||
id: id,
|
||||
value: 0,
|
||||
type: req.body.type || 'habit',
|
||||
text: req.body.title || id,
|
||||
notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task."
|
||||
};
|
||||
if (task.type === 'habit') {
|
||||
task.up = task.down = true;
|
||||
}
|
||||
if (task.type === 'daily' || task.type === 'todo') {
|
||||
task.completed = direction === 'up';
|
||||
}
|
||||
task = addTask(user, task);
|
||||
}
|
||||
var delta = algos.score(user, task, direction);
|
||||
//user.markModified('flags');
|
||||
user.save(function(err, saved) {
|
||||
if (err) return res.json(500, {err: err});
|
||||
res.json(200, _.extend({
|
||||
delta: delta
|
||||
}, saved.toJSON().stats));
|
||||
});
|
||||
|
||||
// if it's a challenge task, sync the score
|
||||
user.syncScoreToChallenge(task, delta);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all tasks
|
||||
*/
|
||||
api.getTasks = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
if (req.query.type) {
|
||||
return res.json(user[req.query.type+'s']);
|
||||
} else {
|
||||
return res.json(_.toArray(user.tasks));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Task
|
||||
*/
|
||||
api.getTask = function(req, res, next) {
|
||||
var task = res.locals.user.tasks[req.params.id];
|
||||
if (_.isEmpty(task)) return res.json(400, {err: "No task found."});
|
||||
return res.json(200, task);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete Task
|
||||
*/
|
||||
api.deleteTask = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
user.deleteTask(res.locals.task.id);
|
||||
user.save(function(err) {
|
||||
if (err) return res.json(500, {err: err});
|
||||
res.send(204);
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
Update Task
|
||||
*/
|
||||
api.updateTask = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
var tid = req.params.id;
|
||||
var task = user.tasks[req.params.id];
|
||||
_.merge(task, req.body);
|
||||
user.save(function(err, saved) {
|
||||
if (err) return res.json(500, {err: err})
|
||||
return res.json(200, task);
|
||||
});
|
||||
};
|
||||
|
||||
api.createTask = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
var task = addTask(user, req.body);
|
||||
user.save(function(err, saved) {
|
||||
if (err) return res.json(500, {err: err});
|
||||
return res.json(201, task);
|
||||
});
|
||||
};
|
||||
|
||||
api.sortTask = function(req, res, next) {
|
||||
var id = req.params.id;
|
||||
var to = req.body.to, from = req.body.from, type = req.body.type;
|
||||
var user = res.locals.user;
|
||||
user[type+'s'].splice(to, 0, user[type+'s'].splice(from, 1)[0]);
|
||||
user.save(function(err, saved) {
|
||||
if (err) return res.json(500, {err: err});
|
||||
return res.json(200, saved.toJSON()[type+'s']);
|
||||
});
|
||||
};
|
||||
|
||||
api.clearCompleted = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
user.todos = _.where(user.todos, {completed: false});
|
||||
return user.save(function(err, saved) {
|
||||
if (err) return res.json(500, {err: err});
|
||||
return res.json(saved);
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Items
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
api.buy = function(req, res, next) {
|
||||
var hasEnough, type, user;
|
||||
user = res.locals.user;
|
||||
type = req.params.type;
|
||||
if (type !== 'weapon' && type !== 'armor' && type !== 'head' && type !== 'shield' && type !== 'potion') {
|
||||
return res.json(400, {err: ":type must be in one of: 'weapon', 'armor', 'head', 'shield', 'potion'"});
|
||||
}
|
||||
hasEnough = items.buyItem(user, type);
|
||||
if (hasEnough) {
|
||||
return user.save(function(err, saved) {
|
||||
if (err) return res.json(500, {err: err});
|
||||
return res.json(200, saved.toJSON().items);
|
||||
});
|
||||
} else {
|
||||
return res.json(200, {err: "Not enough GP"});
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
User
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get User
|
||||
*/
|
||||
api.getUser = function(req, res, next) {
|
||||
var user = res.locals.user.toJSON();
|
||||
user.stats.toNextLevel = algos.tnl(user.stats.lvl);
|
||||
user.stats.maxHealth = 50;
|
||||
delete user.apiToken;
|
||||
if (user.auth) {
|
||||
delete user.auth.hashed_password;
|
||||
delete user.auth.salt;
|
||||
}
|
||||
return res.json(200, user);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update user
|
||||
* FIXME add documentation here
|
||||
*/
|
||||
api.updateUser = function(req, res, next) {
|
||||
var acceptableAttrs, errors, user;
|
||||
user = res.locals.user;
|
||||
errors = [];
|
||||
if (_.isEmpty(req.body)) {
|
||||
return res.json(200, user);
|
||||
}
|
||||
/*
|
||||
# FIXME we need to do some crazy sanitiazation if they're using the old `PUT /user {data}` method.
|
||||
# The new `PUT /user {'stats.hp':50}
|
||||
|
||||
# FIXME - one-by-one we want to widdle down this list, instead replacing each needed set path with API operations
|
||||
# There's a trick here. In order to prevent prevent clobering top-level paths, we add `.` to make sure they're
|
||||
# sending bodies as {"set.this.path":value} instead of {set:{this:{path:value}}}. Permit lastCron since it's top-level
|
||||
# Note: custom is for 3rd party apps
|
||||
*/
|
||||
|
||||
acceptableAttrs = 'tasks. achievements. filters. flags. invitations. items. lastCron party. preferences. profile. stats. tags custom.'.split(' ');
|
||||
_.each(req.body, function(v, k) {
|
||||
if ((_.find(acceptableAttrs, function(attr) {
|
||||
return k.indexOf(attr) === 0;
|
||||
})) != null) {
|
||||
if (_.isObject(v)) {
|
||||
errors.push("Value for " + k + " was an object. Be careful here, you could clobber stuff.");
|
||||
}
|
||||
helpers.dotSet(k, v, user);
|
||||
} else {
|
||||
errors.push("path `" + k + "` was not saved, as it's a protected path. Make sure to send `PUT /api/v1/user` request bodies as `{'set.this.path':value}` instead of `{set:{this:{path:value}}}`");
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return user.save(function(err) {
|
||||
if (!_.isEmpty(errors)) {
|
||||
return res.json(500, {
|
||||
err: errors
|
||||
});
|
||||
}
|
||||
if (err) {
|
||||
return res.json(500, {
|
||||
err: err
|
||||
});
|
||||
}
|
||||
return res.json(200, user);
|
||||
});
|
||||
};
|
||||
|
||||
api.cron = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
algos.cron(user);
|
||||
if (user.isModified()) {
|
||||
res.locals.wasModified = true;
|
||||
user.auth.timestamps.loggedin = new Date();
|
||||
}
|
||||
user.save(next);
|
||||
};
|
||||
|
||||
api.revive = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
algos.revive(user);
|
||||
user.save(function(err, saved) {
|
||||
if (err) return res.json(500, {err: err});
|
||||
return res.json(200, saved);
|
||||
});
|
||||
};
|
||||
|
||||
api.reroll = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
if (user.balance < 1) return res.json(401, {err: "Not enough tokens."});
|
||||
user.balance -= 1;
|
||||
_.each(['habits','dailys','todos'], function(type){
|
||||
_.each(user[type], function(task){
|
||||
task.value = 0;
|
||||
})
|
||||
})
|
||||
user.stats.hp = 50;
|
||||
user.save(function(err, saved) {
|
||||
if (err) return res.json(500, {err: err});
|
||||
return res.json(200, saved);
|
||||
});
|
||||
};
|
||||
|
||||
api.reset = function(req, res){
|
||||
var user = res.locals.user;
|
||||
user.habits = [];
|
||||
user.dailys = [];
|
||||
user.todos = [];
|
||||
user.rewards = [];
|
||||
|
||||
user.stats.hp = 50;
|
||||
user.stats.lvl = 1;
|
||||
user.stats.gp = 0;
|
||||
user.stats.exp = 0;
|
||||
|
||||
user.items.armor = 0;
|
||||
user.items.weapon = 0;
|
||||
user.items.head = 0;
|
||||
user.items.shield = 0;
|
||||
|
||||
user.save(function(err, saved){
|
||||
if (err) return res.json(500,{err:err});
|
||||
res.json(saved);
|
||||
})
|
||||
}
|
||||
|
||||
api['delete'] = function(req, res) {
|
||||
res.locals.user.remove(function(err){
|
||||
if (err) return res.json(500,{err:err});
|
||||
res.send(200);
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Unlock Preferences
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
api.unlock = function(req, res) {
|
||||
var user = res.locals.user;
|
||||
var path = req.query.path;
|
||||
var fullSet = ~path.indexOf(',');
|
||||
|
||||
// 5G per set, 2G per individual
|
||||
cost = fullSet ? 1.25 : 0.5;
|
||||
|
||||
if (user.balance < cost)
|
||||
return res.json(401, {err: 'Not enough gems'});
|
||||
|
||||
if (fullSet) {
|
||||
var paths = path.split(',');
|
||||
_.each(paths, function(p){
|
||||
helpers.dotSet('purchased.' + p, true, user);
|
||||
});
|
||||
} else {
|
||||
if (helpers.dotGet('purchased.' + path, user) === true)
|
||||
return res.json(401, {err: 'User already purchased that'});
|
||||
helpers.dotSet('purchased.' + path, true, user);
|
||||
}
|
||||
|
||||
user.balance -= cost;
|
||||
user._v++;
|
||||
user.markModified('purchased');
|
||||
user.save(function(err, saved){
|
||||
if (err) res.json(500, {err:err});
|
||||
res.send(200);
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Buy Gems
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
api.addTenGems = function(req, res) {
|
||||
var user = res.locals.user;
|
||||
user.balance += 2.5;
|
||||
user.save(function(err){
|
||||
if (err) return res.json(500,{err:err});
|
||||
res.send(204);
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Setup Stripe response when posting payment
|
||||
*/
|
||||
api.buyGems = function(req, res) {
|
||||
var api_key = nconf.get('STRIPE_API_KEY');
|
||||
var stripe = require("stripe")(api_key);
|
||||
var token = req.body.id;
|
||||
// console.dir {token:token, req:req}, 'stripe'
|
||||
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
stripe.charges.create({
|
||||
amount: "500", // $5
|
||||
currency: "usd",
|
||||
card: token
|
||||
}, cb);
|
||||
},
|
||||
function(response, cb) {
|
||||
res.locals.user.balance += 5;
|
||||
res.locals.user.purchased.ads = true;
|
||||
res.locals.user.save(cb);
|
||||
}
|
||||
], function(err, saved){
|
||||
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
|
||||
res.send(200, saved);
|
||||
});
|
||||
};
|
||||
|
||||
api.buyGemsPaypalIPN = function(req, res) {
|
||||
res.send(200);
|
||||
ipn.verify(req.body, function callback(err, msg) {
|
||||
if (err) {
|
||||
console.error(msg);
|
||||
res.send(500, msg);
|
||||
} else {
|
||||
if (req.body.payment_status == 'Completed') {
|
||||
//Payment has been confirmed as completed
|
||||
var parts = url.parse(req.body.custom, true);
|
||||
var uid = parts.query.uid; //, apiToken = query.apiToken;
|
||||
if (!uid) throw new Error("uuid or apiToken not found when completing paypal transaction");
|
||||
User.findById(uid, function(err, user) {
|
||||
if (err) throw err;
|
||||
if (_.isEmpty(user)) throw "user not found with uuid " + uuid + " when completing paypal transaction"
|
||||
user.balance += 5;
|
||||
user.purchased.ads = true;
|
||||
user.save();
|
||||
console.log('PayPal transaction completed and user updated');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Tags
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
api.deleteTag = function(req, res){
|
||||
var user = res.locals.user;
|
||||
var tid = req.params.tid || req.body.tag;
|
||||
var i = _.findIndex(user.tags, {id:tid});
|
||||
if (~i) {
|
||||
var tag = user.tags[i];
|
||||
delete user.filters[tag.id];
|
||||
user.tags.splice(i,1);
|
||||
// remove tag from all tasks
|
||||
_.each(['habits','dailys','todos','rewards'], function(type){
|
||||
_.each(user[type], function(task){
|
||||
delete task.tags[tag.id];
|
||||
})
|
||||
})
|
||||
user.save(function(err,saved){
|
||||
if (err) return res.json(500, {err: err});
|
||||
// Need to use this until we found a way to update the ui for tasks when a tag is deleted
|
||||
res.locals.wasModified = true;
|
||||
res.send(200);
|
||||
});
|
||||
} else {
|
||||
res.json(400, {err:'Tag not found'});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Batch Update
|
||||
Run a bunch of updates all at once
|
||||
------------------------------------------------------------------------
|
||||
*/
|
||||
api.batchUpdate = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
var oldSend = res.send;
|
||||
var oldJson = res.json;
|
||||
var performAction = function(action, cb) {
|
||||
|
||||
// TODO come up with a more consistent approach here. like:
|
||||
// req.body=action.data; delete action.data; _.defaults(req.params, action)
|
||||
// Would require changing action.dir on mobile app
|
||||
req.params.id = action.data && action.data.id;
|
||||
req.params.direction = action.dir;
|
||||
req.params.type = action.type;
|
||||
req.body = action.data;
|
||||
res.send = res.json = function(code, data) {
|
||||
if (_.isNumber(code) && code >= 400) {
|
||||
console.error({
|
||||
code: code,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
//FIXME send error messages down
|
||||
return cb();
|
||||
};
|
||||
switch (action.op) {
|
||||
case "score":
|
||||
api.scoreTask(req, res);
|
||||
break;
|
||||
case "buy":
|
||||
api.buy(req, res);
|
||||
break;
|
||||
case "sortTask":
|
||||
api.verifyTaskExists(req, res, function() {
|
||||
api.sortTask(req, res);
|
||||
});
|
||||
break;
|
||||
case "addTask":
|
||||
api.createTask(req, res);
|
||||
break;
|
||||
case "delTask":
|
||||
api.verifyTaskExists(req, res, function() {
|
||||
api.deleteTask(req, res);
|
||||
});
|
||||
break;
|
||||
case "set":
|
||||
api.updateUser(req, res);
|
||||
break;
|
||||
case "delTag":
|
||||
api.deleteTag(req, res);
|
||||
break;
|
||||
case "revive":
|
||||
api.revive(req, res);
|
||||
break;
|
||||
case "clear-completed":
|
||||
api.clearCompleted(req, res);
|
||||
break;
|
||||
case "reroll":
|
||||
api.reroll(req, res);
|
||||
break;
|
||||
default:
|
||||
cb();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Setup the array of functions we're going to call in parallel with async
|
||||
var actions = _.transform(req.body || [], function(result, action) {
|
||||
if (!_.isEmpty(action)) {
|
||||
result.push(function(cb) {
|
||||
performAction(action, cb);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// call all the operations, then return the user object to the requester
|
||||
async.series(actions, function(err) {
|
||||
res.json = oldJson;
|
||||
res.send = oldSend;
|
||||
if (err) return res.json(500, {err: err});
|
||||
var response = user.toJSON();
|
||||
response.wasModified = res.locals.wasModified;
|
||||
if (response._tmp && response._tmp.drop) response.wasModified = true;
|
||||
|
||||
// Send the response to the server
|
||||
if(response.wasModified){
|
||||
res.json(200, response);
|
||||
}else{
|
||||
res.json(200, {_v: response._v});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
var nconf = require('nconf');
|
||||
var _ = require('lodash');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
module.exports.forceSSL = function(req, res, next){
|
||||
var baseUrl = nconf.get("BASE_URL");
|
||||
// Note x-forwarded-proto is used by Heroku & nginx, you'll have to do something different if you're not using those
|
||||
if (req.headers['x-forwarded-proto'] && req.headers['x-forwarded-proto'] !== 'https'
|
||||
&& nconf.get('NODE_ENV') === 'production'
|
||||
&& baseUrl.indexOf('https') === 0) {
|
||||
return res.redirect(baseUrl + req.url);
|
||||
}
|
||||
next()
|
||||
}
|
||||
|
||||
module.exports.splash = function(req, res, next) {
|
||||
if (req.url == '/' && !req.headers['x-api-user'] && !req.headers['x-api-key'] && !(req.session && req.session.userId))
|
||||
return res.redirect('/static/front')
|
||||
next()
|
||||
};
|
||||
|
||||
module.exports.cors = function(req, res, next) {
|
||||
res.header("Access-Control-Allow-Origin", req.headers.origin || "*");
|
||||
res.header("Access-Control-Allow-Methods", "OPTIONS,GET,POST,PUT,HEAD,DELETE");
|
||||
res.header("Access-Control-Allow-Headers", "Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key");
|
||||
if (req.method === 'OPTIONS') return res.send(200);
|
||||
return next();
|
||||
};
|
||||
|
||||
var buildFiles = [];
|
||||
|
||||
var walk = function(folder){
|
||||
var res = fs.readdirSync(folder);
|
||||
|
||||
res.forEach(function(fileName){
|
||||
file = folder + '/' + fileName;
|
||||
if(fs.statSync(file).isDirectory()){
|
||||
walk(file);
|
||||
}else{
|
||||
var relFolder = path.relative(path.join(__dirname, "/../build"), folder);
|
||||
var old = fileName.replace(/-.{8}(\.[\d\w]+)$/, '$1');
|
||||
|
||||
if(relFolder){
|
||||
old = relFolder + '/' + old;
|
||||
fileName = relFolder + '/' + fileName;
|
||||
}
|
||||
|
||||
buildFiles[old] = fileName
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
walk(path.join(__dirname, "/../build"));
|
||||
|
||||
var getBuildUrl = function(url){
|
||||
if(buildFiles[url]) return '/' + buildFiles[url];
|
||||
|
||||
return '/' + url;
|
||||
}
|
||||
|
||||
var manifestFiles = require("../public/manifest.json");
|
||||
|
||||
var getManifestFiles = function(page){
|
||||
var files = manifestFiles[page];
|
||||
|
||||
if(!files) throw new Error("Page not found!");
|
||||
|
||||
var css = '';
|
||||
|
||||
_.each(files.css, function(file){
|
||||
css += '<link rel="stylesheet" type="text/css" href="' + getBuildUrl(file) + '">';
|
||||
});
|
||||
|
||||
if(nconf.get('NODE_ENV') === 'production'){
|
||||
return css + '<script type="text/javascript" src="' + getBuildUrl(page + '.js') + '"></script>';
|
||||
}else{
|
||||
var results = css;
|
||||
_.each(files.js, function(file){
|
||||
results += '<script type="text/javascript" src="' + getBuildUrl(file) + '"></script>';
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports.locals = function(req, res, next) {
|
||||
res.locals.habitrpg = res.locals.habitrpg || {}
|
||||
_.defaults(res.locals.habitrpg, {
|
||||
NODE_ENV: nconf.get('NODE_ENV'),
|
||||
BASE_URL: nconf.get('BASE_URL'),
|
||||
PAYPAL_MERCHANT: nconf.get('PAYPAL_MERCHANT'),
|
||||
IS_MOBILE: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header('User-Agent')),
|
||||
STRIPE_PUB_KEY: nconf.get('STRIPE_PUB_KEY'),
|
||||
getManifestFiles: getManifestFiles,
|
||||
getBuildUrl: getBuildUrl
|
||||
});
|
||||
next()
|
||||
}
|
||||
/*
|
||||
// translate = (req, res, next) ->
|
||||
// model = req.getModel()
|
||||
// # Set locale to bg on dev
|
||||
// #model.set '_i18n.locale', 'bg' if process.env.NODE_ENV is "development"
|
||||
// next()
|
||||
*/
|
||||
@@ -0,0 +1,122 @@
|
||||
var mongoose = require("mongoose");
|
||||
var Schema = mongoose.Schema;
|
||||
var helpers = require('habitrpg-shared/script/helpers');
|
||||
var _ = require('lodash');
|
||||
var TaskSchema = require('./task').schema;
|
||||
var Group = require('./group').model;
|
||||
|
||||
var ChallengeSchema = new Schema({
|
||||
_id: {type: String, 'default': helpers.uuid},
|
||||
name: String,
|
||||
shortName: String,
|
||||
description: String,
|
||||
habits: [TaskSchema],
|
||||
dailys: [TaskSchema],
|
||||
todos: [TaskSchema],
|
||||
rewards: [TaskSchema],
|
||||
leader: {type: String, ref: 'User'},
|
||||
group: {type: String, ref: 'Group'},
|
||||
timestamp: {type: Date, 'default': Date.now},
|
||||
members: [{type: String, ref: 'User'}],
|
||||
memberCount: {type: Number, 'default': 0},
|
||||
prize: {type: Number, 'default': 0}
|
||||
});
|
||||
|
||||
ChallengeSchema.virtual('tasks').get(function () {
|
||||
var tasks = this.habits.concat(this.dailys).concat(this.todos).concat(this.rewards);
|
||||
var tasks = _.object(_.pluck(tasks,'id'), tasks);
|
||||
return tasks;
|
||||
});
|
||||
|
||||
// FIXME this isn't always triggered, since we sometimes use update() or findByIdAndUpdate()
|
||||
// @see https://github.com/LearnBoost/mongoose/issues/964
|
||||
ChallengeSchema.pre('save', function(next){
|
||||
this.memberCount = _.size(this.members);
|
||||
next()
|
||||
})
|
||||
|
||||
ChallengeSchema.methods.toJSON = function(){
|
||||
var doc = this.toObject();
|
||||
doc.memberCount = doc.members ? _.size(doc.members) : doc.memberCount; // @see pre('save') comment above
|
||||
doc._isMember = this._isMember;
|
||||
return doc;
|
||||
}
|
||||
|
||||
// --------------
|
||||
// Syncing logic
|
||||
// --------------
|
||||
|
||||
function syncableAttrs(task) {
|
||||
var t = (task.toObject) ? task.toObject() : task; // lodash doesn't seem to like _.omit on EmbeddedDocument
|
||||
// only sync/compare important attrs
|
||||
var omitAttrs = 'history tags completed streak'.split(' ');
|
||||
if (t.type != 'reward') omitAttrs.push('value');
|
||||
return _.omit(t, omitAttrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare whether any changes have been made to tasks. If so, we'll want to sync those changes to subscribers
|
||||
*/
|
||||
function comparableData(obj) {
|
||||
return (
|
||||
_.chain(obj.habits.concat(obj.dailys).concat(obj.todos).concat(obj.rewards))
|
||||
.sortBy('id') // we don't want to update if they're sort-order is different
|
||||
.transform(function(result, task){
|
||||
result.push(syncableAttrs(task));
|
||||
}))
|
||||
.toString(); // for comparing arrays easily
|
||||
}
|
||||
|
||||
ChallengeSchema.methods.isOutdated = function(newData) {
|
||||
return comparableData(this) !== comparableData(newData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs all new tasks, deleted tasks, etc to the user object
|
||||
* @param user
|
||||
* @return nothing, user is modified directly. REMEMBER to save the user!
|
||||
*/
|
||||
ChallengeSchema.methods.syncToUser = function(user, cb) {
|
||||
if (!user) return;
|
||||
var self = this;
|
||||
self.shortName = self.shortName || self.name;
|
||||
|
||||
// Sync tags
|
||||
var tags = user.tags || [];
|
||||
var i = _.findIndex(tags, {id: self._id})
|
||||
if (~i) {
|
||||
if (tags[i].name !== self.shortName) {
|
||||
// update the name - it's been changed since
|
||||
user.tags[i].name = self.shortName;
|
||||
}
|
||||
} else {
|
||||
user.tags.push({
|
||||
id: self._id,
|
||||
name: self.shortName,
|
||||
challenge: true
|
||||
});
|
||||
}
|
||||
|
||||
// Sync new tasks and updated tasks
|
||||
_.each(self.tasks, function(task){
|
||||
var list = user[task.type+'s'];
|
||||
var userTask = user.tasks[task.id] || (list.push(syncableAttrs(task)), list[list.length-1]);
|
||||
userTask.challenge = {id:self._id};
|
||||
userTask.tags = userTask.tags || {};
|
||||
userTask.tags[self._id] = true;
|
||||
_.merge(userTask, syncableAttrs(task));
|
||||
})
|
||||
|
||||
// Flag deleted tasks as "broken"
|
||||
_.each(user.tasks, function(task){
|
||||
if (task.challenge && task.challenge.id==self._id && !self.tasks[task.id]) {
|
||||
task.challenge.broken = 'TASK_DELETED';
|
||||
}
|
||||
})
|
||||
|
||||
user.save(cb);
|
||||
};
|
||||
|
||||
|
||||
module.exports.schema = ChallengeSchema;
|
||||
module.exports.model = mongoose.model("Challenge", ChallengeSchema);
|
||||
@@ -0,0 +1,83 @@
|
||||
var mongoose = require("mongoose");
|
||||
var Schema = mongoose.Schema;
|
||||
var helpers = require('habitrpg-shared/script/helpers');
|
||||
var _ = require('lodash');
|
||||
|
||||
var GroupSchema = new Schema({
|
||||
_id: {type: String, 'default': helpers.uuid},
|
||||
name: String,
|
||||
description: String,
|
||||
leader: {type: String, ref: 'User'},
|
||||
members: [{type: String, ref: 'User'}],
|
||||
invites: [{type: String, ref: 'User'}],
|
||||
type: {type: String, "enum": ['guild', 'party']},
|
||||
privacy: {type: String, "enum": ['private', 'public']},
|
||||
_v: {type: Number,'default': 0},
|
||||
websites: Array,
|
||||
chat: Array,
|
||||
/*
|
||||
# [{
|
||||
# timestamp: Date
|
||||
# user: String
|
||||
# text: String
|
||||
# contributor: String
|
||||
# uuid: String
|
||||
# id: String
|
||||
# }]
|
||||
*/
|
||||
|
||||
memberCount: {type: Number, 'default': 0},
|
||||
challengeCount: {type: Number, 'default': 0},
|
||||
balance: Number,
|
||||
logo: String,
|
||||
leaderMessage: String,
|
||||
challenges: [{type:'String', ref:'Challenge'}] // do we need this? could depend on back-ref instead (Challenge.find({group:GID}))
|
||||
}, {
|
||||
strict: 'throw',
|
||||
minimize: false // So empty objects are returned
|
||||
});
|
||||
|
||||
/**
|
||||
* Derby duplicated stuff. This is a temporary solution, once we're completely off derby we'll run an mongo migration
|
||||
* to remove duplicates, then take these fucntions out
|
||||
*/
|
||||
function removeDuplicates(doc){
|
||||
// Remove duplicate members
|
||||
if (doc.members) {
|
||||
var uniqMembers = _.uniq(doc.members);
|
||||
if (uniqMembers.length != doc.members.length) {
|
||||
doc.members = uniqMembers;
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.websites) {
|
||||
var uniqWebsites = _.uniq(doc.websites);
|
||||
if (uniqWebsites.length != doc.websites.length) {
|
||||
doc.websites = uniqWebsites;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME this isn't always triggered, since we sometimes use update() or findByIdAndUpdate()
|
||||
// @see https://github.com/LearnBoost/mongoose/issues/964
|
||||
GroupSchema.pre('save', function(next){
|
||||
removeDuplicates(this);
|
||||
this.memberCount = _.size(this.members);
|
||||
this.challengeCount = _.size(this.challenges);
|
||||
next();
|
||||
})
|
||||
|
||||
GroupSchema.methods.toJSON = function(){
|
||||
var doc = this.toObject();
|
||||
removeDuplicates(doc);
|
||||
doc._isMember = this._isMember;
|
||||
|
||||
// @see pre('save') comment above
|
||||
this.memberCount = _.size(this.members);
|
||||
this.challengeCount = _.size(this.challenges);
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
module.exports.schema = GroupSchema;
|
||||
module.exports.model = mongoose.model("Group", GroupSchema);
|
||||
@@ -0,0 +1,48 @@
|
||||
// User.js
|
||||
// =======
|
||||
// Defines the user data model (schema) for use via the API.
|
||||
|
||||
// Dependencies
|
||||
// ------------
|
||||
var mongoose = require("mongoose");
|
||||
var Schema = mongoose.Schema;
|
||||
var helpers = require('habitrpg-shared/script/helpers');
|
||||
var _ = require('lodash');
|
||||
|
||||
// Task Schema
|
||||
// -----------
|
||||
|
||||
var TaskSchema = new Schema({
|
||||
//_id:{type: String,'default': helpers.uuid},
|
||||
id: {type: String,'default': helpers.uuid},
|
||||
history: Array, // [{date:Date, value:Number}], // this causes major performance problems
|
||||
text: String,
|
||||
date: String, // due date for todos // FIXME we're getting parse errors, people have stored as "today" and "3/13". Need to run a migration & put this back to type: Date
|
||||
notes: {type: String, 'default': ''},
|
||||
tags: {type: Schema.Types.Mixed, 'default': {}}, //{ "4ddf03d9-54bd-41a3-b011-ca1f1d2e9371" : true },
|
||||
type: {type:String, 'default': 'habit'}, // habit, daily
|
||||
up: {type: Boolean, 'default': true},
|
||||
down: {type: Boolean, 'default': true},
|
||||
value: {type: Number, 'default': 0},
|
||||
completed: {type: Boolean, 'default': false},
|
||||
priority: {type: String, 'default': '!'}, //'!!' // FIXME this should be a number or something
|
||||
repeat: {type: Schema.Types.Mixed, 'default': {m:1, t:1, w:1, th:1, f:1, s:1, su:1} },
|
||||
streak: {type: Number, 'default': 0},
|
||||
challenge: {
|
||||
id: {type: 'String', ref:'Challenge'},
|
||||
broken: String, // CHALLENGE_DELETED, TASK_DELETED, UNSUBSCRIBED, CHALLENGE_CLOSED
|
||||
winner: String // user.profile.name
|
||||
// group: {type: 'Strign', ref: 'Group'} // if we restore this, rename `id` above to `challenge`
|
||||
}
|
||||
},{
|
||||
_id: false
|
||||
});
|
||||
|
||||
/**
|
||||
* Workaround for bug when _id & id were out of sync, we can remove this after challenges has been running for a while
|
||||
*/
|
||||
TaskSchema.post('init', function(doc){
|
||||
if (!doc.id && doc._id) doc.id = doc._id;
|
||||
})
|
||||
|
||||
module.exports.schema = TaskSchema;
|
||||
@@ -0,0 +1,303 @@
|
||||
// User.js
|
||||
// =======
|
||||
// Defines the user data model (schema) for use via the API.
|
||||
|
||||
// Dependencies
|
||||
// ------------
|
||||
var mongoose = require("mongoose");
|
||||
var Schema = mongoose.Schema;
|
||||
var helpers = require('habitrpg-shared/script/helpers');
|
||||
var _ = require('lodash');
|
||||
var TaskSchema = require('./task').schema;
|
||||
var Challenge = require('./challenge').model;
|
||||
|
||||
// User Schema
|
||||
// -----------
|
||||
|
||||
var UserSchema = new Schema({
|
||||
// ### UUID and API Token
|
||||
_id: {
|
||||
type: String,
|
||||
'default': helpers.uuid
|
||||
},
|
||||
apiToken: {
|
||||
type: String,
|
||||
'default': helpers.uuid
|
||||
},
|
||||
|
||||
// ### Mongoose Update Object
|
||||
// We want to know *every* time an object updates. Mongoose uses __v to designate when an object contains arrays which
|
||||
// have been updated (http://goo.gl/gQLz41), but we want *every* update
|
||||
_v: { type: Number, 'default': 0 },
|
||||
achievements: {
|
||||
originalUser: Boolean,
|
||||
helpedHabit: Boolean,
|
||||
ultimateGear: Boolean,
|
||||
beastMaster: Boolean,
|
||||
veteran: Boolean,
|
||||
streak: Number,
|
||||
challenges: Array
|
||||
},
|
||||
auth: {
|
||||
facebook: Schema.Types.Mixed,
|
||||
local: {
|
||||
email: String,
|
||||
hashed_password: String,
|
||||
salt: String,
|
||||
username: String
|
||||
},
|
||||
timestamps: {
|
||||
created: {type: Date,'default': Date.now},
|
||||
loggedin: {type: Date,'default': Date.now}
|
||||
}
|
||||
},
|
||||
|
||||
backer: {
|
||||
tier: Number,
|
||||
//admin: Boolean, // FIXME migrate to contributor.admin
|
||||
npc: String,
|
||||
//contributor: String, // FIXME migrate to contributor.text
|
||||
tokensApplied: Boolean
|
||||
},
|
||||
|
||||
contributor: {
|
||||
level: Number, // 1-7, see https://trello.com/c/wkFzONhE/277-contributor-gear
|
||||
admin: Boolean,
|
||||
text: String, // Artisan, Friend, Blacksmith, etc
|
||||
},
|
||||
|
||||
balance: Number,
|
||||
filters: {type: Schema.Types.Mixed, 'default': {}},
|
||||
|
||||
purchased: {
|
||||
ads: {type: Boolean, 'default': false},
|
||||
skin: {type: Schema.Types.Mixed, 'default': {}}, // eg, {skeleton: true, pumpkin: true, eb052b: true}
|
||||
hair: {type: Schema.Types.Mixed, 'default': {}}
|
||||
},
|
||||
|
||||
flags: {
|
||||
customizationsNotification: {type: Boolean, 'default': false},
|
||||
showTour: {type: Boolean, 'default': true},
|
||||
dropsEnabled: {type: Boolean, 'default': false},
|
||||
itemsEnabled: {type: Boolean, 'default': false},
|
||||
newStuff: {type: Boolean, 'default': false},
|
||||
rewrite: {type: Boolean, 'default': true},
|
||||
partyEnabled: Boolean, // FIXME do we need this?
|
||||
petsEnabled: {type: Boolean, 'default': false},
|
||||
rest: {type: Boolean, 'default': false}, // fixme - change to preferences.resting once we're off derby
|
||||
contributor: Boolean
|
||||
},
|
||||
history: {
|
||||
exp: Array, // [{date: Date, value: Number}], // big peformance issues if these are defined
|
||||
todos: Array //[{data: Date, value: Number}] // big peformance issues if these are defined
|
||||
},
|
||||
|
||||
/* FIXME remove?*/
|
||||
invitations: {
|
||||
guilds: {type: Array, 'default': []},
|
||||
party: Schema.Types.Mixed
|
||||
},
|
||||
items: {
|
||||
armor: Number,
|
||||
weapon: Number,
|
||||
head: Number,
|
||||
shield: Number,
|
||||
|
||||
/*FIXME - tidy this up, not the best way to store current pet*/
|
||||
|
||||
currentPet: {
|
||||
/*Cactus*/
|
||||
|
||||
text: String,
|
||||
/*Cactus*/
|
||||
|
||||
name: String,
|
||||
/*3*/
|
||||
|
||||
value: Number,
|
||||
/*"Find a hatching potion to pour on this egg, and one day it will hatch into a loyal pet.",*/
|
||||
|
||||
notes: String,
|
||||
/*Skeleton*/
|
||||
|
||||
modifier: String,
|
||||
/*Cactus-Skeleton*/
|
||||
|
||||
str: String
|
||||
},
|
||||
|
||||
eggs: [
|
||||
{
|
||||
// example: You've found a Wolf Egg! Find a hatching potion to pour on this egg, and one day it will hatch into a loyal pet
|
||||
dialog: String,
|
||||
// example: Wolf
|
||||
name: String,
|
||||
// example: Find a hatching potion to pour on this egg, and one day it will hatch into a loyal pet.
|
||||
notes: String,
|
||||
// example: Wolf
|
||||
text: String,
|
||||
/* type: String, //Egg // this is forcing mongoose to return object as "[object Object]", but I don't think this is needed anyway? */
|
||||
// example: 3
|
||||
value: Number
|
||||
}
|
||||
],
|
||||
hatchingPotions: Array, // ["Base", "Skeleton",...]
|
||||
lastDrop: {
|
||||
date: {type: Date, 'default': Date.now},
|
||||
count: {type: Number, 'default': 0}
|
||||
},
|
||||
// ["BearCub-Base", "Cactus-Base", ...]
|
||||
|
||||
pets: Array
|
||||
},
|
||||
|
||||
lastCron: {
|
||||
type: Date,
|
||||
'default': Date.now
|
||||
},
|
||||
|
||||
// FIXME remove?
|
||||
party: {
|
||||
//party._id // FIXME make these populate docs?
|
||||
current: String, // party._id
|
||||
invitation: String, // party._id
|
||||
lastMessageSeen: String,
|
||||
leader: Boolean
|
||||
},
|
||||
preferences: {
|
||||
armorSet: String,
|
||||
dayStart: {type:Number, 'default': 0},
|
||||
gender: {type:String, 'default': 'm'},
|
||||
hair: {type:String, 'default':'blond'},
|
||||
hideHeader: {type:Boolean, 'default':false},
|
||||
showHelm: {type:Boolean, 'default':true},
|
||||
skin: {type:String, 'default':'white'},
|
||||
timezoneOffset: Number
|
||||
},
|
||||
profile: {
|
||||
blurb: String,
|
||||
imageUrl: String,
|
||||
name: String,
|
||||
websites: Array // styled like --> ["http://ocdevel.com" ]
|
||||
},
|
||||
stats: {
|
||||
hp: Number,
|
||||
exp: Number,
|
||||
gp: Number,
|
||||
lvl: Number
|
||||
},
|
||||
tags: [
|
||||
{
|
||||
id: String,
|
||||
name: String,
|
||||
challenge: String
|
||||
}
|
||||
],
|
||||
|
||||
challenges: [{type: 'String', ref:'Challenge'}],
|
||||
|
||||
habits: [TaskSchema],
|
||||
dailys: [TaskSchema],
|
||||
todos: [TaskSchema],
|
||||
rewards: [TaskSchema],
|
||||
|
||||
}, {
|
||||
strict: true,
|
||||
minimize: false // So empty objects are returned
|
||||
});
|
||||
|
||||
UserSchema.methods.deleteTask = function(tid) {
|
||||
//user[t.type+'s'].id(t.id).remove();
|
||||
var task = this.tasks[tid];
|
||||
var i = this[task.type+'s'].indexOf(task);
|
||||
if (~i) this[task.type+'s'].splice(i,1);
|
||||
}
|
||||
|
||||
UserSchema.methods.toJSON = function() {
|
||||
var doc = this.toObject();
|
||||
doc.id = doc._id;
|
||||
|
||||
// FIXME? Is this a reference to `doc.filters` or just disabled code? Remove?
|
||||
doc.filters = {};
|
||||
doc._tmp = this._tmp; // be sure to send down drop notifs
|
||||
|
||||
return doc;
|
||||
};
|
||||
|
||||
UserSchema.virtual('tasks').get(function () {
|
||||
var tasks = this.habits.concat(this.dailys).concat(this.todos).concat(this.rewards);
|
||||
var tasks = _.object(_.pluck(tasks,'id'), tasks);
|
||||
return tasks;
|
||||
});
|
||||
|
||||
// FIXME - since we're using special @post('init') above, we need to flag when the original path was modified.
|
||||
// Custom setter/getter virtuals?
|
||||
|
||||
UserSchema.pre('save', function(next) {
|
||||
//this.markModified('tasks');
|
||||
|
||||
if (!this.profile.name) {
|
||||
var fb = this.auth.facebook;
|
||||
this.profile.name =
|
||||
(this.auth.local && this.auth.local.username) ||
|
||||
(fb && (fb.displayName || fb.name || fb.username || (fb.first_name && fb.first_name + ' ' + fb.last_name))) ||
|
||||
'Anonymous';
|
||||
}
|
||||
|
||||
if(!this.achievements.beastMaster && this.items.pets.length >= 90){
|
||||
this.achievements.beastMaster = true;
|
||||
}
|
||||
|
||||
//our own version incrementer
|
||||
this._v++;
|
||||
next();
|
||||
});
|
||||
|
||||
UserSchema.methods.syncScoreToChallenge = function(task, delta){
|
||||
if (!task.challenge || !task.challenge.id || task.challenge.broken) return;
|
||||
if (task.type == 'reward') return; // we don't want to update the reward GP cost
|
||||
var self = this;
|
||||
Challenge.findById(task.challenge.id, function(err, chal){
|
||||
if (err) throw err;
|
||||
var t = chal.tasks[task.id];
|
||||
if (!t) return chal.syncToUser(self); // this task was removed from the challenge, notify user
|
||||
t.value += delta;
|
||||
t.history.push({value: t.value, date: +new Date});
|
||||
chal.save();
|
||||
});
|
||||
}
|
||||
|
||||
UserSchema.methods.unlink = function(options, cb) {
|
||||
var cid = options.cid, keep = options.keep, tid = options.tid;
|
||||
var self = this;
|
||||
switch (keep) {
|
||||
case 'keep':
|
||||
self.tasks[tid].challenge = {};
|
||||
break;
|
||||
case 'remove':
|
||||
self.deleteTask(tid);
|
||||
break;
|
||||
case 'keep-all':
|
||||
_.each(self.tasks, function(t){
|
||||
if (t.challenge && t.challenge.id == cid) {
|
||||
t.challenge = {};
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'remove-all':
|
||||
_.each(self.tasks, function(t){
|
||||
if (t.challenge && t.challenge.id == cid) {
|
||||
self.deleteTask(t.id);
|
||||
}
|
||||
})
|
||||
break;
|
||||
}
|
||||
self.markModified('habits');
|
||||
self.markModified('dailys');
|
||||
self.markModified('todos');
|
||||
self.markModified('rewards');
|
||||
self.save(cb);
|
||||
}
|
||||
|
||||
module.exports.schema = UserSchema;
|
||||
module.exports.model = mongoose.model("User", UserSchema);
|
||||
@@ -0,0 +1,102 @@
|
||||
var express = require('express');
|
||||
var router = new express.Router();
|
||||
var user = require('../controllers/user');
|
||||
var groups = require('../controllers/groups');
|
||||
var auth = require('../controllers/auth');
|
||||
var challenges = require('../controllers/challenges');
|
||||
var nconf = require('nconf');
|
||||
|
||||
/*
|
||||
---------- /api/v1 API ------------
|
||||
Every url added to router is prefaced by /api/v1
|
||||
See ./routes/coffee for routes
|
||||
|
||||
v1 user. Requires x-api-user (user id) and x-api-key (api key) headers, Test with:
|
||||
$ cd node_modules/racer && npm install && cd ../..
|
||||
$ mocha test/user.mocha.coffee
|
||||
*/
|
||||
|
||||
var verifyTaskExists = user.verifyTaskExists
|
||||
var cron = user.cron;
|
||||
|
||||
router.get('/status', function(req, res) {
|
||||
return res.json({
|
||||
status: 'up'
|
||||
});
|
||||
});
|
||||
|
||||
/* Scoring*/
|
||||
router.post('/user/task/:id/:direction', auth.auth, cron, user.scoreTask);
|
||||
router.post('/user/tasks/:id/:direction', auth.auth, cron, user.scoreTask);
|
||||
|
||||
/* Tasks*/
|
||||
router.get('/user/tasks', auth.auth, cron, user.getTasks);
|
||||
router.get('/user/task/:id', auth.auth, cron, user.getTask);
|
||||
router.put('/user/task/:id', auth.auth, cron, verifyTaskExists, user.updateTask);
|
||||
router["delete"]('/user/task/:id', auth.auth, cron, verifyTaskExists, user.deleteTask);
|
||||
router.post('/user/task', auth.auth, cron, user.createTask);
|
||||
router.put('/user/task/:id/sort', auth.auth, cron, verifyTaskExists, user.sortTask);
|
||||
router.post('/user/clear-completed', auth.auth, cron, user.clearCompleted);
|
||||
router.post('/user/task/:id/unlink', auth.auth, challenges.unlink); // removing cron since they may want to remove task first
|
||||
if (nconf.get('NODE_ENV') == 'development') {
|
||||
router.post('/user/addTenGems', auth.auth, user.addTenGems);
|
||||
}
|
||||
|
||||
/* Items*/
|
||||
router.post('/user/buy/:type', auth.auth, cron, user.buy);
|
||||
|
||||
/* User*/
|
||||
router.get('/user', auth.auth, cron, user.getUser);
|
||||
router.put('/user', auth.auth, cron, user.updateUser);
|
||||
router.post('/user/revive', auth.auth, cron, user.revive);
|
||||
router.post('/user/batch-update', auth.auth, cron, user.batchUpdate);
|
||||
router.post('/user/reroll', auth.auth, cron, user.reroll);
|
||||
router.post('/user/buy-gems', auth.auth, user.buyGems);
|
||||
router.post('/user/buy-gems/paypal-ipn', user.buyGemsPaypalIPN);
|
||||
router.post('/user/unlock', auth.auth, cron, user.unlock);
|
||||
router.post('/user/reset', auth.auth, user.reset);
|
||||
router['delete']('/user', auth.auth, user['delete']);
|
||||
|
||||
/* Tags */
|
||||
router['delete']('/user/tags/:tid', auth.auth, user.deleteTag);
|
||||
|
||||
/* Groups*/
|
||||
router.get('/groups', auth.auth, groups.list);
|
||||
router.post('/groups', auth.auth, groups.create);
|
||||
router.get('/groups/:gid', auth.auth, groups.get);
|
||||
router.post('/groups/:gid', auth.auth, groups.attachGroup, groups.update);
|
||||
router.put('/groups/:gid', auth.auth, groups.attachGroup, groups.update);
|
||||
//DELETE /groups/:gid
|
||||
|
||||
router.post('/groups/:gid/join', auth.auth, groups.attachGroup, groups.join);
|
||||
router.post('/groups/:gid/leave', auth.auth, groups.attachGroup, groups.leave);
|
||||
router.post('/groups/:gid/invite', auth.auth, groups.attachGroup, groups.invite);
|
||||
router.post('/groups/:gid/removeMember', auth.auth, groups.attachGroup, groups.removeMember);
|
||||
|
||||
//GET /groups/:gid/chat
|
||||
router.post('/groups/:gid/chat', auth.auth, groups.attachGroup, groups.postChat);
|
||||
router["delete"]('/groups/:gid/chat/:messageId', auth.auth, groups.attachGroup, groups.deleteChatMessage);
|
||||
//PUT /groups/:gid/chat/:messageId
|
||||
|
||||
/* Members */
|
||||
router.get('/members/:uid', groups.getMember);
|
||||
router.post('/members/:uid', auth.auth, groups.updateMember); // only for admins
|
||||
|
||||
// Market
|
||||
router.post('/market/buy', auth.auth, user.marketBuy);
|
||||
|
||||
/* Challenges */
|
||||
// Note: while challenges belong to groups, and would therefore make sense as a nested resource
|
||||
// (eg /groups/:gid/challenges/:cid), they will also be referenced by users from the "challenges" tab
|
||||
// without knowing which group they belong to. So to prevent unecessary lookups, we have them as a top-level resource
|
||||
router.get('/challenges', auth.auth, challenges.list)
|
||||
router.post('/challenges', auth.auth, challenges.create)
|
||||
router.get('/challenges/:cid', auth.auth, challenges.get)
|
||||
router.post('/challenges/:cid', auth.auth, challenges.update)
|
||||
router['delete']('/challenges/:cid', auth.auth, challenges['delete'])
|
||||
router.post('/challenges/:cid/close', auth.auth, challenges.selectWinner)
|
||||
router.post('/challenges/:cid/join', auth.auth, challenges.join)
|
||||
router.post('/challenges/:cid/leave', auth.auth, challenges.leave)
|
||||
router.get('/challenges/:cid/member/:uid', auth.auth, challenges.getMember)
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,13 @@
|
||||
var auth = require('../controllers/auth');
|
||||
var express = require('express');
|
||||
var router = new express.Router();
|
||||
|
||||
/* auth.auth*/
|
||||
auth.setupPassport(router); //FIXME make this consistent with the others
|
||||
router.post('/api/v1/register', auth.registerUser);
|
||||
router.post('/api/v1/user/auth/local', auth.loginLocal);
|
||||
router.post('/api/v1/user/auth/facebook', auth.loginFacebook);
|
||||
router.post('/api/v1/user/reset-password', auth.resetPassword);
|
||||
router.post('/api/v1/user/change-password', auth.auth, auth.changePassword);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,49 @@
|
||||
var nconf = require('nconf');
|
||||
var express = require('express');
|
||||
var router = new express.Router();
|
||||
var _ = require('lodash');
|
||||
|
||||
// -------- App --------
|
||||
router.get('/', function(req, res) {
|
||||
return res.render('index', {
|
||||
title: 'HabitRPG | Your Life, The Role Playing Game',
|
||||
env: res.locals.habitrpg
|
||||
});
|
||||
});
|
||||
|
||||
// -------- Marketing --------
|
||||
|
||||
router.get('/splash.html', function(req, res) {
|
||||
res.redirect('/static/front');
|
||||
});
|
||||
|
||||
router.get('/static/front', function(req, res) {
|
||||
res.render('static/front', {env: res.locals.habitrpg, isFrontPage: true});
|
||||
});
|
||||
|
||||
router.get('/static/about', function(req, res) {
|
||||
res.redirect('http://community.habitrpg.com/node/97');
|
||||
});
|
||||
|
||||
router.get('/static/team', function(req, res) {
|
||||
res.redirect('http://community.habitrpg.com/node/96');
|
||||
});
|
||||
|
||||
router.get('/static/extensions', function(req, res) {
|
||||
res.redirect('http://community.habitrpg.com/extensions');
|
||||
});
|
||||
|
||||
router.get('/static/faq', function(req, res) {
|
||||
res.redirect('http://community.habitrpg.com/faq-page');
|
||||
});
|
||||
|
||||
router.get('/static/privacy', function(req, res) {
|
||||
res.render('static/privacy', {env: res.locals.habitrpg});
|
||||
});
|
||||
|
||||
router.get('/static/terms', function(req, res) {
|
||||
res.render('static/terms', {env: res.locals.habitrpg});
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
require('coffee-script') // for habitrpg-shared
|
||||
var nconf = require('nconf');
|
||||
require('./config');
|
||||
var async = require('async');
|
||||
var mongoose = require('mongoose');
|
||||
User = require('./models/user').model;
|
||||
Group = require('./models/group').model;
|
||||
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
mongoose.connect(nconf.get('NODE_DB_URI'), cb);
|
||||
},
|
||||
function(cb){
|
||||
Group.findById('habitrpg', cb);
|
||||
},
|
||||
function(tavern, cb){
|
||||
console.log({tavern:tavern,cb:cb});
|
||||
if (!tavern) {
|
||||
tavern = new Group({
|
||||
_id: 'habitrpg',
|
||||
chat: [],
|
||||
leader: '9',
|
||||
name: 'HabitRPG',
|
||||
type: 'guild'
|
||||
});
|
||||
tavern.save(cb)
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
],function(err){
|
||||
if (err) throw err;
|
||||
console.log("Done initializing database");
|
||||
})
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
require('coffee-script') // remove this once we've fully converted over
|
||||
|
||||
var express = require("express");
|
||||
var http = require("http");
|
||||
var path = require("path");
|
||||
var app = express();
|
||||
var nconf = require('nconf');
|
||||
var utils = require('./utils');
|
||||
var middleware = require('./middleware');
|
||||
var server;
|
||||
var TWO_WEEKS = 1000 * 60 * 60 * 24 * 14;
|
||||
|
||||
// ------------ Setup configurations ------------
|
||||
require('./config');
|
||||
process.on("uncaughtException", function(error) {
|
||||
// when we hit an error, send it to admin as an email. If no ADMIN_EMAIL is present, just send it to yourself (SMTP_USER)
|
||||
utils.sendEmail({
|
||||
from: "HabitRPG <" + nconf.get('SMTP_USER') + ">",
|
||||
to: nconf.get('ADMIN_EMAIL') || nconf.get('SMTP_USER'),
|
||||
subject: "HabitRPG Error",
|
||||
text: error.stack
|
||||
});
|
||||
console.error(error.stack);
|
||||
});
|
||||
|
||||
// ------------ MongoDB Configuration ------------
|
||||
mongoose = require('mongoose');
|
||||
require('./models/user'); //load up the user schema - TODO is this necessary?
|
||||
require('./models/group');
|
||||
require('./models/challenge');
|
||||
mongoose.connect(nconf.get('NODE_DB_URI'), function(err) {
|
||||
if (err) throw err;
|
||||
console.info('Connected with Mongoose');
|
||||
});
|
||||
|
||||
|
||||
// ------------ Passport Configuration ------------
|
||||
var passport = require('passport')
|
||||
var util = require('util')
|
||||
var FacebookStrategy = require('passport-facebook').Strategy;
|
||||
// Passport session setup.
|
||||
// To support persistent login sessions, Passport needs to be able to
|
||||
// serialize users into and deserialize users out of the session. Typically,
|
||||
// this will be as simple as storing the user ID when serializing, and finding
|
||||
// the user by ID when deserializing. However, since this example does not
|
||||
// have a database of user records, the complete Facebook profile is serialized
|
||||
// and deserialized.
|
||||
passport.serializeUser(function(user, done) {
|
||||
done(null, user);
|
||||
});
|
||||
|
||||
passport.deserializeUser(function(obj, done) {
|
||||
done(null, obj);
|
||||
});
|
||||
|
||||
|
||||
// Use the FacebookStrategy within Passport.
|
||||
// Strategies in Passport require a `verify` function, which accept
|
||||
// credentials (in this case, an accessToken, refreshToken, and Facebook
|
||||
// profile), and invoke a callback with a user object.
|
||||
passport.use(new FacebookStrategy({
|
||||
clientID: nconf.get("FACEBOOK_KEY"),
|
||||
clientSecret: nconf.get("FACEBOOK_SECRET"),
|
||||
callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback"
|
||||
},
|
||||
function(accessToken, refreshToken, profile, done) {
|
||||
// asynchronous verification, for effect...
|
||||
//process.nextTick(function () {
|
||||
|
||||
// To keep the example simple, the user's Facebook profile is returned to
|
||||
// represent the logged-in user. In a typical application, you would want
|
||||
// to associate the Facebook account with a user record in your database,
|
||||
// and return that user instead.
|
||||
return done(null, profile);
|
||||
//});
|
||||
}
|
||||
));
|
||||
|
||||
// ------------ Server Configuration ------------
|
||||
app.set("port", nconf.get('PORT'));
|
||||
|
||||
if (!process.env.SUPPRESS) app.use(express.logger("dev"));
|
||||
app.use(express.compress());
|
||||
app.set("views", __dirname + "/../views");
|
||||
app.set("view engine", "jade");
|
||||
app.use(express.favicon());
|
||||
app.use(middleware.cors);
|
||||
app.use(middleware.forceSSL);
|
||||
app.use(express.urlencoded());
|
||||
app.use(express.json());
|
||||
app.use(express.methodOverride());
|
||||
//app.use(express.cookieParser(nconf.get('SESSION_SECRET')));
|
||||
app.use(express.cookieParser());
|
||||
app.use(express.cookieSession({ secret: nconf.get('SESSION_SECRET'), httpOnly: false, cookie: { maxAge: TWO_WEEKS }}));
|
||||
//app.use(express.session());
|
||||
app.use(middleware.splash);
|
||||
app.use(middleware.locals);
|
||||
|
||||
// Initialize Passport! Also use passport.session() middleware, to support
|
||||
// persistent login sessions (recommended).
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
app.use(app.router);
|
||||
|
||||
var maxAge = (nconf.get('NODE_ENV') === 'production') ? 31536000000 : 0;
|
||||
app.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge }));
|
||||
app.use(express['static'](path.join(__dirname, "/../public")));
|
||||
|
||||
// development only
|
||||
if ("development" === app.get("env")) {
|
||||
app.use(express.errorHandler());
|
||||
}
|
||||
|
||||
// Custom Directives
|
||||
app.use(require('./routes/pages').middleware);
|
||||
app.use(require('./routes/auth').middleware);
|
||||
app.use('/api/v1', require('./routes/api').middleware);
|
||||
app.use(require('./controllers/deprecated').middleware);
|
||||
server = http.createServer(app).listen(app.get("port"), function() {
|
||||
return console.log("Express server listening on port " + app.get("port"));
|
||||
});
|
||||
|
||||
module.exports = server;
|
||||
|
||||
/*
|
||||
#ONE_YEAR = 1000 * 60 * 60 * 24 * 365
|
||||
#root = path.dirname path.dirname __dirname
|
||||
#publicPath = path.join root, 'public'
|
||||
#
|
||||
#
|
||||
#expressApp
|
||||
# .use(express.favicon("#{publicPath}/favicon.ico"))
|
||||
# # Gzip static files and serve from memory
|
||||
# .use(gzippo.staticGzip(publicPath, maxAge: ONE_YEAR))
|
||||
# # Gzip dynamically rendered content
|
||||
# .use(express.compress())
|
||||
# .use(middleware.translate)
|
||||
# .use(auth.middleware(strategies, options))
|
||||
# .use(serverError(root))
|
||||
#
|
||||
#
|
||||
## Errors
|
||||
#expressApp.all '*', (req) ->
|
||||
# throw "404: #{req.url}"
|
||||
*/
|
||||
@@ -1,271 +0,0 @@
|
||||
express = require 'express'
|
||||
router = new express.Router()
|
||||
|
||||
scoring = require '../app/scoring'
|
||||
_ = require 'underscore'
|
||||
{ tnl } = require '../app/algos'
|
||||
validator = require 'derby-auth/node_modules/validator'
|
||||
check = validator.check
|
||||
sanitize = validator.sanitize
|
||||
|
||||
NO_TOKEN_OR_UID = err: "You must include a token and uid (user id) in your request"
|
||||
NO_USER_FOUND = err: "No user found."
|
||||
|
||||
# ---------- /api/v1 API ------------
|
||||
# Every url added beneath router is prefaced by /api/v1
|
||||
|
||||
###
|
||||
v1 API. Requires api-v1-user (user id) and api-v1-key (api key) headers, Test with:
|
||||
$ cd node_modules/racer && npm install && cd ../..
|
||||
$ mocha test/api.mocha.coffee
|
||||
###
|
||||
|
||||
###
|
||||
API Status
|
||||
###
|
||||
router.get '/status', (req, res) ->
|
||||
res.json status: 'up'
|
||||
|
||||
###
|
||||
beforeEach auth interceptor
|
||||
###
|
||||
auth = (req, res, next) ->
|
||||
uid = req.headers['x-api-user']
|
||||
token = req.headers['x-api-key']
|
||||
return res.json 401, NO_TOKEN_OR_UID unless uid || token
|
||||
|
||||
model = req.getModel()
|
||||
query = model.query('users').withIdAndToken(uid, token)
|
||||
|
||||
query.fetch (err, user) ->
|
||||
return res.json err: err if err
|
||||
req.user = user
|
||||
req.userObj = user.get()
|
||||
return res.json 401, NO_USER_FOUND if !req.userObj || _.isEmpty(req.userObj)
|
||||
req._isServer = true
|
||||
next()
|
||||
|
||||
###
|
||||
GET /user
|
||||
###
|
||||
router.get '/user', auth, (req, res) ->
|
||||
user = req.userObj
|
||||
|
||||
user.stats.toNextLevel = tnl user.stats.lvl
|
||||
user.stats.maxHealth = 50
|
||||
|
||||
delete user.apiToken
|
||||
if user.auth
|
||||
delete user.auth.hashed_password
|
||||
delete user.auth.salt
|
||||
|
||||
res.json user
|
||||
|
||||
###
|
||||
TODO POST /user
|
||||
when a put attempt didn't work, create a new one with POST
|
||||
###
|
||||
|
||||
###
|
||||
PUT /user
|
||||
###
|
||||
router.put '/user', auth, (req, res) ->
|
||||
user = req.user
|
||||
partialUser = req.body.user
|
||||
|
||||
# REVISIT is this the best way of handling protected v acceptable attr mass-setting? Possible pitfalls: (1) we have to remember
|
||||
# to update here when we add new schema attrs in the future, (2) developers can't assign random variables (which
|
||||
# is currently beneficial for Kevin & Paul). Pros: protects accidental or malicious user data corruption
|
||||
|
||||
# TODO - this accounts for single-nested items (stats.hp, stats.exp) but will clobber any other depth.
|
||||
# See http://stackoverflow.com/a/6394168/362790 for when we need to cross that road
|
||||
|
||||
acceptableAttrs = ['flags', 'history', 'items', 'preferences', 'profile', 'stats']
|
||||
user.set 'lastCron', partialUser.lastCron if partialUser.lastCron?
|
||||
_.each acceptableAttrs, (attr) ->
|
||||
_.each partialUser[attr], (val, key) -> user.set("#{attr}.#{key}", val)
|
||||
|
||||
updateTasks partialUser.tasks, req.user, req.getModel() if partialUser.tasks?
|
||||
|
||||
userObj = user.get()
|
||||
userObj.tasks = _.toArray(userObj.tasks) # FIXME figure out how we're going to consistently handle this. should always be array
|
||||
res.json 201, userObj
|
||||
|
||||
###
|
||||
GET /user/task/:id
|
||||
###
|
||||
router.get '/user/task/:id', auth, (req, res) ->
|
||||
task = req.userObj.tasks[req.params.id]
|
||||
return res.json 400, err: "No task found." if !task || _.isEmpty(task)
|
||||
|
||||
res.json 200, task
|
||||
|
||||
###
|
||||
validate task
|
||||
###
|
||||
validateTask = (req, res, next) ->
|
||||
task = {}
|
||||
newTask = { type, text, notes, value, up, down, completed } = req.body
|
||||
|
||||
# If we're updating, get the task from the user
|
||||
if req.method is 'PUT' or req.method is 'DELETE'
|
||||
task = req.userObj?.tasks[req.params.id]
|
||||
return res.json 400, err: "No task found." if !task || _.isEmpty(task)
|
||||
# Strip for now
|
||||
type = undefined
|
||||
delete newTask.type
|
||||
else if req.method is 'POST'
|
||||
newTask.value = sanitize(value).toInt()
|
||||
newTask.value = 0 if isNaN newTask.value
|
||||
unless /^(habit|todo|daily|reward)$/.test type
|
||||
return res.json 400, err: 'type must be habit, todo, daily, or reward'
|
||||
|
||||
newTask.text = sanitize(text).xss() if typeof text is "string"
|
||||
newTask.notes = sanitize(notes).xss() if typeof notes is "string"
|
||||
|
||||
switch type
|
||||
when 'habit'
|
||||
newTask.up = true unless typeof up is 'boolean'
|
||||
newTask.down = true unless typeof down is 'boolean'
|
||||
when 'daily', 'todo'
|
||||
newTask.completed = false unless typeof completed is 'boolean'
|
||||
|
||||
_.extend task, newTask
|
||||
req.task = task
|
||||
next()
|
||||
|
||||
###
|
||||
PUT /user/task/:id
|
||||
###
|
||||
router.put '/user/task/:id', auth, validateTask, (req, res) ->
|
||||
req.user.set "tasks.#{req.task.id}", req.task
|
||||
|
||||
res.json 200, req.task
|
||||
|
||||
###
|
||||
DELETE /user/task/:id
|
||||
###
|
||||
router.delete '/user/task/:id', auth, validateTask, (req, res) ->
|
||||
taskIds = req.user.get "#{req.task.type}Ids"
|
||||
|
||||
req.user.del "tasks.#{req.task.id}"
|
||||
# Remove one id from array of typeIds
|
||||
req.user.remove "#{req.task.type}Ids", taskIds.indexOf(req.task.id), 1
|
||||
|
||||
res.send 204
|
||||
|
||||
###
|
||||
POST /user/tasks
|
||||
###
|
||||
updateTasks = (tasks, user, model) ->
|
||||
for idx, task of tasks
|
||||
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
|
||||
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
|
||||
tasks[idx] = task
|
||||
return tasks
|
||||
|
||||
router.post '/user/tasks', auth, (req, res) ->
|
||||
tasks = updateTasks req.body, req.user, req.getModel()
|
||||
res.json 201, tasks
|
||||
|
||||
|
||||
###
|
||||
POST /user/task/
|
||||
###
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
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")
|
||||
|
||||
res.json 200, tasks
|
||||
|
||||
###
|
||||
This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login
|
||||
###
|
||||
scoreTask = (req, res, next) ->
|
||||
{taskId, direction} = req.params
|
||||
{title, service, icon, type} = req.body
|
||||
type ||= 'habit'
|
||||
|
||||
# Send error responses for improper API call
|
||||
return res.send(500, ':taskId required') unless taskId
|
||||
return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down']
|
||||
|
||||
model = req.getModel()
|
||||
{user, userObj} = req
|
||||
|
||||
model.ref('_user', user)
|
||||
|
||||
existingTask = model.at "_user.tasks.#{taskId}"
|
||||
# TODO add service & icon to task
|
||||
# If task exists, set it's compltion
|
||||
if existingTask.get()
|
||||
# Set completed if type is daily or todo
|
||||
existingTask.set 'completed', (direction is 'up') if /^(daily|todo)$/.test existingTask.get('type')
|
||||
else
|
||||
task =
|
||||
id: taskId
|
||||
type: type
|
||||
text: (title || taskId)
|
||||
value: 0
|
||||
notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task."
|
||||
|
||||
switch type
|
||||
when 'habit'
|
||||
task.up = true
|
||||
task.down = true
|
||||
when 'daily', 'todo'
|
||||
task.completed = direction is 'up'
|
||||
|
||||
model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
|
||||
model.at("_#{type}List").push task
|
||||
|
||||
delta = scoring.score(model, taskId, direction)
|
||||
result = model.get '_user.stats'
|
||||
result.delta = delta
|
||||
res.json result
|
||||
|
||||
###
|
||||
POST /user/tasks/:taskId/:direction
|
||||
###
|
||||
router.post '/user/task/:taskId/:direction', auth, scoreTask
|
||||
router.post '/user/tasks/:taskId/:direction', auth, scoreTask
|
||||
|
||||
module.exports = router
|
||||
module.exports.auth = auth
|
||||
module.exports.scoreTask = scoreTask # export so deprecated can call it
|
||||
@@ -1,52 +0,0 @@
|
||||
express = require 'express'
|
||||
router = new express.Router()
|
||||
|
||||
scoring = require '../app/scoring'
|
||||
_ = require 'underscore'
|
||||
icalendar = require('icalendar')
|
||||
api = require './api'
|
||||
|
||||
# ---------- Deprecated Paths ------------
|
||||
|
||||
deprecatedMessage = 'This API is no longer supported, see https://github.com/lefnire/habitrpg/wiki/API for new protocol'
|
||||
|
||||
router.get '/:uid/up/:score?', (req, res) -> res.send(500, deprecatedMessage)
|
||||
router.get '/:uid/down/:score?', (req, res) -> res.send(500, deprecatedMessage)
|
||||
router.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> res.send(500, deprecatedMessage)
|
||||
|
||||
# Redirect to new API
|
||||
initDeprecated = (req, res, next) ->
|
||||
req.headers['x-api-user'] = req.params.uid
|
||||
req.headers['x-api-key'] = req.body.apiToken
|
||||
next()
|
||||
|
||||
router.post '/v1/users/:uid/tasks/:taskId/:direction', initDeprecated, api.auth, api.scoreTask
|
||||
|
||||
router.get '/v1/users/:uid/calendar.ics', (req, res) ->
|
||||
#return next() #disable for now
|
||||
{uid} = req.params
|
||||
{apiToken} = req.query
|
||||
|
||||
model = req.getModel()
|
||||
query = model.query('users').withIdAndToken(uid, apiToken)
|
||||
query.fetch (err, result) ->
|
||||
return res.send(500, err) if err
|
||||
tasks = result.get('tasks')
|
||||
# tasks = result[0].tasks
|
||||
tasksWithDates = _.filter tasks, (task) -> !!task.date
|
||||
return res.send(500, "No events found") if _.isEmpty(tasksWithDates)
|
||||
|
||||
ical = new icalendar.iCalendar()
|
||||
ical.addProperty('NAME', 'HabitRPG')
|
||||
_.each tasksWithDates, (task) ->
|
||||
event = new icalendar.VEvent(task.id);
|
||||
event.setSummary(task.text);
|
||||
d = new Date(task.date)
|
||||
d.date_only = true
|
||||
event.setDate d
|
||||
ical.addComponent event
|
||||
res.type('text/calendar')
|
||||
formattedIcal = ical.toString().replace(/DTSTART\:/g, 'DTSTART;VALUE=DATE:')
|
||||
res.send(200, formattedIcal)
|
||||
|
||||
module.exports = router
|
||||
@@ -1,70 +0,0 @@
|
||||
_ = require 'underscore'
|
||||
character = require "../app/character"
|
||||
|
||||
module.exports.middleware = (req, res, next) ->
|
||||
model = req.getModel()
|
||||
model.set '_stripePubKey', process.env.STRIPE_PUB_KEY
|
||||
return next()
|
||||
|
||||
module.exports.app = (appExports, model) ->
|
||||
|
||||
appExports.showStripe = (e, el) ->
|
||||
token = (res) ->
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/charge",
|
||||
data: res
|
||||
}).success ->
|
||||
window.location.href = "/"
|
||||
.error (err) ->
|
||||
alert err.responseText
|
||||
|
||||
disableAds = if (model.get('_user.flags.ads') is 'hide') then '' else 'Disable Ads, '
|
||||
|
||||
StripeCheckout.open
|
||||
key: model.get('_stripePubKey')
|
||||
address: false
|
||||
amount: 500
|
||||
name: "Checkout"
|
||||
description: "Buy 20 Gems, #{disableAds}Support the Developers"
|
||||
panelLabel: "Checkout"
|
||||
token: token
|
||||
|
||||
###
|
||||
Buy Reroll Button
|
||||
###
|
||||
appExports.buyReroll = (e, el, next) ->
|
||||
batch = new character.BatchUpdate(model)
|
||||
obj = model.get('_user')
|
||||
batch.set 'balance', obj.balance-1
|
||||
_.each obj.tasks, (task) -> batch.set("tasks.#{task.id}.value", 0) unless task.type == 'reward'
|
||||
batch.commit()
|
||||
|
||||
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.get('_userId') #or model.session.userId # see http://goo.gl/TPYIt
|
||||
req._isServer = true
|
||||
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
|
||||
@@ -1,21 +0,0 @@
|
||||
derby = require 'derby'
|
||||
{isProduction} = derby.util
|
||||
|
||||
module.exports = (root) ->
|
||||
staticPages = derby.createStatic root
|
||||
|
||||
return (err, req, res, next) ->
|
||||
return next() unless err?
|
||||
|
||||
console.log(if err.stack then err.stack else err)
|
||||
|
||||
## Customize error handling here ##
|
||||
message = err.message || err.toString()
|
||||
status = parseInt message
|
||||
if status is 404
|
||||
staticPages.render '404', res, {url: req.url}, 404
|
||||
else if status >= 400 and status < 600
|
||||
res.send status
|
||||
else
|
||||
#TODO send error to email
|
||||
res.redirect('/500.html')
|
||||
@@ -1,24 +0,0 @@
|
||||
express = require 'express'
|
||||
router = new express.Router()
|
||||
|
||||
path = require 'path'
|
||||
derby = require 'derby'
|
||||
|
||||
# ---------- Static Pages ------------
|
||||
staticPages = derby.createStatic path.dirname(path.dirname(__dirname))
|
||||
|
||||
beforeEach = (req, res, next) ->
|
||||
req.getModel().set '_nodeEnv', 'production' # we don't want cheat buttons on static pages
|
||||
next()
|
||||
|
||||
router.get '/splash.html', (req, res) -> res.redirect('/static/front')
|
||||
router.get '/static/front', beforeEach, (req, res) -> staticPages.render 'static/front', res
|
||||
router.get '/static/about', (req, res) -> res.redirect 'http://community.habitrpg.com/node/97'
|
||||
router.get '/static/team', (req, res) -> res.redirect 'http://community.habitrpg.com/node/96'
|
||||
router.get '/static/extensions', (req, res) -> res.redirect 'http://community.habitrpg.com/extensions'
|
||||
router.get '/static/faq', (req, res) -> res.redirect 'http://community.habitrpg.com/faq-page'
|
||||
|
||||
router.get '/static/privacy', beforeEach, (req, res) -> staticPages.render 'static/privacy', res
|
||||
router.get '/static/terms', beforeEach, (req, res) -> staticPages.render 'static/terms', res
|
||||
|
||||
module.exports = router
|
||||
@@ -1,146 +0,0 @@
|
||||
derbyAuth = require('derby-auth/store')
|
||||
|
||||
###
|
||||
Setup read / write access
|
||||
@param store
|
||||
###
|
||||
|
||||
module.exports.customAccessControl = (store) ->
|
||||
userAccess(store)
|
||||
partySystem(store)
|
||||
tavernSystem(store)
|
||||
REST(store)
|
||||
|
||||
###
|
||||
General user access
|
||||
###
|
||||
userAccess = (store) ->
|
||||
|
||||
store.readPathAccess "users.*", -> # captures, accept, err ->
|
||||
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 = arguments[arguments.length - 2]
|
||||
uid = arguments[0]
|
||||
accept (uid is @session.userId) or derbyAuth.isServer(@)
|
||||
|
||||
store.writeAccess "*", "users.*", -> # captures, value, accept, err ->
|
||||
accept = arguments[arguments.length-2]
|
||||
err = arguments[arguments.length - 1]
|
||||
# return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@)
|
||||
|
||||
return accept(true) if derbyAuth.isServer(@)
|
||||
|
||||
return accept(false) if derbyAuth.bustedSession(@)
|
||||
|
||||
captures = arguments[0].split('.')
|
||||
uid = captures.shift()
|
||||
attrPath = captures.join('.') # new array shifted left, after shift() was run
|
||||
|
||||
if attrPath is 'backer'
|
||||
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'
|
||||
return accept(true)
|
||||
|
||||
# Same session (user.id = this.session.userId)
|
||||
return accept(true) if uid is @session.userId
|
||||
|
||||
accept(false)
|
||||
|
||||
store.writeAccess "*", "users.*.balance", (id, newBalance, accept, err) ->
|
||||
# return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@)
|
||||
return accept(false) if derbyAuth.bustedSession(@)
|
||||
|
||||
oldBalance = @session.req?._racerModel?.get("users.#{id}.balance") || 0
|
||||
purchasingSomethingOnClient = newBalance < oldBalance
|
||||
accept(purchasingSomethingOnClient or derbyAuth.isServer(@))
|
||||
|
||||
store.writeAccess "*", "users.*.flags.ads", -> # captures, value, accept, err ->
|
||||
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(derbyAuth.isServer(@))
|
||||
|
||||
|
||||
###
|
||||
REST
|
||||
Get user with API token
|
||||
###
|
||||
REST = (store) ->
|
||||
store.query.expose "users", "withIdAndToken", (uid, token) ->
|
||||
@where("id").equals(uid)
|
||||
.where('apiToken').equals(token)
|
||||
.findOne()
|
||||
|
||||
store.queryAccess "users", "withIdAndToken", (uid, token, accept, err) ->
|
||||
return accept(true) if uid && token
|
||||
accept(false) # only user has id & token
|
||||
|
||||
|
||||
###
|
||||
Party permissions
|
||||
###
|
||||
partySystem = (store) ->
|
||||
store.query.expose "users", "party", (ids) ->
|
||||
@where("id").within(ids)
|
||||
.only('stats',
|
||||
'items',
|
||||
'party',
|
||||
'profile',
|
||||
'achievements',
|
||||
'backer',
|
||||
'preferences',
|
||||
'auth.local.username',
|
||||
'auth.facebook.displayName')
|
||||
|
||||
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
|
||||
|
||||
store.query.expose "parties", "withId", (id) ->
|
||||
@where("id").equals(id).findOne()
|
||||
|
||||
store.queryAccess "parties", "withId", (id, accept, err) ->
|
||||
# return err(derbyAuth.SESSION_INVALIDATED_ERROR) if derbyAuth.bustedSession(@)
|
||||
return accept(false) if derbyAuth.bustedSession(@)
|
||||
accept(true)
|
||||
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
var nodemailer = require('nodemailer');
|
||||
var nconf = require('nconf');
|
||||
var crypto = require('crypto');
|
||||
|
||||
module.exports.sendEmail = function(mailData) {
|
||||
var smtpTransport = nodemailer.createTransport("SMTP",{
|
||||
service: nconf.get('SMTP_SERVICE'),
|
||||
auth: {
|
||||
user: nconf.get('SMTP_USER'),
|
||||
pass: nconf.get('SMTP_PASS')
|
||||
}
|
||||
});
|
||||
smtpTransport.sendMail(mailData, function(error, response){
|
||||
if(error){
|
||||
console.log(error);
|
||||
}else{
|
||||
console.log("Message sent: " + response.message);
|
||||
}
|
||||
smtpTransport.close(); // shut down the connection pool, no more messages
|
||||
});
|
||||
}
|
||||
|
||||
// Encryption using http://dailyjs.com/2010/12/06/node-tutorial-5/
|
||||
// Note: would use [password-hash](https://github.com/davidwood/node-password-hash), but we need to run
|
||||
// model.query().equals(), so it's a PITA to work in their verify() function
|
||||
|
||||
module.exports.encryptPassword = function(password, salt) {
|
||||
return crypto.createHmac('sha1', salt).update(password).digest('hex');
|
||||
}
|
||||
|
||||
module.exports.makeSalt = function() {
|
||||
var len = 10;
|
||||
return crypto.randomBytes(Math.ceil(len / 2)).toString('hex').substring(0, len);
|
||||
}
|
||||
Reference in New Issue
Block a user