<commit_before>const uuid = require('uuid');
const _ = require('underscore');

const AbilityDsl = require('./abilitydsl.js');
const CardAction = require('./cardaction.js');
const CardForcedInterrupt = require('./cardforcedinterrupt.js');
const CardForcedReaction = require('./cardforcedreaction.js');
const CardInterrupt = require('./cardinterrupt.js');
const CardReaction = require('./cardreaction.js');
const CustomPlayAction = require('./customplayaction.js');
const EventRegistrar = require('./eventregistrar.js');

const ValidKeywords = [
    'ambush',
    'insight',
    'intimidate',
    'pillage',
    'renown',
    'stealth',
    'terminal',
    'limited'
];
const LocationsWithEventHandling = ['play area', 'active plot', 'faction', 'agenda'];

class BaseCard {
    constructor(owner, cardData) {
        this.owner = owner;
        this.controller = owner;
        this.game = this.owner.game;
        this.cardData = cardData;

        this.uuid = uuid.v1();
        this.code = cardData.code;
        this.name = cardData.name;
        this.facedown = false;
        this.blankCount = 0;

        this.tokens = {};
        this.plotModifierValues = {
            gold: 0,
            initiative: 0,
            reserve: 0
        };
        this.canProvidePlotModifier = {
            gold: true,
            initiative: true,
            reserve: true
        };
        this.abilityRestrictions = [];
        this.menu = _([]);
        this.events = new EventRegistrar(this.game, this);

        this.abilities = { actions: [], reactions: [], persistentEffects: [], playActions: [] };
        this.parseKeywords(cardData.text || '');
        this.parseTraits(cardData.traits || '');
        this.setupCardAbilities(AbilityDsl);

        this.factions = {};
        this.addFaction(cardData.faction_code);
    }

    parseKeywords(text) {
        var firstLine = text.split('\n')[0];
        var potentialKeywords = _.map(firstLine.split('.'), k => k.toLowerCase().trim());

        this.keywords = {};
        this.printedKeywords = [];
        this.allowedAttachmentTrait = 'any';

        _.each(potentialKeywords, keyword => {
            if(_.contains(ValidKeywords, keyword)) {
                this.printedKeywords.push(keyword);
            } else if(keyword.indexOf('no attachment') === 0) {
                var match = keyword.match(/no attachments except <[bi]>(.*)<\/[bi]>/);
                if(match) {
                    this.allowedAttachmentTrait = match[1];
                } else {
                    this.allowedAttachmentTrait = 'none';
                }
            } else if(keyword.indexOf('ambush') === 0) {
                match = keyword.match(/ambush \((.*)\)/);
                if(match) {
                    this.ambushCost = parseInt(match[1]);
                }
            } else if(keyword.indexOf('bestow') === 0) {
                match = keyword.match(/bestow \((.*)\)/);
                if(match) {
                    this.bestowMax = parseInt(match[1]);
                }
            }
        });

        if(this.printedKeywords.length > 0) {
            this.persistentEffect({
                match: this,
                effect: AbilityDsl.effects.addMultipleKeywords(this.printedKeywords)
            });
        }
    }

    parseTraits(traits) {
        this.traits = {};

        var firstLine = traits.split('\n')[0];

        _.each(firstLine.split('.'), trait => this.addTrait(trait.toLowerCase().trim()));
    }

    registerEvents(events) {
        this.eventsForRegistration = events;
    }

    setupCardAbilities() {
    }

    plotModifiers(modifiers) {
        this.plotModifierValues = _.extend(this.plotModifierValues, modifiers);
        if(modifiers.gold) {
            this.persistentEffect({
                condition: () => this.canProvidePlotModifier['gold'],
                match: card => card.controller.activePlot === card,
                targetController: 'current',
                effect: AbilityDsl.effects.modifyGold(modifiers.gold)
            });
        }
        if(modifiers.initiative) {
            this.persistentEffect({
                condition: () => this.canProvidePlotModifier['initiative'],
                match: card => card.controller.activePlot === card,
                targetController: 'current',
                effect: AbilityDsl.effects.modifyInitiative(modifiers.initiative)
            });
        }
        if(modifiers.reserve) {
            this.persistentEffect({
                condition: () => this.canProvidePlotModifier['reserve'],
                match: card => card.controller.activePlot === card,
                targetController: 'current',
                effect: AbilityDsl.effects.modifyReserve(modifiers.reserve)
            });
        }
    }

    action(properties) {
        var action = new CardAction(this.game, this, properties);

        if(!action.isClickToActivate() && action.allowMenu()) {
            var index = this.abilities.actions.length;
            this.menu.push(action.getMenuItem(index));
        }
        this.abilities.actions.push(action);
    }

    reaction(properties) {
        var reaction = new CardReaction(this.game, this, properties);
        this.abilities.reactions.push(reaction);
    }

    forcedReaction(properties) {
        var reaction = new CardForcedReaction(this.game, this, properties);
        this.abilities.reactions.push(reaction);
    }

    interrupt(properties) {
        var reaction = new CardInterrupt(this.game, this, properties);
        this.abilities.reactions.push(reaction);
    }

    forcedInterrupt(properties) {
        var reaction = new CardForcedInterrupt(this.game, this, properties);
        this.abilities.reactions.push(reaction);
    }

    /**
     * Defines a special play action that can occur when the card is outside the
     * play area (e.g. Lady-in-Waiting's dupe marshal ability)
     */
    playAction(properties) {
        this.abilities.playActions.push(new CustomPlayAction(properties));
    }

    /**
     * Applies an effect that continues as long as the card providing the effect
     * is both in play and not blank.
     */
    persistentEffect(properties) {
        this.abilities.persistentEffects.push(_.extend({ duration: 'persistent' }, properties));
    }

    /**
     * Applies an effect with the specified properties while the current card is
     * attached to another card. By default the effect will target the parent
     * card, but you can provide a match function to narrow down whether the
     * effect is applied (for cases where the effect only applies to specific
     * characters).
     */
    whileAttached(properties) {
        this.persistentEffect({
            condition: properties.condition,
            match: (card, context) => card === this.parent && (!properties.match || properties.match(card, context)),
            targetController: 'any',
            effect: properties.effect,
            recalculateWhen: properties.recalculateWhen
        });
    }

    /**
     * Applies an immediate effect which lasts until the end of the current
     * challenge.
     */
    untilEndOfChallenge(propertyFactory) {
        var properties = propertyFactory(AbilityDsl);
        this.game.addEffect(this, _.extend({ duration: 'untilEndOfChallenge' }, properties));
    }

    /**
     * Applies an immediate effect which lasts until the end of the phase.
     */
    untilEndOfPhase(propertyFactory) {
        var properties = propertyFactory(AbilityDsl);
        this.game.addEffect(this, _.extend({ duration: 'untilEndOfPhase' }, properties));
    }

    /**
     * Applies an immediate effect which expires at the end of the phase. Per
     * game rules this duration is outside of the phase.
     */
    atEndOfPhase(propertyFactory) {
        var properties = propertyFactory(AbilityDsl);
        this.game.addEffect(this, _.extend({ duration: 'atEndOfPhase' }, properties));
    }

    /**
     * Applies an immediate effect which lasts until the end of the round.
     */
    untilEndOfRound(propertyFactory) {
        var properties = propertyFactory(AbilityDsl);
        this.game.addEffect(this, _.extend({ duration: 'untilEndOfRound' }, properties));
    }

    doAction(player, arg) {
        var action = this.abilities.actions[arg];

        if(!action) {
            return;
        }

        action.execute(player, arg);
    }

    hasKeyword(keyword) {
        var keywordCount = this.keywords[keyword.toLowerCase()] || 0;
        return keywordCount > 0;
    }

    hasPrintedKeyword(keyword) {
        return this.printedKeywords.includes(keyword.toLowerCase());
    }

    hasTrait(trait) {
        return !!this.traits[trait.toLowerCase()];
    }

    isFaction(faction) {
        return !!this.factions[faction.toLowerCase()];
    }

    isLoyal() {
        return this.cardData.is_loyal;
    }

    play() {
        _.each(this.abilities.persistentEffects, effect => {
            this.game.addEffect(this, effect);
        });
    }

    leavesPlay() {
        this.tokens = {};
    }

    moveTo(targetLocation) {
        if(LocationsWithEventHandling.includes(targetLocation) && !LocationsWithEventHandling.includes(this.location)) {
            this.events.register(this.eventsForRegistration);
        } else if(LocationsWithEventHandling.includes(this.location) && !LocationsWithEventHandling.includes(targetLocation)) {
            this.events.unregisterAll();
        }

        _.each(this.abilities.actions, action => {
            if(action.isEventListeningLocation(targetLocation) && !action.isEventListeningLocation(this.location)) {
                action.registerEvents();
            } else if(action.isEventListeningLocation(this.location) && !action.isEventListeningLocation(targetLocation)) {
                action.unregisterEvents();
            }
        });
        _.each(this.abilities.reactions, reaction => {
            if(reaction.isEventListeningLocation(targetLocation) && !reaction.isEventListeningLocation(this.location)) {
                reaction.registerEvents();
            } else if(reaction.isEventListeningLocation(this.location) && !reaction.isEventListeningLocation(targetLocation)) {
                reaction.unregisterEvents();
            }
        });

        if(targetLocation !== 'play area') {
            this.facedown = false;
        }

        this.location = targetLocation;
    }

    modifyDominance(player, strength) {
        return strength;
    }

    canPlay() {
        return true;
    }

    getMenu() {
        var menu = [];

        if(this.menu.isEmpty()) {
            return undefined;
        }

        menu.push({ command: 'click', text: 'Select Card' });
        menu = menu.concat(this.menu.value());

        return menu;
    }

    isUnique() {
        return this.cardData.is_unique;
    }

    isBlank() {
        return this.blankCount > 0;
    }

    getType() {
        return this.cardData.type_code;
    }

    getPrintedFaction() {
        return this.cardData.faction_code;
    }

    modifyClaim(player, type, claim) {
        return claim;
    }

    setBlank() {
        var before = this.isBlank();
        this.blankCount++;
        var after = this.isBlank();
        if(!before && after) {
            this.game.raiseEvent('onCardBlankToggled', this, after);
        }
    }

    allowGameAction(actionType) {
        let currentAbilityContext = this.game.currentAbilityContext;
        return !_.any(this.abilityRestrictions, restriction => restriction.isMatch(actionType, currentAbilityContext));
    }

    allowEffectFrom(sourceCard) {
        let currentAbilityContext = { source: 'card', card: sourceCard, stage: 'effect' };
        return !_.any(this.abilityRestrictions, restriction => restriction.isMatch('applyEffect', currentAbilityContext));
    }

    addAbilityRestriction(restriction) {
        this.abilityRestrictions.push(restriction);
    }

    removeAbilityRestriction(restriction) {
        this.abilityRestrictions = _.reject(this.abilityRestrictions, r => r === restriction);
    }

    addKeyword(keyword) {
        var lowerCaseKeyword = keyword.toLowerCase();
        this.keywords[lowerCaseKeyword] = this.keywords[lowerCaseKeyword] || 0;
        this.keywords[lowerCaseKeyword]++;
    }

    addTrait(trait) {
        var lowerCaseTrait = trait.toLowerCase();

        if(!lowerCaseTrait || lowerCaseTrait === '') {
            return;
        }

        if(!this.traits[lowerCaseTrait]) {
            this.traits[lowerCaseTrait] = 1;
        } else {
            this.traits[lowerCaseTrait]++;
        }
    }

    addFaction(faction) {
        if(!faction) {
            return;
        }

        var lowerCaseFaction = faction.toLowerCase();
        this.factions[lowerCaseFaction] = this.factions[lowerCaseFaction] || 0;
        this.factions[lowerCaseFaction]++;
    }

    removeKeyword(keyword) {
        var lowerCaseKeyword = keyword.toLowerCase();
        this.keywords[lowerCaseKeyword] = this.keywords[lowerCaseKeyword] || 0;
        this.keywords[lowerCaseKeyword]--;
    }

    removeTrait(trait) {
        this.traits[trait.toLowerCase()]--;
    }

    removeFaction(faction) {
        this.factions[faction.toLowerCase()]--;
    }

    clearBlank() {
        var before = this.isBlank();
        this.blankCount--;
        var after = this.isBlank();
        if(before && !after) {
            this.game.raiseEvent('onCardBlankToggled', this, after);
        }
    }

    addToken(type, number) {
        if(_.isUndefined(this.tokens[type])) {
            this.tokens[type] = 0;
        }

        this.tokens[type] += number;
    }

    hasToken(type) {
        return !!this.tokens[type];
    }

    removeToken(type, number) {
        this.tokens[type] -= number;

        if(this.tokens[type] < 0) {
            this.tokens[type] = 0;
        }

        if(this.tokens[type] === 0) {
            delete this.tokens[type];
        }
    }

    onClick(player) {
        var action = _.find(this.abilities.actions, action => action.isClickToActivate());
        if(action) {
            return action.execute(player) || action.deactivate(player);
        }

        return false;
    }

    getShortSummary() {
        return {
            code: this.cardData.code,
            label: this.cardData.label,
            name: this.cardData.name,
            type: this.getType()
        };
    }

    getSummary(activePlayer, hideWhenFaceup) {
        let isActivePlayer = activePlayer === this.owner;

        if(!isActivePlayer && (this.facedown || hideWhenFaceup)) {
            return { facedown: true };
        }

        let selectionState = activePlayer.getCardSelectionState(this);
        let state = {
            code: this.cardData.code,
            controlled: this.owner !== this.controller,
            facedown: this.facedown,
            menu: this.getMenu(),
            name: this.cardData.label,
            new: this.new,
            tokens: this.tokens,
            type: this.getType(),
            uuid: this.uuid
        };

        return _.extend(state, selectionState);
    }
}

module.exports = BaseCard;
<commit_msg>Initial changes for basecard<commit_after>const uuid = require('uuid');
const _ = require('underscore');

const AbilityDsl = require('./abilitydsl.js');
const CardAction = require('./cardaction.js');
const CardForcedInterrupt = require('./cardforcedinterrupt.js');
const CardForcedReaction = require('./cardforcedreaction.js');
const CardInterrupt = require('./cardinterrupt.js');
const CardReaction = require('./cardreaction.js');
const CustomPlayAction = require('./customplayaction.js');
const EventRegistrar = require('./eventregistrar.js');

const ValidKeywords = [
    'ancestral',
    'restricted',
    'limited'
];
const LocationsWithEventHandling = ['play area', 'active plot', 'faction', 'agenda'];

class BaseCard {
    constructor(owner, cardData) {
        this.owner = owner;
        this.controller = owner;
        this.game = this.owner.game;
        this.cardData = cardData;

        this.uuid = uuid.v1();
        this.code = cardData.code;
        this.name = cardData.name;
        this.facedown = false;
        this.blankCount = 0;

        this.tokens = {};
        this.strongholdModifierValues = {
            honor: 0,
            fate: 0,
            influence: 0,
            strength: 0
        };
        this.canProvideStrongholdModifier = {
            honor: true,
            fate: true,
            influence: true,
            strength: true
        };
        this.provinceModifierValues = {
            strength: 0
        };
        this.canProvideProvinceModifier = {
            strength: true
        };
        this.abilityRestrictions = [];
        this.menu = _([]);
        this.events = new EventRegistrar(this.game, this);

        this.abilities = { actions: [], reactions: [], persistentEffects: [], playActions: [] };
        this.parseKeywords(cardData.text || '');
        this.parseTraits(cardData.traits || '');
        this.setupCardAbilities(AbilityDsl);

        this.factions = {};
        this.addFaction(cardData.faction_code);
    }

    parseKeywords(text) {
        var firstLine = text.split('\n')[0];
        var potentialKeywords = _.map(firstLine.split('.'), k => k.toLowerCase().trim());

        this.keywords = {};
        this.printedKeywords = [];
        this.allowedAttachmentTrait = 'any';

        _.each(potentialKeywords, keyword => {
            if(_.contains(ValidKeywords, keyword)) {
                this.printedKeywords.push(keyword);
            }
        });

        if(this.printedKeywords.length > 0) {
            this.persistentEffect({
                match: this,
                effect: AbilityDsl.effects.addMultipleKeywords(this.printedKeywords)
            });
        }
    }

    parseTraits(traits) {
        this.traits = {};

        var firstLine = traits.split('\n')[0];

        _.each(firstLine.split('.'), trait => this.addTrait(trait.toLowerCase().trim()));
    }

    registerEvents(events) {
        this.eventsForRegistration = events;
    }

    setupCardAbilities() {
    }

    provinceModifiers(modifiers) {
        this.provincetModifierValues = _.extend(this.provinceModifierValues, modifiers);
        if(modifiers.strength) {
            this.persistentEffect({
                condition: () => this.canProvideProvinceModifier['strength'],
                match: card => card.controller.activeProvince === card,
                targetController: 'current',
                effect: AbilityDsl.effects.modifyStrength(modifiers.strength)
            });
        }
    }

    action(properties) {
        var action = new CardAction(this.game, this, properties);

        if(!action.isClickToActivate() && action.allowMenu()) {
            var index = this.abilities.actions.length;
            this.menu.push(action.getMenuItem(index));
        }
        this.abilities.actions.push(action);
    }

    reaction(properties) {
        var reaction = new CardReaction(this.game, this, properties);
        this.abilities.reactions.push(reaction);
    }

    forcedReaction(properties) {
        var reaction = new CardForcedReaction(this.game, this, properties);
        this.abilities.reactions.push(reaction);
    }

    interrupt(properties) {
        var reaction = new CardInterrupt(this.game, this, properties);
        this.abilities.reactions.push(reaction);
    }

    forcedInterrupt(properties) {
        var reaction = new CardForcedInterrupt(this.game, this, properties);
        this.abilities.reactions.push(reaction);
    }

    /**
     * Defines a special play action that can occur when the card is outside the
     * play area (e.g. Lady-in-Waiting's dupe marshal ability)
     */
    playAction(properties) {
        this.abilities.playActions.push(new CustomPlayAction(properties));
    }

    /**
     * Applies an effect that continues as long as the card providing the effect
     * is both in play and not blank.
     */
    persistentEffect(properties) {
        this.abilities.persistentEffects.push(_.extend({ duration: 'persistent' }, properties));
    }

    /**
     * Applies an effect with the specified properties while the current card is
     * attached to another card. By default the effect will target the parent
     * card, but you can provide a match function to narrow down whether the
     * effect is applied (for cases where the effect only applies to specific
     * characters).
     */
    whileAttached(properties) {
        this.persistentEffect({
            condition: properties.condition,
            match: (card, context) => card === this.parent && (!properties.match || properties.match(card, context)),
            targetController: 'any',
            effect: properties.effect,
            recalculateWhen: properties.recalculateWhen
        });
    }

    /**
     * Applies an immediate effect which lasts until the end of the current
     * challenge.
     */
    untilEndOfConflict(propertyFactory) {
        var properties = propertyFactory(AbilityDsl);
        this.game.addEffect(this, _.extend({ duration: 'untilEndOfConflict' }, properties));
    }

    /**
     * Applies an immediate effect which lasts until the end of the phase.
     */
    untilEndOfPhase(propertyFactory) {
        var properties = propertyFactory(AbilityDsl);
        this.game.addEffect(this, _.extend({ duration: 'untilEndOfPhase' }, properties));
    }

    /**
     * Applies an immediate effect which expires at the end of the phase. Per
     * game rules this duration is outside of the phase.
     */
    atEndOfPhase(propertyFactory) {
        var properties = propertyFactory(AbilityDsl);
        this.game.addEffect(this, _.extend({ duration: 'atEndOfPhase' }, properties));
    }

    /**
     * Applies an immediate effect which lasts until the end of the round.
     */
    untilEndOfRound(propertyFactory) {
        var properties = propertyFactory(AbilityDsl);
        this.game.addEffect(this, _.extend({ duration: 'untilEndOfRound' }, properties));
    }

    doAction(player, arg) {
        var action = this.abilities.actions[arg];

        if(!action) {
            return;
        }

        action.execute(player, arg);
    }

    hasKeyword(keyword) {
        var keywordCount = this.keywords[keyword.toLowerCase()] || 0;
        return keywordCount > 0;
    }

    hasPrintedKeyword(keyword) {
        return this.printedKeywords.includes(keyword.toLowerCase());
    }

    hasTrait(trait) {
        return !!this.traits[trait.toLowerCase()];
    }

    isFaction(faction) {
        return !!this.factions[faction.toLowerCase()];
    }

    isLoyal() {
        return this.cardData.is_loyal;
    }

    play() {
        _.each(this.abilities.persistentEffects, effect => {
            this.game.addEffect(this, effect);
        });
    }

    leavesPlay() {
        this.tokens = {};
    }

    moveTo(targetLocation) {
        if(LocationsWithEventHandling.includes(targetLocation) && !LocationsWithEventHandling.includes(this.location)) {
            this.events.register(this.eventsForRegistration);
        } else if(LocationsWithEventHandling.includes(this.location) && !LocationsWithEventHandling.includes(targetLocation)) {
            this.events.unregisterAll();
        }

        _.each(this.abilities.actions, action => {
            if(action.isEventListeningLocation(targetLocation) && !action.isEventListeningLocation(this.location)) {
                action.registerEvents();
            } else if(action.isEventListeningLocation(this.location) && !action.isEventListeningLocation(targetLocation)) {
                action.unregisterEvents();
            }
        });
        _.each(this.abilities.reactions, reaction => {
            if(reaction.isEventListeningLocation(targetLocation) && !reaction.isEventListeningLocation(this.location)) {
                reaction.registerEvents();
            } else if(reaction.isEventListeningLocation(this.location) && !reaction.isEventListeningLocation(targetLocation)) {
                reaction.unregisterEvents();
            }
        });

        if(targetLocation !== 'play area') {
            this.facedown = false;
        }

        this.location = targetLocation;
    }

    modifyFavor(player, glory) {
        return glory;
    }

    canPlay() {
        return true;
    }

    getMenu() {
        var menu = [];

        if(this.menu.isEmpty()) {
            return undefined;
        }

        menu.push({ command: 'click', text: 'Select Card' });
        menu = menu.concat(this.menu.value());

        return menu;
    }

    isUnique() {
        return this.cardData.is_unique;
    }

    isBlank() {
        return this.blankCount > 0;
    }

    getType() {
        return this.cardData.type_code;
    }

    getPrintedFaction() {
        return this.cardData.faction_code;
    }

    modifyClaim(player, type, claim) {
        return claim;
    }

    setBlank() {
        var before = this.isBlank();
        this.blankCount++;
        var after = this.isBlank();
        if(!before && after) {
            this.game.raiseEvent('onCardBlankToggled', this, after);
        }
    }

    allowGameAction(actionType) {
        let currentAbilityContext = this.game.currentAbilityContext;
        return !_.any(this.abilityRestrictions, restriction => restriction.isMatch(actionType, currentAbilityContext));
    }

    allowEffectFrom(sourceCard) {
        let currentAbilityContext = { source: 'card', card: sourceCard, stage: 'effect' };
        return !_.any(this.abilityRestrictions, restriction => restriction.isMatch('applyEffect', currentAbilityContext));
    }

    addAbilityRestriction(restriction) {
        this.abilityRestrictions.push(restriction);
    }

    removeAbilityRestriction(restriction) {
        this.abilityRestrictions = _.reject(this.abilityRestrictions, r => r === restriction);
    }

    addKeyword(keyword) {
        var lowerCaseKeyword = keyword.toLowerCase();
        this.keywords[lowerCaseKeyword] = this.keywords[lowerCaseKeyword] || 0;
        this.keywords[lowerCaseKeyword]++;
    }

    addTrait(trait) {
        var lowerCaseTrait = trait.toLowerCase();

        if(!lowerCaseTrait || lowerCaseTrait === '') {
            return;
        }

        if(!this.traits[lowerCaseTrait]) {
            this.traits[lowerCaseTrait] = 1;
        } else {
            this.traits[lowerCaseTrait]++;
        }
    }

    addFaction(faction) {
        if(!faction) {
            return;
        }

        var lowerCaseFaction = faction.toLowerCase();
        this.factions[lowerCaseFaction] = this.factions[lowerCaseFaction] || 0;
        this.factions[lowerCaseFaction]++;
    }

    removeKeyword(keyword) {
        var lowerCaseKeyword = keyword.toLowerCase();
        this.keywords[lowerCaseKeyword] = this.keywords[lowerCaseKeyword] || 0;
        this.keywords[lowerCaseKeyword]--;
    }

    removeTrait(trait) {
        this.traits[trait.toLowerCase()]--;
    }

    removeFaction(faction) {
        this.factions[faction.toLowerCase()]--;
    }

    clearBlank() {
        var before = this.isBlank();
        this.blankCount--;
        var after = this.isBlank();
        if(before && !after) {
            this.game.raiseEvent('onCardBlankToggled', this, after);
        }
    }

    addToken(type, number) {
        if(_.isUndefined(this.tokens[type])) {
            this.tokens[type] = 0;
        }

        this.tokens[type] += number;
    }

    hasToken(type) {
        return !!this.tokens[type];
    }

    removeToken(type, number) {
        this.tokens[type] -= number;

        if(this.tokens[type] < 0) {
            this.tokens[type] = 0;
        }

        if(this.tokens[type] === 0) {
            delete this.tokens[type];
        }
    }

    onClick(player) {
        var action = _.find(this.abilities.actions, action => action.isClickToActivate());
        if(action) {
            return action.execute(player) || action.deactivate(player);
        }

        return false;
    }

    getShortSummary() {
        return {
            code: this.cardData.code,
            label: this.cardData.label,
            name: this.cardData.name,
            type: this.getType()
        };
    }

    getSummary(activePlayer, hideWhenFaceup) {
        let isActivePlayer = activePlayer === this.owner;

        if(!isActivePlayer && (this.facedown || hideWhenFaceup)) {
            return { facedown: true };
        }

        let selectionState = activePlayer.getCardSelectionState(this);
        let state = {
            code: this.cardData.code,
            controlled: this.owner !== this.controller,
            facedown: this.facedown,
            menu: this.getMenu(),
            name: this.cardData.label,
            new: this.new,
            tokens: this.tokens,
            type: this.getType(),
            uuid: this.uuid
        };

        return _.extend(state, selectionState);
    }
}

module.exports = BaseCard;
<|endoftext|>


<commit_before>export const SERVER = {
  GRAPHQL_PORT: process.env.GRAPHQL_PORT || 8081,
  WS_PORT: process.env.WS_PORT || 8091,
  HOST: process.env.HOST || 'localhost'
}

export const DATA_BASE = {
  HOST_DB: process.env.HOST_DB || 'localhost',
  NAME_DB: process.env.NAME_DB || 'graphql',
  USER_DB: process.env.USER_DB || '',
  PASS_DB: process.env.PASS_DB || ''
}
<commit_msg>Ajustes de conexion DB#<commit_after>export const SERVER = {
  GRAPHQL_PORT: process.env.GRAPHQL_PORT || 8081,
  WS_PORT: process.env.WS_PORT || 8091,
  HOST: process.env.HOST || 'localhost'
}

export const DATA_BASE = {
  HOST_DB: process.env.HOST_DB || 'localhost',
  NAME_DB: process.env.NAME_DB || 'graphql',
  USER_DB: process.env.USER_DB || '',
  PASS_DB: process.env.PASS_DB || '',
  PORT_DB: process.env.PORT_DB || '27017'
}
<|endoftext|>


<commit_before>'use strict';

for (var i = 0; i < gameImages.length; i++)
  gameImages[i] = "img/" + gameImages[i];

var onProgressCallback = function(progressInfo) {
  console.log(progressInfo.progress.toFixed(1));
}

// After loading all images, load scenes
loader.add(gameImages)
  .on("progress", onProgressCallback)
  .load(function() {
    console.log("Loading " + sceneLoader.length + " scenes.");
    for (var i = 0; i < sceneLoader.length; i++) {
      console.log("Loading scene " + i);
      if (sceneLoader[i] != null)
        sceneLoader[i]();
      else
        console.log("ERROR: Failed to load scene " + i);
    }
    console.log("Finish");

    // Initial Scene
    setScene(2);
  });
<commit_msg>default scene back to 0<commit_after>'use strict';

for (var i = 0; i < gameImages.length; i++)
  gameImages[i] = "img/" + gameImages[i];

var onProgressCallback = function(progressInfo) {
  console.log(progressInfo.progress.toFixed(1));
}

// After loading all images, load scenes
loader.add(gameImages)
  .on("progress", onProgressCallback)
  .load(function() {
    console.log("Loading " + sceneLoader.length + " scenes.");
    for (var i = 0; i < sceneLoader.length; i++) {
      console.log("Loading scene " + i);
      if (sceneLoader[i] != null)
        sceneLoader[i]();
      else
        console.log("ERROR: Failed to load scene " + i);
    }
    console.log("Finish");

    // Initial Scene
    setScene(0);
  });
<|endoftext|>


<commit_before>var redis = require('redis');
var async = require('async');

var Stratum = require('stratum-pool');

module.exports = function(logger){

    var poolConfigs = JSON.parse(process.env.pools);

    Object.keys(poolConfigs).forEach(function(coin) {
        SetupForPool(logger, poolConfigs[coin]);
    });

};

function SetupForPool(logger, poolOptions){

    if (!poolOptions.shareProcessing ||
        !poolOptions.shareProcessing.internal ||
        !poolOptions.shareProcessing.internal.enabled)
        return;

    var coin = poolOptions.coin.name;
    var processingConfig = poolOptions.shareProcessing.internal;

    var logSystem = 'Payments';
    var logComponent = coin;

    var daemon = new Stratum.daemon.interface([processingConfig.daemon]);
    daemon.once('online', function(){
        logger.debug(logSystem, logComponent, 'Connected to daemon for payment processing');

        daemon.cmd('validateaddress', [poolOptions.address], function(result){
            if (!result[0].response || !result[0].response.ismine){
                logger.error(logSystem, logComponent, 'Daemon does not own pool address - payment processing can not be done with this daemon');
            }
        });
    }).once('connectionFailed', function(error){
        logger.error(logSystem, logComponent, 'Failed to connect to daemon for payment processing: ' + JSON.stringify(error));
    }).on('error', function(error){
        logger.error(logSystem, logComponent);
    }).init();

    var redisClient;

    var connectToRedis = function(){
        var reconnectTimeout;
        redisClient = redis.createClient(processingConfig.redis.port, processingConfig.redis.host);
        redisClient.on('ready', function(){
            clearTimeout(reconnectTimeout);
            logger.debug(logSystem, logComponent, 'Successfully connected to redis database');
        }).on('error', function(err){
                paymentLogger.error('redis', 'Redis client had an error: ' + JSON.stringify(err))
        }).on('end', function(){
            logger.error(logSystem, logComponent, 'Connection to redis database as been ended');
            logger.warning(logSystem, logComponent, 'Trying reconnection to redis in 3 seconds...');
            reconnectTimeout = setTimeout(function(){
                connectToRedis();
            }, 3000);
        });
    };
    connectToRedis();

    var processPayments = function(){
        async.waterfall([

            /* Call redis to get an array of rounds - which are coinbase transactions and block heights from submitted
               blocks. */
            function(callback){

                redisClient.smembers(coin + '_blocksPending', function(error, results){

                    if (error){
                        logger.error(logSystem, logComponent, 'Could get blocks from redis ' + JSON.stringify(error));
                        callback('Check finished - redis error for getting blocks');
                        return;
                    }
                    if (results.length === 0){
                        callback('Check finished - no pending blocks in redis');
                        return;
                    }

                    var rounds = results.map(function(r){
                        var details = r.split(':');
                        return {
                            category: details[0].category,
                            txHash: details[0],
                            height: details[1],
                            reward: details[2],
                            serialized: r
                        };
                    });

                    callback(null, rounds);
                });
            },

            /* Does a batch rpc call to daemon with all the transaction hashes to see if they are confirmed yet.
               It also adds the block reward amount to the round object - which the daemon gives also gives us. */
            function(rounds, callback){

                var batchRPCcommand = rounds.map(function(r){
                    return ['gettransaction', [r.txHash]];
                });

                daemon.batchCmd(batchRPCcommand, function(error, txDetails){

                    if (error || !txDetails){
                        callback('Check finished - daemon rpc error with batch gettransactions ' + JSON.stringify(error));
                        return;
                    }

                    txDetails = txDetails.filter(function(tx){
                        if (tx.error || !tx.result){
                            logger.error(logSystem, logComponent, 'error with requesting transaction from block daemon: ' + JSON.stringify(tx));
                            return false;
                        }
                        return true;
                    });

                    var magnitude;

                    rounds = rounds.filter(function(r){
                        var tx = txDetails.filter(function(tx){return tx.result.txid === r.txHash})[0];

                        if (!tx){
                            logger.error(logSystem, logComponent, 'daemon did not give us back a transaction that we asked for: ' + r.txHash);
                            return;
                        }

                        r.category = tx.result.details[0].category;

                        if (r.category === 'generate'){
                            r.amount = tx.result.amount;

                            var roundMagnitude = r.reward / r.amount;
                            if (!magnitude){
                                magnitude = roundMagnitude;

                                if (roundMagnitude % 10 !== 0)
                                    logger.error(logSystem, logComponent, 'Satosihis in coin is not divisible by 10 which is very odd');
                            }
                            else if (magnitude != roundMagnitude){
                                logger.error(logSystem, logComponent, 'Magnitude in a round was different than in another round. HUGE PROBLEM.');
                            }
                            return true;
                        }
                        else if (r.category === 'orphan')
                            return true;

                    });

                    if (rounds.length === 0){
                        callback('Check finished - no confirmed or orphaned blocks found');
                    }
                    else{
                        callback(null, rounds, magnitude);
                    }
                });
            },

            /* Does a batch redis call to get shares contributed to each round. Then calculates the reward
               amount owned to each miner for each round. */
            function(rounds, magnitude, callback){

                var shareLookups = rounds.map(function(r){
                    return ['hgetall', coin + '_shares:round' + r.height]
                });

                redisClient.multi(shareLookups).exec(function(error, allWorkerShares){
                    if (error){
                        callback('Check finished - redis error with multi get rounds share')
                        return;
                    }

                    var orphanMergeCommands = [];
                    var workerRewards = {};

                    rounds.forEach(function(round, i){
                        var workerShares = allWorkerShares[i];

                        if (round.category === 'orphan'){
                            Object.keys(workerShares).forEach(function(worker){
                                orphanMergeCommands.push(['hincrby', coin + '_shares:roundCurrent', worker, workerShares[worker]]);
                            });
                        }
                        else if (round.category === 'generate'){

                            var reward = round.reward * (1 - processingConfig.feePercent);

                            var totalShares = Object.keys(workerShares).reduce(function(p, c){
                                return p + parseInt(workerShares[c])
                            }, 0);

                            for (var worker in workerShares){
                                var percent = parseInt(workerShares[worker]) / totalShares;
                                var workerRewardTotal = Math.floor(reward * percent);
                                if (!(worker in workerRewards)) workerRewards[worker] = 0;
                                workerRewards[worker] += workerRewardTotal;
                            }
                        }
                    });

                    callback(null, rounds, magnitude, workerRewards, orphanMergeCommands);
                });
            },

            /* Does a batch call to redis to get worker existing balances from coin_balances*/
            function(rounds, magnitude, workerRewards, orphanMergeCommands, callback){

                var workers = Object.keys(workerRewards);

                redisClient.hmget([coin + '_balances'].concat(workers), function(error, results){
                    if (error && workers.length !== 0){
                        callback('Check finished - redis error with multi get balances ' + JSON.stringify(error));
                        return;
                    }

                    var workerBalances = {};

                    for (var i = 0; i < workers.length; i++){
                        workerBalances[workers[i]] = (parseInt(results[i]) || 0);
                    }

                    callback(null, rounds, magnitude, workerRewards, orphanMergeCommands, workerBalances);
                });

            },

            /* Calculate if any payments are ready to be sent and trigger them sending
             Get balance different for each address and pass it along as object of latest balances such as
             {worker1: balance1, worker2, balance2}
             when deciding the sent balance, it the difference should be -1*amount they had in db,
             if not sending the balance, the differnce should be +(the amount they earned this round)
             */
            function(rounds, magnitude, workerRewards, orphanMergeCommands, workerBalances, callback){

                //number of satoshis in a single coin unit - this can be different for coins so we calculate it :)

                daemon.cmd('getbalance', [''], function(results){

                    var totalBalance = results[0].response * magnitude;
                    var toBePaid = 0;
                    var workerPayments = {};

                    var balanceUpdateCommands = [];
                    var workerPayoutsCommand = [];

                    for (var worker in workerRewards){
                        workerPayments[worker] = ((workerPayments[worker] || 0) + workerRewards[worker]);
                    }
                    for (var worker in workerBalances){
                        workerPayments[worker] = ((workerPayments[worker] || 0) + workerBalances[worker]);
                    }

                    if (Object.keys(workerPayments).length > 0){
                        var coinPrecision = magnitude.toString().length - 1;
                        for (var worker in workerPayments){
                            if (workerPayments[worker] < processingConfig.minimumPayment * magnitude){
                                balanceUpdateCommands.push(['hincrby', coin + '_balances', worker, workerRewards[worker]]);
                                delete workerPayments[worker];
                            }
                            else{
                                if (workerBalances[worker] !== 0){
                                    balanceUpdateCommands.push(['hincrby', coin + '_balances', worker, -1 * workerBalances[worker]]);
                                }
                                var rewardInPrecision = (workerRewards[worker] / magnitude).toFixed(coinPrecision);
                                workerPayoutsCommand.push(['hincrbyfloat', coin + '_payouts', worker, rewardInPrecision]);
                                toBePaid += workerPayments[worker];
                            }
                        }

                    }

                    // txfee included in feeAmountToBeCollected
                    var feeAmountToBeCollected = parseFloat((toBePaid / (1 - processingConfig.feePercent) * processingConfig.feePercent).toFixed(coinPrecision));
                    var balanceLeftOver = totalBalance - toBePaid - feeAmountToBeCollected;
                    var minReserveSatoshis = processingConfig.minimumReserve * magnitude;
                    if (balanceLeftOver < minReserveSatoshis){

                        callback('Check finished - payments would wipe out minimum reserve, tried to pay out ' + toBePaid +
                            ' and collect ' + feeAmountToBeCollected + ' as fees' +
                            ' but only have ' + totalBalance + '. Left over balance would be ' + balanceLeftOver +
                            ', needs to be at least ' + minReserveSatoshis);
                        return;
                    }

                    var movePendingCommands = [];
                    var roundsToDelete = [];
                    rounds.forEach(function(r){
                        var destinationSet = r.category === 'orphan' ? '_blocksOrphaned' : '_blocksConfirmed';
                        movePendingCommands.push(['smove', coin + '_blocksPending', coin + destinationSet, r.serialized]);
                        roundsToDelete.push(coin + '_shares:round' + r.height)
                    });

                    var finalRedisCommands = [];

                    if (movePendingCommands.length > 0)
                        finalRedisCommands = finalRedisCommands.concat(movePendingCommands);

                    if (orphanMergeCommands.length > 0)
                        finalRedisCommands = finalRedisCommands.concat(orphanMergeCommands);

                    if (balanceUpdateCommands.length > 0)
                        finalRedisCommands = finalRedisCommands.concat(balanceUpdateCommands);

                    if (workerPayoutsCommand.length > 0)
                        finalRedisCommands = finalRedisCommands.concat(workerPayoutsCommand);

                    if (roundsToDelete.length > 0)
                        finalRedisCommands.push(['del'].concat(roundsToDelete));

                    if (toBePaid !== 0)
                        finalRedisCommands.push(['hincrbyfloat', coin + '_stats', 'totalPaid', (toBePaid / magnitude).toFixed(coinPrecision)]);

                    callback(null, magnitude, results[0].response, workerPayments, finalRedisCommands);

                });
            },

            function(magnitude, balanceBefore, workerPayments, finalRedisCommands, callback){

                //This does the final all-or-nothing atom transaction if block deamon sent payments
                var finalizeRedisTx = function(){
                    redisClient.multi(finalRedisCommands).exec(function(error, results){
                        if (error){
                            callback('Check finished - error with final redis commands for cleaning up ' + JSON.stringify(error));
                            return;
                        }
                        logger.debug(logSystem, logComponent, 'Payments processing performed an interval');
                    });
                };

                if (Object.keys(workerPayments).length === 0){
                    finalizeRedisTx();
                }
                else{

                    var coinPrecision = magnitude.toString().length - 1;
                    var addressAmounts = {};
                    var totalAmountUnits = 0;
                    for (var address in workerPayments){
                        var coinUnits = parseFloat((workerPayments[address] / magnitude).toFixed(coinPrecision));
                        addressAmounts[address] = coinUnits;
                        totalAmountUnits += coinUnits;
                    }

                    logger.debug(logSystem, logComponent, 'Payments about to be sent to: ' + JSON.stringify(addressAmounts));
                    daemon.cmd('sendmany', ['', addressAmounts], function(results){
                        if (results[0].error){
                            callback('Check finished - error with sendmany ' + JSON.stringify(results[0].error));
                            return;
                        }
                        finalizeRedisTx();
                        var totalWorkers = Object.keys(workerPayments).length;
                        logger.debug(logSystem, logComponent, 'Payments sent, a total of ' + totalAmountUnits + ' ' + poolOptions.coin.symbol +
                            ' was sent to ' + totalWorkers + ' miners');
                        setTimeout(function() { // not sure if we need some time to let daemon update the wallet balance
                            daemon.cmd('getbalance', [''], function(results){
                                var balanceDiff = balanceBefore - results[0].response;
                                var txFee = balanceDiff - totalAmountUnits;
                                var feeAmountUnits = parseFloat((totalAmountUnits / (1 - processingConfig.feePercent) * processingConfig.feePercent).toFixed(coinPrecision));
                                var poolFees = feeAmountUnits - txFee;
                                daemon.cmd('move', ['', processingConfig.feeCollectAccount, poolFees], function(results){
                                    if (results[0].error){
                                        callback('Check finished - error with move ' + JSON.stringify(results[0].error));
                                        return;
                                    }
                                    callback(null, poolFees + ' ' + poolOptions.coin.symbol + ' collected as pool fee');
                                });
                            });
                        }, 1000);
                    });
                }
            }
        ], function(error, result){
            if (error)
                logger.debug(logSystem, logComponent, error);

            else{
                logger.debug(logSystem, logComponent, result);
                // not sure if we need some time to let daemon update the wallet balance
                setTimeout(withdrawalProfit, 1000);
            }
        });
    };

    var withdrawalProfit = function(){

        if (!processingConfig.feeWithdrawalThreshold) return;

        logger.debug(logSystem, logComponent, 'Profit withdrawal started');
        daemon.cmd('getbalance', [processingConfig.feeCollectAccount], function(results){

            // We have to pay some tx fee here too but maybe we shoudn't really care about it too much as long as fee is less
            // then minimumReserve value. Because in this case even if feeCollectAccount account will have negative balance
            // total wallet balance will be positive and feeCollectAccount account will be refilled during next payment processing.
            // But to be as much accurate as we can we use getinfo command to retrieve minimum tx fee (paytxfee).
            daemon.cmd('getinfo', [], function(result){

                var paytxfee;
                if (!result[0].response || !result[0].response.paytxfee){
                    logger.error(logSystem, logComponent, 'Daemon does not have paytxfee property on getinfo method results - withdrawal processing could be broken with this daemon');
                    paytxfee = 0;
                } else {
                    paytxfee = result[0].response.paytxfee;
                }

                var withdrawalAmount = results[0].response - paytxfee;

                if (withdrawalAmount < processingConfig.feeWithdrawalThreshold){
                    logger.debug(logSystem, logComponent, 'Not enough profit to withdraw yet');
                }
                else{

                    var withdrawal = {};
                    withdrawal[processingConfig.feeReceiveAddress] = withdrawalAmount;

                    daemon.cmd('sendmany', [processingConfig.feeCollectAccount, withdrawal], function(results){
                        if (results[0].error){
                            logger.debug(logSystem, logComponent, 'Profit withdrawal finished - error with sendmany ' + JSON.stringify(results[0].error));
                            return;
                        }
                        logger.debug(logSystem, logComponent, 'Profit sent, a total of ' + withdrawalAmount + ' ' + poolOptions.coin.symbol +
                            ' was sent to ' + processingConfig.feeReceiveAddress);
                    });
                }
            });

        });

    };

    setInterval(processPayments, processingConfig.paymentInterval * 1000);
    setTimeout(processPayments, 100);

};<commit_msg>get tx fee paid from transaction made by sendmany<commit_after>var redis = require('redis');
var async = require('async');

var Stratum = require('stratum-pool');

module.exports = function(logger){

    var poolConfigs = JSON.parse(process.env.pools);

    Object.keys(poolConfigs).forEach(function(coin) {
        SetupForPool(logger, poolConfigs[coin]);
    });

};

function SetupForPool(logger, poolOptions){

    if (!poolOptions.shareProcessing ||
        !poolOptions.shareProcessing.internal ||
        !poolOptions.shareProcessing.internal.enabled)
        return;

    var coin = poolOptions.coin.name;
    var processingConfig = poolOptions.shareProcessing.internal;

    var logSystem = 'Payments';
    var logComponent = coin;

    var daemon = new Stratum.daemon.interface([processingConfig.daemon]);
    daemon.once('online', function(){
        logger.debug(logSystem, logComponent, 'Connected to daemon for payment processing');

        daemon.cmd('validateaddress', [poolOptions.address], function(result){
            if (!result[0].response || !result[0].response.ismine){
                logger.error(logSystem, logComponent, 'Daemon does not own pool address - payment processing can not be done with this daemon');
            }
        });
    }).once('connectionFailed', function(error){
        logger.error(logSystem, logComponent, 'Failed to connect to daemon for payment processing: ' + JSON.stringify(error));
    }).on('error', function(error){
        logger.error(logSystem, logComponent);
    }).init();

    var redisClient;

    var connectToRedis = function(){
        var reconnectTimeout;
        redisClient = redis.createClient(processingConfig.redis.port, processingConfig.redis.host);
        redisClient.on('ready', function(){
            clearTimeout(reconnectTimeout);
            logger.debug(logSystem, logComponent, 'Successfully connected to redis database');
        }).on('error', function(err){
                paymentLogger.error('redis', 'Redis client had an error: ' + JSON.stringify(err))
        }).on('end', function(){
            logger.error(logSystem, logComponent, 'Connection to redis database as been ended');
            logger.warning(logSystem, logComponent, 'Trying reconnection to redis in 3 seconds...');
            reconnectTimeout = setTimeout(function(){
                connectToRedis();
            }, 3000);
        });
    };
    connectToRedis();

    var processPayments = function(){
        async.waterfall([

            /* Call redis to get an array of rounds - which are coinbase transactions and block heights from submitted
               blocks. */
            function(callback){

                redisClient.smembers(coin + '_blocksPending', function(error, results){

                    if (error){
                        logger.error(logSystem, logComponent, 'Could get blocks from redis ' + JSON.stringify(error));
                        callback('Check finished - redis error for getting blocks');
                        return;
                    }
                    if (results.length === 0){
                        callback('Check finished - no pending blocks in redis');
                        return;
                    }

                    var rounds = results.map(function(r){
                        var details = r.split(':');
                        return {
                            category: details[0].category,
                            txHash: details[0],
                            height: details[1],
                            reward: details[2],
                            serialized: r
                        };
                    });

                    callback(null, rounds);
                });
            },

            /* Does a batch rpc call to daemon with all the transaction hashes to see if they are confirmed yet.
               It also adds the block reward amount to the round object - which the daemon gives also gives us. */
            function(rounds, callback){

                var batchRPCcommand = rounds.map(function(r){
                    return ['gettransaction', [r.txHash]];
                });

                daemon.batchCmd(batchRPCcommand, function(error, txDetails){

                    if (error || !txDetails){
                        callback('Check finished - daemon rpc error with batch gettransactions ' + JSON.stringify(error));
                        return;
                    }

                    txDetails = txDetails.filter(function(tx){
                        if (tx.error || !tx.result){
                            logger.error(logSystem, logComponent, 'error with requesting transaction from block daemon: ' + JSON.stringify(tx));
                            return false;
                        }
                        return true;
                    });

                    var magnitude;

                    rounds = rounds.filter(function(r){
                        var tx = txDetails.filter(function(tx){return tx.result.txid === r.txHash})[0];

                        if (!tx){
                            logger.error(logSystem, logComponent, 'daemon did not give us back a transaction that we asked for: ' + r.txHash);
                            return;
                        }

                        r.category = tx.result.details[0].category;

                        if (r.category === 'generate'){
                            r.amount = tx.result.amount;

                            var roundMagnitude = r.reward / r.amount;
                            if (!magnitude){
                                magnitude = roundMagnitude;

                                if (roundMagnitude % 10 !== 0)
                                    logger.error(logSystem, logComponent, 'Satosihis in coin is not divisible by 10 which is very odd');
                            }
                            else if (magnitude != roundMagnitude){
                                logger.error(logSystem, logComponent, 'Magnitude in a round was different than in another round. HUGE PROBLEM.');
                            }
                            return true;
                        }
                        else if (r.category === 'orphan')
                            return true;

                    });

                    if (rounds.length === 0){
                        callback('Check finished - no confirmed or orphaned blocks found');
                    }
                    else{
                        callback(null, rounds, magnitude);
                    }
                });
            },

            /* Does a batch redis call to get shares contributed to each round. Then calculates the reward
               amount owned to each miner for each round. */
            function(rounds, magnitude, callback){

                var shareLookups = rounds.map(function(r){
                    return ['hgetall', coin + '_shares:round' + r.height]
                });

                redisClient.multi(shareLookups).exec(function(error, allWorkerShares){
                    if (error){
                        callback('Check finished - redis error with multi get rounds share')
                        return;
                    }

                    var orphanMergeCommands = [];
                    var workerRewards = {};

                    rounds.forEach(function(round, i){
                        var workerShares = allWorkerShares[i];

                        if (round.category === 'orphan'){
                            Object.keys(workerShares).forEach(function(worker){
                                orphanMergeCommands.push(['hincrby', coin + '_shares:roundCurrent', worker, workerShares[worker]]);
                            });
                        }
                        else if (round.category === 'generate'){

                            var reward = round.reward * (1 - processingConfig.feePercent);

                            var totalShares = Object.keys(workerShares).reduce(function(p, c){
                                return p + parseInt(workerShares[c])
                            }, 0);

                            for (var worker in workerShares){
                                var percent = parseInt(workerShares[worker]) / totalShares;
                                var workerRewardTotal = Math.floor(reward * percent);
                                if (!(worker in workerRewards)) workerRewards[worker] = 0;
                                workerRewards[worker] += workerRewardTotal;
                            }
                        }
                    });

                    callback(null, rounds, magnitude, workerRewards, orphanMergeCommands);
                });
            },

            /* Does a batch call to redis to get worker existing balances from coin_balances*/
            function(rounds, magnitude, workerRewards, orphanMergeCommands, callback){

                var workers = Object.keys(workerRewards);

                redisClient.hmget([coin + '_balances'].concat(workers), function(error, results){
                    if (error && workers.length !== 0){
                        callback('Check finished - redis error with multi get balances ' + JSON.stringify(error));
                        return;
                    }

                    var workerBalances = {};

                    for (var i = 0; i < workers.length; i++){
                        workerBalances[workers[i]] = (parseInt(results[i]) || 0);
                    }

                    callback(null, rounds, magnitude, workerRewards, orphanMergeCommands, workerBalances);
                });

            },

            /* Calculate if any payments are ready to be sent and trigger them sending
             Get balance different for each address and pass it along as object of latest balances such as
             {worker1: balance1, worker2, balance2}
             when deciding the sent balance, it the difference should be -1*amount they had in db,
             if not sending the balance, the differnce should be +(the amount they earned this round)
             */
            function(rounds, magnitude, workerRewards, orphanMergeCommands, workerBalances, callback){

                //number of satoshis in a single coin unit - this can be different for coins so we calculate it :)

                daemon.cmd('getbalance', [''], function(results){

                    var totalBalance = results[0].response * magnitude;
                    var toBePaid = 0;
                    var workerPayments = {};

                    var balanceUpdateCommands = [];
                    var workerPayoutsCommand = [];

                    for (var worker in workerRewards){
                        workerPayments[worker] = ((workerPayments[worker] || 0) + workerRewards[worker]);
                    }
                    for (var worker in workerBalances){
                        workerPayments[worker] = ((workerPayments[worker] || 0) + workerBalances[worker]);
                    }

                    if (Object.keys(workerPayments).length > 0){
                        var coinPrecision = magnitude.toString().length - 1;
                        for (var worker in workerPayments){
                            if (workerPayments[worker] < processingConfig.minimumPayment * magnitude){
                                balanceUpdateCommands.push(['hincrby', coin + '_balances', worker, workerRewards[worker]]);
                                delete workerPayments[worker];
                            }
                            else{
                                if (workerBalances[worker] !== 0){
                                    balanceUpdateCommands.push(['hincrby', coin + '_balances', worker, -1 * workerBalances[worker]]);
                                }
                                var rewardInPrecision = (workerRewards[worker] / magnitude).toFixed(coinPrecision);
                                workerPayoutsCommand.push(['hincrbyfloat', coin + '_payouts', worker, rewardInPrecision]);
                                toBePaid += workerPayments[worker];
                            }
                        }

                    }

                    // txfee included in feeAmountToBeCollected
                    var feeAmountToBeCollected = parseFloat((toBePaid / (1 - processingConfig.feePercent) * processingConfig.feePercent).toFixed(coinPrecision));
                    var balanceLeftOver = totalBalance - toBePaid - feeAmountToBeCollected;
                    var minReserveSatoshis = processingConfig.minimumReserve * magnitude;
                    if (balanceLeftOver < minReserveSatoshis){

                        callback('Check finished - payments would wipe out minimum reserve, tried to pay out ' + toBePaid +
                            ' and collect ' + feeAmountToBeCollected + ' as fees' +
                            ' but only have ' + totalBalance + '. Left over balance would be ' + balanceLeftOver +
                            ', needs to be at least ' + minReserveSatoshis);
                        return;
                    }

                    var movePendingCommands = [];
                    var roundsToDelete = [];
                    rounds.forEach(function(r){
                        var destinationSet = r.category === 'orphan' ? '_blocksOrphaned' : '_blocksConfirmed';
                        movePendingCommands.push(['smove', coin + '_blocksPending', coin + destinationSet, r.serialized]);
                        roundsToDelete.push(coin + '_shares:round' + r.height)
                    });

                    var finalRedisCommands = [];

                    if (movePendingCommands.length > 0)
                        finalRedisCommands = finalRedisCommands.concat(movePendingCommands);

                    if (orphanMergeCommands.length > 0)
                        finalRedisCommands = finalRedisCommands.concat(orphanMergeCommands);

                    if (balanceUpdateCommands.length > 0)
                        finalRedisCommands = finalRedisCommands.concat(balanceUpdateCommands);

                    if (workerPayoutsCommand.length > 0)
                        finalRedisCommands = finalRedisCommands.concat(workerPayoutsCommand);

                    if (roundsToDelete.length > 0)
                        finalRedisCommands.push(['del'].concat(roundsToDelete));

                    if (toBePaid !== 0)
                        finalRedisCommands.push(['hincrbyfloat', coin + '_stats', 'totalPaid', (toBePaid / magnitude).toFixed(coinPrecision)]);

                    callback(null, magnitude, workerPayments, finalRedisCommands);

                });
            },

            function(magnitude, workerPayments, finalRedisCommands, callback){

                //This does the final all-or-nothing atom transaction if block deamon sent payments
                var finalizeRedisTx = function(){
                    redisClient.multi(finalRedisCommands).exec(function(error, results){
                        if (error){
                            callback('Check finished - error with final redis commands for cleaning up ' + JSON.stringify(error));
                            return;
                        }
                        logger.debug(logSystem, logComponent, 'Payments processing performed an interval');
                    });
                };

                if (Object.keys(workerPayments).length === 0){
                    finalizeRedisTx();
                }
                else{

                    var coinPrecision = magnitude.toString().length - 1;
                    var addressAmounts = {};
                    var totalAmountUnits = 0;
                    for (var address in workerPayments){
                        var coinUnits = parseFloat((workerPayments[address] / magnitude).toFixed(coinPrecision));
                        addressAmounts[address] = coinUnits;
                        totalAmountUnits += coinUnits;
                    }

                    logger.debug(logSystem, logComponent, 'Payments about to be sent to: ' + JSON.stringify(addressAmounts));
                    daemon.cmd('sendmany', ['', addressAmounts], function(results){
                        if (results[0].error){
                            callback('Check finished - error with sendmany ' + JSON.stringify(results[0].error));
                            return;
                        }
                        finalizeRedisTx();
                        var totalWorkers = Object.keys(workerPayments).length;
                        logger.debug(logSystem, logComponent, 'Payments sent, a total of ' + totalAmountUnits + ' ' + poolOptions.coin.symbol +
                            ' was sent to ' + totalWorkers + ' miners');
                        daemon.cmd('gettransaction', [results[0].response], function(results){
                            if (results[0].error){
                                callback('Check finished - error with gettransaction ' + JSON.stringify(results[0].error));
                                return;
                            }
                            var feeAmountUnits = parseFloat((totalAmountUnits / (1 - processingConfig.feePercent) * processingConfig.feePercent).toFixed(coinPrecision));
                            var poolFees = feeAmountUnits - results[0].response.fee;
                            daemon.cmd('move', ['', processingConfig.feeCollectAccount, poolFees], function(results){
                                if (results[0].error){
                                    callback('Check finished - error with move ' + JSON.stringify(results[0].error));
                                    return;
                                }
                                callback(null, poolFees + ' ' + poolOptions.coin.symbol + ' collected as pool fee');
                            });
                        });
                    });
                }
            }
        ], function(error, result){
            if (error)
                logger.debug(logSystem, logComponent, error);

            else{
                logger.debug(logSystem, logComponent, result);
                // not sure if we need some time to let daemon update the wallet balance
                setTimeout(withdrawalProfit, 1000);
            }
        });
    };

    var withdrawalProfit = function(){

        if (!processingConfig.feeWithdrawalThreshold) return;

        logger.debug(logSystem, logComponent, 'Profit withdrawal started');
        daemon.cmd('getbalance', [processingConfig.feeCollectAccount], function(results){

            // We have to pay some tx fee here too but maybe we shoudn't really care about it too much as long as fee is less
            // then minimumReserve value. Because in this case even if feeCollectAccount account will have negative balance
            // total wallet balance will be positive and feeCollectAccount account will be refilled during next payment processing.
            // But to be as much accurate as we can we use getinfo command to retrieve minimum tx fee (paytxfee).
            daemon.cmd('getinfo', [], function(result){

                var paytxfee;
                if (!result[0].response || !result[0].response.paytxfee){
                    logger.error(logSystem, logComponent, 'Daemon does not have paytxfee property on getinfo method results - withdrawal processing could be broken with this daemon');
                    paytxfee = 0;
                } else {
                    paytxfee = result[0].response.paytxfee;
                }

                var withdrawalAmount = results[0].response - paytxfee;

                if (withdrawalAmount < processingConfig.feeWithdrawalThreshold){
                    logger.debug(logSystem, logComponent, 'Not enough profit to withdraw yet');
                }
                else{

                    var withdrawal = {};
                    withdrawal[processingConfig.feeReceiveAddress] = withdrawalAmount;

                    daemon.cmd('sendmany', [processingConfig.feeCollectAccount, withdrawal], function(results){
                        if (results[0].error){
                            logger.debug(logSystem, logComponent, 'Profit withdrawal finished - error with sendmany ' + JSON.stringify(results[0].error));
                            return;
                        }
                        logger.debug(logSystem, logComponent, 'Profit sent, a total of ' + withdrawalAmount + ' ' + poolOptions.coin.symbol +
                            ' was sent to ' + processingConfig.feeReceiveAddress);
                    });
                }
            });

        });

    };

    setInterval(processPayments, processingConfig.paymentInterval * 1000);
    setTimeout(processPayments, 100);

};<|endoftext|>


<commit_before>/**
    @external Viz
    @see https://github.com/d3plus/d3plus-viz#Viz
*/
import {max, sum} from "d3-array";
import {nest} from "d3-collection";
import {accessor, assign, configPrep, constant, elem} from "d3plus-common";
import {Circle, Path, Rect} from "d3plus-shape";
import {Viz} from "d3plus-viz";

const tau = Math.PI * 2;

/**
    @class Radar
    @extends Plot
    @desc Creates a radar visualization based on an array of data.
*/
export default class Radar extends Viz {

  /**
      @memberof Radar
      @desc Invoked when creating a new class instance, and overrides any default parameters inherited from Plot.
      @private
  */
  constructor() {
    super();

    this._axisConfig = {
      fill: constant("none"),
      stroke: constant("#CCC"),
      strokeWidth: constant(1)
    };
    this._discrete = "metric";
    this._hover = true;
    this._levels = 6;
    this._metric = accessor("metric");
    this._radarPadding = 100;
    this._shape = constant("Path");
    this._shapeConfig = assign(this._shapeConfig, {
      Circle: {
        r: accessor("r", 0)
      },
      Path: {}
    });
    this._value = accessor("value");
  }

  /**
      Extends the draw behavior of the abstract Plot class.
      @private
  */
  _draw(callback) {
    super._draw(callback);
    const height = this._height - this._margin.top - this._margin.bottom,
          width = this._width - this._margin.left - this._margin.right;

    const depth = this._depth || 0,
          radius = (Math.min(height, width) - this._radarPadding) / 2,
          transform = `translate(${width / 2}, ${height / 2})`;

    const nestedAxisData = nest()
        .key(this._metric)
        .entries(this._filteredData),
          nestedGroupData = nest()
        .key(this._id)
        .key(this._metric)
        .entries(this._filteredData);

    const maxValue = max(nestedGroupData.map(h => h.values.map(d => sum(d.values, (x, i) => this._value(x, i)))).flat());

    const group = this._id(this._filteredData[0]),
          item = this._filteredData[0];

    let id = "";
    for (const property in this._filteredData[0]) {
      if (group === item[property]) {
        id = property;
        break;
      }
    }

    const circularAxis = Array.from(Array(this._levels).keys()).map(d => ({
      id: d,
      r: radius * ((d + 1) / this._levels)
    }));

    new Circle()
      .data(circularAxis)
      .select(
        elem("g.d3plus-Radar-radial-circles", {
          parent: this._select,
          enter: {transform},
          update: {transform}
        }).node()
      )
      .config(this._axisConfig)
      .render();

    const totalAxis = nestedAxisData.length;
    const polarAxis = nestedAxisData
      .map((d, i) => {
        const width = 100;
        const fontSize =
          this._shapeConfig.labelConfig.fontSize &&
            this._shapeConfig.labelConfig.fontSize(d, i) ||
          11;

        const lineHeight = fontSize * 1.4;
        const height = lineHeight * 2;
        const padding = 10,
              quadrant = parseInt(360 - 360 / totalAxis * i / 90, 10) % 4 + 1,
              radians = tau / totalAxis * i;

        let angle = 360 / totalAxis * i;

        let textAnchor = "start";
        let x = padding;

        if (quadrant === 2 || quadrant === 3) {
          x = -width - padding;
          textAnchor = "end";
          angle += 180;
        }

        const labelBounds = {
          x,
          y: -height / 2,
          width,
          height
        };

        return {
          id: d.key,
          angle,
          textAnchor,
          labelBounds,
          rotateAnchor: [-x, height / 2],
          x: radius * Math.cos(radians),
          y: radius * Math.sin(radians)
        };
      })
      .sort((a, b) => a.key - b.key);

    new Rect()
      .data(polarAxis)
      .rotate(d => d.angle)
      .width(0)
      .height(0)
      .x(d => d.x)
      .y(d => d.y)
      .label(d => d.id)
      .labelBounds(d => d.labelBounds)
      .labelConfig({
        padding: 0,
        textAnchor: d => d.data.textAnchor,
        rotateAnchor: d => d.data.data.rotateAnchor,
        fontColor: "black",
        verticalAlign: "middle"
      })
      .select(
        elem("g.d3plus-Radar-text", {
          parent: this._select,
          enter: {transform},
          update: {transform}
        }).node()
      )
      .render();

    new Path()
      .data(polarAxis)
      .d(d => `M${0},${0} ${-d.x},${-d.y}`)
      .select(
        elem("g.d3plus-Radar-axis", {
          parent: this._select,
          enter: {transform},
          update: {transform}
        }).node()
      )
      .config(this._axisConfig)
      .render();

    const groupData = nestedGroupData.map(h => {
      const q = h.values.map((d, i) => {
        const value = sum(d.values, (x, i) => this._value(x, i));
        const r = value / maxValue * radius,
              radians = tau / totalAxis * i;
        return {
          x: r * Math.cos(radians),
          y: r * Math.sin(radians)
        };
      });

      const d = `M ${q[0].x} ${q[0].y} ${q
        .map(l => `L ${l.x} ${l.y}`)
        .join(" ")} L ${q[0].x} ${q[0].y}`;

      return {[id]: h.key, d, __d3plusShape__: true, data: h.values};
    });

    this._shapes.push(
      new Path()
        .data(groupData)
        .id(d => d.id)
        .d(d => d.d)
        .select(
          elem("g.d3plus-Radar-items", {
            parent: this._select,
            enter: {transform},
            update: {transform}
          }).node()
        )
        .config(configPrep.bind(this)(this._shapeConfig, "shape", "Path"))
        .render()
    );

    return this;
  }

  /**
      @memberof Radar
      @desc If *value* is specified, sets the sum accessor to the specified function or number and returns the current class instance. If *value* is not specified, returns the current size accessor.
      @param {String} *value*
      @chainable
  */
  metric(_) {
    return arguments.length ? (this._metric = typeof _ === "function" ? _ : accessor(_), this) : this._metric;
  }

  /**
      @memberof Radar
      @desc If *value* is specified, sets the padding of the chart and returns the current class instance. If *value* is not specified, returns the current radarPadding. By default, the radarPadding is 100.
      @param {Number} [*value* = 100]
      @chainable
  */
  radarPadding(_) {
    return arguments.length
      ? (this._radarPadding = _, this)
      : this._radarPadding;
  }

  /**
      @memberof Radar
      @desc If *value* is specified, sets the value accessor to the specified function or number and returns the current class instance. If *value* is not specified, returns the current value accessor.
      @param {Function|String} *value*
      @example
function value(d) {
  return d.value;
}
  */
  value(_) {
    return arguments.length
      ? (this._value = typeof _ === "function" ? _ : accessor(_), this)
      : this._value;
  }

}
<commit_msg>improves definition of metric method<commit_after>/**
    @external Viz
    @see https://github.com/d3plus/d3plus-viz#Viz
*/
import {max, merge, sum} from "d3-array";
import {nest} from "d3-collection";
import {accessor, assign, configPrep, constant, elem} from "d3plus-common";
import {Circle, Path, Rect} from "d3plus-shape";
import {Viz} from "d3plus-viz";

const tau = Math.PI * 2;

/**
    @class Radar
    @extends Plot
    @desc Creates a radar visualization based on an array of data.
*/
export default class Radar extends Viz {

  /**
      @memberof Radar
      @desc Invoked when creating a new class instance, and overrides any default parameters inherited from Plot.
      @private
  */
  constructor() {
    super();

    this._axisConfig = {
      fill: constant("none"),
      stroke: constant("#CCC"),
      strokeWidth: constant(1)
    };
    this._discrete = "metric";
    this._hover = true;
    this._levels = 6;
    this._metric = accessor("metric");
    this._radarPadding = 100;
    this._shape = constant("Path");
    this._shapeConfig = assign(this._shapeConfig, {
      Circle: {
        r: accessor("r", 0)
      },
      Path: {}
    });
    this._value = accessor("value");
  }

  /**
      Extends the draw behavior of the abstract Plot class.
      @private
  */
  _draw(callback) {
    super._draw(callback);
    const height = this._height - this._margin.top - this._margin.bottom,
          width = this._width - this._margin.left - this._margin.right;

    const depth = this._depth || 0,
          radius = (Math.min(height, width) - this._radarPadding) / 2,
          transform = `translate(${width / 2}, ${height / 2})`;

    const nestedAxisData = nest()
        .key(this._metric)
        .entries(this._filteredData),
          nestedGroupData = nest()
        .key(this._id)
        .key(this._metric)
        .entries(this._filteredData);

    const maxValue = max(nestedGroupData.map(h => h.values.map(d => sum(d.values, (x, i) => this._value(x, i)))).flat());

    const group = this._id(this._filteredData[0]),
          item = this._filteredData[0];

    let id = "";
    for (const property in this._filteredData[0]) {
      if (group === item[property]) {
        id = property;
        break;
      }
    }

    const circularAxis = Array.from(Array(this._levels).keys()).map(d => ({
      id: d,
      r: radius * ((d + 1) / this._levels)
    }));

    new Circle()
      .data(circularAxis)
      .select(
        elem("g.d3plus-Radar-radial-circles", {
          parent: this._select,
          enter: {transform},
          update: {transform}
        }).node()
      )
      .config(this._axisConfig)
      .render();

    const totalAxis = nestedAxisData.length;
    const polarAxis = nestedAxisData
      .map((d, i) => {
        const width = 100;
        const fontSize =
          this._shapeConfig.labelConfig.fontSize &&
            this._shapeConfig.labelConfig.fontSize(d, i) ||
          11;

        const lineHeight = fontSize * 1.4;
        const height = lineHeight * 2;
        const padding = 10,
              quadrant = parseInt(360 - 360 / totalAxis * i / 90, 10) % 4 + 1,
              radians = tau / totalAxis * i;

        let angle = 360 / totalAxis * i;

        let textAnchor = "start";
        let x = padding;

        if (quadrant === 2 || quadrant === 3) {
          x = -width - padding;
          textAnchor = "end";
          angle += 180;
        }

        const labelBounds = {
          x,
          y: -height / 2,
          width,
          height
        };

        return {
          id: d.key,
          angle,
          textAnchor,
          labelBounds,
          rotateAnchor: [-x, height / 2],
          x: radius * Math.cos(radians),
          y: radius * Math.sin(radians)
        };
      })
      .sort((a, b) => a.key - b.key);

    new Rect()
      .data(polarAxis)
      .rotate(d => d.angle)
      .width(0)
      .height(0)
      .x(d => d.x)
      .y(d => d.y)
      .label(d => d.id)
      .labelBounds(d => d.labelBounds)
      .labelConfig({
        padding: 0,
        textAnchor: d => d.data.textAnchor,
        rotateAnchor: d => d.data.data.rotateAnchor,
        fontColor: "black",
        verticalAlign: "middle"
      })
      .select(
        elem("g.d3plus-Radar-text", {
          parent: this._select,
          enter: {transform},
          update: {transform}
        }).node()
      )
      .render();

    new Path()
      .data(polarAxis)
      .d(d => `M${0},${0} ${-d.x},${-d.y}`)
      .select(
        elem("g.d3plus-Radar-axis", {
          parent: this._select,
          enter: {transform},
          update: {transform}
        }).node()
      )
      .config(this._axisConfig)
      .render();

    const groupData = nestedGroupData.map(h => {
      const q = h.values.map((d, i) => {
        const value = sum(d.values, (x, i) => this._value(x, i));
        const r = value / maxValue * radius,
              radians = tau / totalAxis * i;
        return {
          x: r * Math.cos(radians),
          y: r * Math.sin(radians)
        };
      });

      const d = `M ${q[0].x} ${q[0].y} ${q
        .map(l => `L ${l.x} ${l.y}`)
        .join(" ")} L ${q[0].x} ${q[0].y}`;

      return {id: h.key, d, __d3plus__: true, data: assign(...merge(h.values.map(d => d.values)))};
    });

    this._shapes.push(
      new Path()
        .data(groupData)
        .d(d => d.d)
        .select(
          elem("g.d3plus-Radar-items", {
            parent: this._select,
            enter: {transform},
            update: {transform}
          }).node()
        )
        .config(configPrep.bind(this)(this._shapeConfig, "shape", "Path"))
        .render()
    );

    return this;
  }

  /**
      @memberof Radar
      @desc Defines the value used as axis. If *value* is specified, sets the accessor to the specified metric function. If *value* is not specified, returns the current metric accessor.
      @param {Function|String} *value*
      @chainable
  */
  metric(_) {
    return arguments.length ? (this._metric = typeof _ === "function" ? _ : accessor(_), this) : this._metric;
  }

  /**
      @memberof Radar
      @desc If *value* is specified, sets the padding of the chart and returns the current class instance. If *value* is not specified, returns the current radarPadding. By default, the radarPadding is 100.
      @param {Number} [*value* = 100]
      @chainable
  */
  radarPadding(_) {
    return arguments.length
      ? (this._radarPadding = _, this)
      : this._radarPadding;
  }

  /**
      @memberof Radar
      @desc If *value* is specified, sets the value accessor to the specified function or number and returns the current class instance. If *value* is not specified, returns the current value accessor.
      @param {Function|String} *value*
      @example
function value(d) {
  return d.value;
}
  */
  value(_) {
    return arguments.length
      ? (this._value = typeof _ === "function" ? _ : accessor(_), this)
      : this._value;
  }

}
<|endoftext|>


<commit_before>var DataModel = require("dme/DataModel");
var RestrictiveFacet = require("dme/RestrictiveFacet");
var PermissiveFacet= require("dme/PermissiveFacet");
var models = require("./models");
var MongoStore = require("dme/store/mongodb");
var config = require("./config");
var when = require('promised-io/promise').when;
var dataModel = new DataModel({});

console.log("Found Models: ", models);

Object.keys(models).forEach(function(modelId){
	var mongoStore = new MongoStore(modelId,{url: config.get("mongo").url});
	var model = new models[modelId].Model(mongoStore,{});
	var publicFacet,userFacet;
	switch(modelId){
		case "user":
			publicFacet = new RestrictiveFacet({model: model,
				get: function(id, opts) {
					return this.model.get(id,opts);
				},
				query: function(query,opts){
					return when(this.model.query(query,opts), function(response){
						if (response && response.results) {
							response.results = response.results.map(function(res){
								delete res.resetCode;
								delete res.email;
								delete res.password;
								return res;
							});
						}

						return response
					});
				}
			})
			userFacet = new RestrictiveFacet({model: model,
				get: function(id, opts) {
					console.log("GET User: ", id);
					return when(this.model.get(id,opts), function(user){
						console.log("User: ", user);
						delete user.resetCode;
						delete user.email;
						delete user.password;
						return user;
					});
				},
				post: function(obj, opts) {
					return this.model.post(obj,opts);
				},

				query: function(query,opts){
					return when(this.model.query(query,opts), function(response){
						if (response && response.results) {
							response.results = response.results.map(function(res){
								delete res.resetCode;
								delete res.email
								delete res.password;
								return res;
							});
						}

						console.log('response: ', response);
						return response
					});
				},

				notify: function(event,message,subject,opts){
					var req = opts.req || {};
					if (req.user && req.user.id) {
						console.log("Mail To: ", req.user.id);
						return when(this.model.mail(req.user.id.replace("@patricbrc.org",""),message, "[PATRIC-" + event + "] " + subject,opts), function(){
							return {results: "Notification Sent"}
						});
					}else{
						opts.res.status(403);
						throw Error("Forbidden");
					}
				}
			})
			break;
		default:
			publicFacet = new RestrictiveFacet({});
			userFacet = new RestrictiveFacet({});
			break;
	}
	dataModel.set(modelId,model,{public:publicFacet, user:userFacet ,admin:new PermissiveFacet({model:model})});
});

module.exports = dataModel;

<commit_msg>fix email return for the owning user<commit_after>var DataModel = require("dme/DataModel");
var RestrictiveFacet = require("dme/RestrictiveFacet");
var PermissiveFacet= require("dme/PermissiveFacet");
var models = require("./models");
var MongoStore = require("dme/store/mongodb");
var config = require("./config");
var when = require('promised-io/promise').when;
var dataModel = new DataModel({});

console.log("Found Models: ", models);

Object.keys(models).forEach(function(modelId){
	var mongoStore = new MongoStore(modelId,{url: config.get("mongo").url});
	var model = new models[modelId].Model(mongoStore,{});
	var publicFacet,userFacet;
	switch(modelId){
		case "user":
			publicFacet = new RestrictiveFacet({model: model,
				get: function(id, opts) {
					return this.model.get(id,opts);
				},
				query: function(query,opts){
					return when(this.model.query(query,opts), function(response){
						if (response && response.results) {
							response.results = response.results.map(function(res){
								delete res.resetCode;
								delete res.email;
								delete res.password;
								return res;
							});
						}

						return response
					});
				}
			})
			userFacet = new RestrictiveFacet({model: model,
				get: function(id, opts) {
					return when(this.model.get(id,opts), function(user){
						delete user.resetCode;
						delete user.password;

						var uid;

						if (opts && opts.req && opts.req.user){
							uid = opts.req.user.id.replace("@patricbrc.org","");
						}

						//console.log("User Get ID: ", (opts && opts.req && opts.req.user)?opts.req.user.id:"No user in profile");
						//if (!opts || !opts.req || !opts.req.user || (opts.req.user.id!=id)){
						if (!uid || (uid!=id)){
							delete user.email;
						}
						return user;
					});
				},
				post: function(obj, opts) {
					if (!opts || !opts.req || !opts.req.user || (opts.req.user.id!=obj.id)) {
						throw new Error("Permission Denied");
					}
					return this.model.post(obj,opts);
				},

				query: function(query,opts){
					return when(this.model.query(query,opts), function(response){
						if (response && response.results) {
							response.results = response.results.map(function(res){
								delete res.resetCode;
								delete res.email
								delete res.password;
								return res;
							});
						}

						return response
					});
				},

				notify: function(event,message,subject,opts){
					var req = opts.req || {};
					if (req.user && req.user.id) {
						console.log("Mail To: ", req.user.id);
						return when(this.model.mail(req.user.id.replace("@patricbrc.org",""),message, "[PATRIC-" + event + "] " + subject,opts), function(){
							return {results: "Notification Sent"}
						});
					}else{
						opts.res.status(403);
						throw Error("Forbidden");
					}
				}
			})
			break;
		default:
			publicFacet = new RestrictiveFacet({});
			userFacet = new RestrictiveFacet({});
			break;
	}
	dataModel.set(modelId,model,{public:publicFacet, user:userFacet ,admin:new PermissiveFacet({model:model})});
});

module.exports = dataModel;

<|endoftext|>