<commit_before>const async = require('async');
const url = require('url');

const Logger = require('werelogs').Logger;

const config = require('../../conf/Config');
const zookeeper = require('../clients/zookeeper');
const ProvisionDispatcher = require('../provisioning/ProvisionDispatcher');
const IngestionReader = require('./IngestionReader');
const MetricsProducer = require('../MetricsProducer');
const MetricsConsumer = require('../MetricsConsumer');
const MongoIngestionInterface =
    require('../../extensions/ingestion/utils/MongoIngestionInterface');

class IngestionPopulator {
    /**
     * Create an ingestion populator object to populate various kafka
     * queues from the metadata log
     *
     * @constructor
     * @param {Object} zkConfig - zookeeper configuration object
     * @param {String} zkConfig.connectionString - zookeeper
     *   connection string as "host:port[/chroot]"
     * @param {Object} kafkaConfig - kafka configuration object
     * @param {string} kafkaConfig.hosts - kafka hosts list
     * as "host:port[,host:port...]"
     * @param {Object} qpConfig - queue populator configuration
     * @param {String} qpConfig.zookeeperPath - sub-path to use for
     *   storing populator state in zookeeper
     * @param {String} qpConfig.logSource - type of source
     *   log: "bucketd" (raft log) or "dmd" (bucketfile)
     * @param {Object} [qpConfig.bucketd] - bucketd source
     *   configuration (mandatory if logSource is "bucket")
     * @param {Object} [qpConfig.dmd] - dmd source
     *   configuration (mandatory if logSource is "dmd")
     * @param {Object} [qpConfig.mongo] - mongo source
     *   configuration (mandatory if logSource is "mongo")
     * @param {Object} mConfig - metrics configuration object
     * @param {string} mConfig.topic - metrics topic
     * @param {Object} rConfig - redis configuration object
     * @param {Object} ingestionConfig - ingestion extension configurations
     * @param {Object} s3Config - configuration to connect to S3 Client
     */
    constructor(zkConfig, kafkaConfig, qpConfig, mConfig, rConfig,
                ingestionConfig, s3Config) {
        this.zkConfig = zkConfig;
        this.kafkaConfig = kafkaConfig;
        this.qpConfig = qpConfig;
        this.mConfig = mConfig;
        this.rConfig = rConfig;
        this.ingestionConfig = ingestionConfig;
        this.s3Config = s3Config;

        this.log = new Logger('Backbeat:IngestionPopulator');

        // mongo client interface for finding ingestion buckets
        this._mongoClient = null;

        // list of active log readers
        this.logReaders = [];

        // list of updated log readers, if any
        this.logReadersUpdate = [];
        // metrics clients
        this._mProducer = null;
        this._mConsumer = null;
        // location details to be updated from config initManagement updates
        this._locationDetails = null;

        // provisioned ingestion log sources
        // i.e.: { an-ingestion-location: new ProvisionDispatcher() }
        this._provisionedIngestionSources = {};
    }

    /**
     * Open the ingestion populator
     *
     * @param {function} cb - callback function
     * @return {undefined}
     */
    open(cb) {
        this._loadExtension();
        async.series([
            next => this._setupMetricsClients(err => {
                if (err) {
                    this.log.error('error setting up metrics client', {
                        method: 'IngestionPopulator.open',
                        error: err,
                    });
                }
                return next(err);
            }),
            next => this._setupIngestionExtension(err => {
                if (err) {
                    this.log.error(
                        'error setting up ingestion extension', {
                            method: 'IngestionPopulator.open',
                            error: err,
                        });
                }
                return next(err);
            }),
            next => this._setupZookeeper(next),
            next => {
                this._mongoClient =
                    new MongoIngestionInterface(this.qpConfig.mongo);
                this._mongoClient.setup(err => {
                    if (err) {
                        this.log.error('error setting up mongo client', {
                            method: 'IngestionPopulator.open',
                            error: err,
                        });
                    }
                    return next(err);
                });
            },
        ], err => {
            if (err) {
                this.log.error('error starting up ingestion populator',
                    { method: 'IngestionPopulator.open',
                        error: err });
                return cb(err);
            }
            this._locationDetails = config.getLocations();
            config.on('locations-update', () => {
                this._locationDetails = config.getLocations();
            });
            return cb();
        });
    }

    _setupMetricsClients(cb) {
        // Metrics Consumer
        this._mConsumer = new MetricsConsumer(this.rConfig, this.mConfig,
            this.kafkaConfig);
        this._mConsumer.start();

        // Metrics Producer
        this._mProducer = new MetricsProducer(this.kafkaConfig, this.mConfig);
        this._mProducer.setupProducer(cb);
    }

    /**
     * Check mongo for new buckets and apply any updates
     * @param {Function} cb - callback(error)
     * @return {undefined}
     */
    applyUpdates(cb) {
        this._mongoClient.getIngestionBuckets((err, buckets) => {
            if (err) {
                return cb(err);
            }
            // Example buckets list:
            // [
            //     {
            //         name: 'my-zenko-bucket',
            //         ingestion: { Status: 'enabled' },
            //         locationConstraint: 'my-ring'
            //     },
            // ]

            // Example this._locationDetails
            // {
            //     'my-ring': {
            //         accessKey: ...,
            //         secretKey: ...,
            //         endpoint: 'http://10.100.1.128:8000',
            //         locationType: 'scality_s3',
            //         bucketName: 'source-bucket-1'
            //     }
            // }

            const sources = Object.keys(this._provisionedIngestionSources);

            return async.series([
                done => async.each(buckets, (bucket, next) => {
                    // zenko bucket name
                    const key = bucket.name;
                    if (!sources.includes(key)) {
                        // Add new ingestion source
                        const locationDetails =
                            this._locationDetails[bucket.locationConstraint];
                        const endpoint = url.parse(locationDetails.endpoint);
                        // TODO: will change for non-ring sources
                        const newSource = {
                            // target zenko bucket name
                            name: bucket.name,
                            // source bucket name to ingest from
                            bucket: locationDetails.bucketName,
                            // location storage name
                            prefix: bucket.locationConstraint,
                            // TODO: make configurable
                            cronRule: '*/5 * * * * *',
                            zookeeperSuffix: `/${bucket.name}`,
                            // source (s3c) endpoint
                            host: endpoint.hostname,
                            port: parseInt(endpoint.port, 10),
                            https: endpoint.protocol.slice(0, -1) === 'https',
                            type: locationDetails.locationType,
                            raftCount: 8,
                        };
                        return this.addNewLogSource(newSource, next);
                    }
                    const index = sources.findIndex(s => s === key);
                    // if an existing source, stop tracking it from
                    // `sources`
                    if (index >= 0) {
                        sources.splice(index, 1);
                    }
                    return next();
                }, done),
                // Any leftover `sources` have been removed
                // Close these logs..
                done => async.each(sources,
                    (source, next) => this.closeLogState(source, next),
                    done),
            ], cb);
        });
    }

    /**
     * add an Ingestion Reader
     * @param {Object} source - new source object
     * @param {Function} cb - callback(error)
     * @return {undefined}
     */
    addNewLogSource(source, cb) {
        this._extension.createZkPath(err => {
            if (err) {
                return cb(err);
            }
            const zookeeperUrlIngest =
                `${this.zkConfig.connectionString}` +
                `${this.ingestionConfig.zookeeperPath}/${source.name}`;
            const zkEndpointIngest = `${zookeeperUrlIngest}/raft-id-dispatcher`;
            return this._subscribeToLogSourceDispatcher(zkEndpointIngest,
                source, cb);
        }, source);
    }

    _subscribeToLogSourceDispatcher(zkEndpoint, bucketd, cb) {
        const key = bucketd.name;
        if (this._provisionedIngestionSources[key]) {
            this.log.info('waiting to be provisioned a log source',
                          { zkEndpoint });
            return cb();
        }

        const ingestionPath = this.ingestionConfig.zookeeperPath;
        // TODO: Remove/Replace ProvisionDispatcher
        const raftIdDispatcher =
            new ProvisionDispatcher({ connectionString: zkEndpoint });
        this._provisionedIngestionSources[key] = raftIdDispatcher;

        raftIdDispatcher.subscribe((err, items) => {
            if (err) {
                this.log.error(
                    'error when receiving log source provision list',
                    { zkEndpoint, error: err });
                // do not escalate error in order to allow retries.
                // provisioning log source may take a while
                return undefined;
            }
            if (items.length === 0) {
                this.log.info('no log source provisioned, idling',
                              { zkEndpoint });
            }
            const newRaftReaders = items.map(token => {
                const ingestionReader = new IngestionReader({
                    zkClient: this.zkClient,
                    kafkaConfig: this.kafkaConfig,
                    bucketdConfig: bucketd,
                    raftId: token,
                    logger: this.log,
                    extensions: [this._extension],
                    metricsProducer: this._mProducer,
                    ingestionPath,
                    qpConfig: this.qpConfig,
                    s3Config: this.s3Config,
                    bucket: bucketd.name,
                });
                return ingestionReader;
            });
            this.logReadersUpdate.push(...newRaftReaders);
            return undefined;
        });
        this.log.info('waiting to be provisioned a log source',
                      { zkEndpoint });
        return cb();
    }

    _setupZookeeper(done) {
        const populatorZkPath = this.ingestionConfig.zookeeperPath;
        const zookeeperUrl =
            `${this.zkConfig.connectionString}${populatorZkPath}`;
        this.log.info('opening zookeeper connection for persisting ' +
                      'populator state',
                      { zookeeperUrl });
        this.zkClient = zookeeper.createClient(zookeeperUrl, {
            autoCreateNamespace: this.zkConfig.autoCreateNamespace,
        });
        this.zkClient.connect();
        this.zkClient.once('error', done);
        this.zkClient.once('ready', () => {
            // just in case there would be more 'error' events emitted
            this.zkClient.removeAllListeners('error');
            done();
        });
    }

    _loadExtension() {
        const index = require('../../extensions/ingestion/index.js');
        if (!index.queuePopulatorExtension) {
            this.log.fatal('Missing ingestion populator extension file');
            process.exit(1);
        }
        // eslint-disable-next-line new-cap
        const ext = new index.queuePopulatorExtension({
            config: this.ingestionConfig,
            logger: this.log,
        });
        ext.setZkConfig(this.zkConfig);
        this._extension = ext;

        this.log.info('ingestion extension is active');
    }

    _setupIngestionExtension(cb) {
        return this._extension.setupZookeeper(cb);
    }

    _setupUpdatedReaders(done) {
        const newReaders = this.logReadersUpdate;
        this.logReadersUpdate = [];
        async.each(newReaders, (logReader, cb) => logReader.setup(cb),
                   err => {
                       if (err) {
                           return done(err);
                       }
                       this.logReaders.push(...newReaders);
                       return done();
                   });
    }

    /**
     * Close all provisioned ingestion sources
     * @param {function} cb - callback function
     * @return {undefined}
     */
    close(cb) {
        return async.each(
            Object.keys(this._provisionedIngestionSources),
            (raftIdDispatcher, done) => {
                this.closeLogState(raftIdDispatcher, done);
            }, cb);
    }

    /**
     * remove and close an Ingestion Reader
     * @param {String} key - source key (zenko bucket name)
     * @param {Function} cb - callback(error)
     * @return {undefined}
     */
    closeLogState(key, cb) {
        const raftIdDispatcher = this._provisionedIngestionSources[key];
        if (raftIdDispatcher !== undefined) {
            delete this._provisionedIngestionSources[key];
            this._checkAndRemoveLogReader(key, this.logReaders);
            this._checkAndRemoveLogReader(key, this.logReadersUpdate);

            return raftIdDispatcher.unsubscribe(cb);
        }
        return process.nextTick(cb);
    }

    _checkAndRemoveLogReader(name, list) {
        const index = list.findIndex(lr =>
            lr.getTargetZenkoBucketName() === name);
        if (index !== -1) {
            list.splice(index, 1);
        }
    }

    _processLogEntries(params, done) {
        return async.map(
            this.logReaders,
            (logReader, cb) => logReader.processLogEntries(params, cb),
            done);
    }

    processLogEntries(params, done) {
        if (this.logReadersUpdate.length >= 1) {
            return this._setupUpdatedReaders(err => {
                if (err) {
                    return done(err);
                }
                return this._processLogEntries(params, done);
            });
        }
        return this._processLogEntries(params, done);
    }

    zkStatus() {
        return this.zkClient.getState();
    }
}

module.exports = IngestionPopulator;
<commit_msg>post-review changes<commit_after>const async = require('async');
const url = require('url');

const Logger = require('werelogs').Logger;

const config = require('../../conf/Config');
const zookeeper = require('../clients/zookeeper');
const ProvisionDispatcher = require('../provisioning/ProvisionDispatcher');
const IngestionReader = require('./IngestionReader');
const MetricsProducer = require('../MetricsProducer');
const MetricsConsumer = require('../MetricsConsumer');
const MongoIngestionInterface =
    require('../../extensions/ingestion/utils/MongoIngestionInterface');

class IngestionPopulator {
    /**
     * Create an ingestion populator object to populate various kafka
     * queues from the metadata log
     *
     * @constructor
     * @param {Object} zkConfig - zookeeper configuration object
     * @param {String} zkConfig.connectionString - zookeeper
     *   connection string as "host:port[/chroot]"
     * @param {Object} kafkaConfig - kafka configuration object
     * @param {string} kafkaConfig.hosts - kafka hosts list
     * as "host:port[,host:port...]"
     * @param {Object} qpConfig - queue populator configuration
     * @param {String} qpConfig.zookeeperPath - sub-path to use for
     *   storing populator state in zookeeper
     * @param {String} qpConfig.logSource - type of source
     *   log: "bucketd" (raft log) or "dmd" (bucketfile)
     * @param {Object} [qpConfig.bucketd] - bucketd source
     *   configuration (mandatory if logSource is "bucket")
     * @param {Object} [qpConfig.dmd] - dmd source
     *   configuration (mandatory if logSource is "dmd")
     * @param {Object} [qpConfig.mongo] - mongo source
     *   configuration (mandatory if logSource is "mongo")
     * @param {Object} mConfig - metrics configuration object
     * @param {string} mConfig.topic - metrics topic
     * @param {Object} rConfig - redis configuration object
     * @param {Object} ingestionConfig - ingestion extension configurations
     * @param {Object} s3Config - configuration to connect to S3 Client
     */
    constructor(zkConfig, kafkaConfig, qpConfig, mConfig, rConfig,
                ingestionConfig, s3Config) {
        this.zkConfig = zkConfig;
        this.kafkaConfig = kafkaConfig;
        this.qpConfig = qpConfig;
        this.mConfig = mConfig;
        this.rConfig = rConfig;
        this.ingestionConfig = ingestionConfig;
        this.s3Config = s3Config;

        this.log = new Logger('Backbeat:IngestionPopulator');

        // mongo client interface for finding ingestion buckets
        this._mongoClient = null;

        // list of active log readers
        this.logReaders = [];

        // list of updated log readers, if any
        this.logReadersUpdate = [];
        // metrics clients
        this._mProducer = null;
        this._mConsumer = null;
        // location details to be updated from config initManagement updates
        this._locationDetails = null;

        // provisioned ingestion log sources
        // i.e.: { an-ingestion-location: new ProvisionDispatcher() }
        this._provisionedIngestionSources = {};
    }

    /**
     * Open the ingestion populator
     *
     * @param {function} cb - callback function
     * @return {undefined}
     */
    open(cb) {
        this._loadExtension();
        async.series([
            next => this._setupMetricsClients(err => {
                if (err) {
                    this.log.error('error setting up metrics client', {
                        method: 'IngestionPopulator.open',
                        error: err,
                    });
                }
                return next(err);
            }),
            next => this._setupIngestionExtension(err => {
                if (err) {
                    this.log.error(
                        'error setting up ingestion extension', {
                            method: 'IngestionPopulator.open',
                            error: err,
                        });
                }
                return next(err);
            }),
            next => this._setupZookeeper(next),
            next => {
                this._mongoClient =
                    new MongoIngestionInterface(this.qpConfig.mongo);
                this._mongoClient.setup(err => {
                    if (err) {
                        this.log.error('error setting up mongo client', {
                            method: 'IngestionPopulator.open',
                            error: err,
                        });
                    }
                    return next(err);
                });
            },
        ], err => {
            if (err) {
                this.log.error('error starting up ingestion populator',
                    { method: 'IngestionPopulator.open',
                        error: err });
                return cb(err);
            }
            this._locationDetails = config.getLocations();
            config.on('locations-update', () => {
                this._locationDetails = config.getLocations();
            });
            return cb();
        });
    }

    _setupMetricsClients(cb) {
        // Metrics Consumer
        this._mConsumer = new MetricsConsumer(this.rConfig, this.mConfig,
            this.kafkaConfig);
        this._mConsumer.start();

        // Metrics Producer
        this._mProducer = new MetricsProducer(this.kafkaConfig, this.mConfig);
        this._mProducer.setupProducer(cb);
    }

    /**
     * Check mongo for new buckets and apply any updates
     * @param {Function} cb - callback(error)
     * @return {undefined}
     */
    applyUpdates(cb) {
        this._mongoClient.getIngestionBuckets((err, buckets) => {
            if (err) {
                return cb(err);
            }
            // Example buckets list:
            // [
            //     {
            //         name: 'my-zenko-bucket',
            //         ingestion: { Status: 'enabled' },
            //         locationConstraint: 'my-ring'
            //     },
            // ]

            // Example this._locationDetails
            // {
            //     'my-ring': {
            //         accessKey: ...,
            //         secretKey: ...,
            //         endpoint: 'http://10.100.1.128:8000',
            //         locationType: 'scality_s3',
            //         bucketName: 'source-bucket-1'
            //     }
            // }

            const sources = Object.keys(this._provisionedIngestionSources);

            return async.series([
                done => async.each(buckets, (bucket, next) => {
                    // zenko bucket name
                    const key = bucket.name;
                    if (!sources.includes(key)) {
                        // Add new ingestion source
                        const locationDetails =
                            this._locationDetails[bucket.locationConstraint];
                        const endpoint = url.parse(locationDetails.endpoint);
                        // TODO: will change for non-ring sources
                        const newSource = {
                            // target zenko bucket name
                            name: bucket.name,
                            // source bucket name to ingest from
                            bucket: locationDetails.bucketName,
                            // location storage name
                            prefix: bucket.locationConstraint,
                            // TODO: make configurable
                            cronRule: '*/5 * * * * *',
                            zookeeperSuffix: `/${bucket.name}`,
                            // source (s3c) endpoint
                            host: endpoint.hostname,
                            port: parseInt(endpoint.port, 10),
                            https: endpoint.protocol.slice(0, -1) === 'https',
                            type: locationDetails.locationType,
                            raftCount: 8,
                        };
                        return this.addNewLogSource(newSource, next);
                    }
                    const index = sources.findIndex(s => s === key);
                    // if an existing source, stop tracking it from
                    // `sources`
                    if (index >= 0) {
                        sources.splice(index, 1);
                    }
                    return next();
                }, done),
                // Any leftover `sources` have been removed
                // Close these logs..
                done => async.each(sources,
                    (source, next) => this.closeLogState(source, next),
                    done),
            ], cb);
        });
    }

    /**
     * add an Ingestion Reader
     * @param {Object} source - new source object
     * @param {Function} cb - callback(error)
     * @return {undefined}
     */
    addNewLogSource(source, cb) {
        this._extension.createZkPath(err => {
            if (err) {
                return cb(err);
            }
            const zookeeperUrlIngest =
                `${this.zkConfig.connectionString}` +
                `${this.ingestionConfig.zookeeperPath}/${source.name}`;
            const zkEndpointIngest = `${zookeeperUrlIngest}/raft-id-dispatcher`;
            this._subscribeToLogSourceDispatcher(zkEndpointIngest, source);
            return cb();
        }, source);
    }

    _subscribeToLogSourceDispatcher(zkEndpoint, bucketd) {
        const key = bucketd.name;
        if (this._provisionedIngestionSources[key]) {
            this.log.info('waiting to be provisioned a log source',
                          { zkEndpoint });
            return undefined;
        }

        const ingestionPath = this.ingestionConfig.zookeeperPath;
        // TODO: Remove/Replace ProvisionDispatcher
        const raftIdDispatcher =
            new ProvisionDispatcher({ connectionString: zkEndpoint });
        this._provisionedIngestionSources[key] = raftIdDispatcher;

        raftIdDispatcher.subscribe((err, items) => {
            if (err) {
                this.log.error(
                    'error when receiving log source provision list',
                    { zkEndpoint, error: err });
                // do not escalate error in order to allow retries.
                // provisioning log source may take a while
                return undefined;
            }
            if (items.length === 0) {
                this.log.info('no log source provisioned, idling',
                              { zkEndpoint });
            }
            const newRaftReaders = items.map(token => {
                const ingestionReader = new IngestionReader({
                    zkClient: this.zkClient,
                    kafkaConfig: this.kafkaConfig,
                    bucketdConfig: bucketd,
                    raftId: token,
                    logger: this.log,
                    extensions: [this._extension],
                    metricsProducer: this._mProducer,
                    ingestionPath,
                    qpConfig: this.qpConfig,
                    s3Config: this.s3Config,
                    bucket: bucketd.name,
                });
                return ingestionReader;
            });
            this.logReadersUpdate.push(...newRaftReaders);
            return undefined;
        });
        this.log.info('waiting to be provisioned a log source',
                      { zkEndpoint });
        return undefined;
    }

    _setupZookeeper(done) {
        const populatorZkPath = this.ingestionConfig.zookeeperPath;
        const zookeeperUrl =
            `${this.zkConfig.connectionString}${populatorZkPath}`;
        this.log.info('opening zookeeper connection for persisting ' +
                      'populator state',
                      { zookeeperUrl });
        this.zkClient = zookeeper.createClient(zookeeperUrl, {
            autoCreateNamespace: this.zkConfig.autoCreateNamespace,
        });
        this.zkClient.connect();
        this.zkClient.once('error', done);
        this.zkClient.once('ready', () => {
            // just in case there would be more 'error' events emitted
            this.zkClient.removeAllListeners('error');
            done();
        });
    }

    _loadExtension() {
        const index = require('../../extensions/ingestion/index.js');
        if (!index.queuePopulatorExtension) {
            this.log.fatal('Missing ingestion populator extension file');
            process.exit(1);
        }
        // eslint-disable-next-line new-cap
        const ext = new index.queuePopulatorExtension({
            config: this.ingestionConfig,
            logger: this.log,
        });
        ext.setZkConfig(this.zkConfig);
        this._extension = ext;

        this.log.info('ingestion extension is active');
    }

    _setupIngestionExtension(cb) {
        return this._extension.setupZookeeper(cb);
    }

    _setupUpdatedReaders(done) {
        const newReaders = this.logReadersUpdate;
        this.logReadersUpdate = [];
        async.each(newReaders, (logReader, cb) => logReader.setup(cb),
                   err => {
                       if (err) {
                           return done(err);
                       }
                       this.logReaders.push(...newReaders);
                       return done();
                   });
    }

    /**
     * Close all provisioned ingestion sources
     * @param {function} cb - callback function
     * @return {undefined}
     */
    close(cb) {
        return async.each(
            Object.keys(this._provisionedIngestionSources),
            (raftIdDispatcher, done) => {
                this.closeLogState(raftIdDispatcher, done);
            }, cb);
    }

    /**
     * remove and close an Ingestion Reader
     * @param {String} key - source key (zenko bucket name)
     * @param {Function} cb - callback(error)
     * @return {undefined}
     */
    closeLogState(key, cb) {
        const raftIdDispatcher = this._provisionedIngestionSources[key];
        if (raftIdDispatcher !== undefined) {
            delete this._provisionedIngestionSources[key];
            this._checkAndRemoveLogReader(key, this.logReaders);
            this._checkAndRemoveLogReader(key, this.logReadersUpdate);

            return raftIdDispatcher.unsubscribe(cb);
        }
        return process.nextTick(cb);
    }

    _checkAndRemoveLogReader(name, list) {
        const index = list.findIndex(lr =>
            lr.getTargetZenkoBucketName() === name);
        if (index !== -1) {
            list.splice(index, 1);
        }
    }

    _processLogEntries(params, done) {
        return async.map(
            this.logReaders,
            (logReader, cb) => logReader.processLogEntries(params, cb),
            done);
    }

    processLogEntries(params, done) {
        if (this.logReadersUpdate.length >= 1) {
            return this._setupUpdatedReaders(err => {
                if (err) {
                    return done(err);
                }
                return this._processLogEntries(params, done);
            });
        }
        return this._processLogEntries(params, done);
    }

    zkStatus() {
        return this.zkClient.getState();
    }
}

module.exports = IngestionPopulator;
<|endoftext|>


<commit_before>'use strict';

/*!
 * V4Fire Client Core
 * https://github.com/V4Fire/Client
 *
 * Released under the MIT license
 * https://github.com/V4Fire/Client/blob/master/LICENSE
 */

const
	$C = require('collection.js');

const
	{String} = require('sugar'),
	{args} = require('./helpers.webpack');

const
	fs = require('fs'),
	path = require('path'),
	findUp = require('find-up'),
	glob = require('glob'),
	mkdirp = require('mkdirp'),
	hasha = require('hasha'),
	pzlr = require('@pzlr/build-core');

const
	IN_PROCESS = 3;

/**
 * Builds entry points for WebPack by the specified parameters and returns a tree of dependencies
 *
 * @param {string} entries - path to base entry points
 * @param {string} blocks - path to a block folder
 * @param {string} lib - path to a lib folder
 * @param {string} output - output path
 * @param {string} cache - path to a cache folder
 * @param {string} assetsJSON - path to assets.json file
 * @returns {{processes, entry, dependencies, commonPacks}}
 */
module.exports = function ({entries, blocks, lib, output, cache, assetsJSON}) {
	let
		cacheFile;

	if (cache) {
		mkdirp.sync(cache);
		cacheFile = path.join(cache, 'graph.json');
		if (fs.existsSync(cacheFile)) {
			return require(cacheFile);
		}
	}

	function getCommonName(i) {
		return `common_${i}`;
	}

	const
		components = '!(core|models|libs|entries)/**/@(index|*.index).js';

	const files = [].concat(
		glob.sync(path.join(lib, '@v4fire/client/src', components)),
		glob.sync(path.join(blocks, components))
	);

	// Load index declarations of blocks
	const blockMap = $C(files).reduce((map, el) => {
		const
			decl = pzlr.declaration.parse(fs.readFileSync(el)),
			cwd = path.dirname(el);

		const
			nm = decl.name,
			url = (ext) => path.join(cwd, `${nm}.${ext}`);

		const
			logic = url('js'),
			style = url('styl'),
			tpl = url('ss'),
			html = url('ess');

		if (fs.existsSync(logic)) {
			decl.logic = logic;
		}

		if (fs.existsSync(style)) {
			decl.style = style;
		}

		if (fs.existsSync(tpl)) {
			decl.tpl = tpl;
		}

		if (fs.existsSync(html)) {
			decl.html = html;
		}

		if (decl.libs.length) {
			const root = findUp.sync('src', {cwd});
			$C(decl.libs).set((el) => path.join(root, el));
		}

		decl.src = el;

		const
			obj = map[nm];

		if (obj) {
			obj.styles = (obj.styles || []).concat(glob.sync(path.join(cwd, `${nm}_*.styl`)));

		} else {
			map[nm] = decl;
		}

		return map;
	}, {});

	// Parse --build arguments
	let entriesFilter;
	if (typeof args.build === 'string') {
		entriesFilter = $C(args.build.split(',')).reduce((map, el) => (map[el] = true, map), {});
		entriesFilter.index = true;
	}

	// Load entries
	const entriesList = $C(glob.sync(path.join(entries, '*.js'))).get((el) => {
		if (entriesFilter) {
			return entriesFilter[path.basename(el, '.js')];
		}

		return true;
	});

	// Build a graph with dependencies for each block
	function getBlocks(name, runtimeDependencies = [], compileDependencies = [], parents = []) {
		compileDependencies.unshift(name);
		runtimeDependencies.unshift(name);

		const
			block = blockMap[name];

		if (!block) {
			throw new Error(`Block "${name}" not exists!`);
		}

		$C(block.dependencies).forEach((block) => {
			block = path.basename(block.replace(/^@/, ''));
			compileDependencies.unshift(block);
			runtimeDependencies.unshift(block);
			getBlocks(block, runtimeDependencies, compileDependencies);
		});

		if (block.parent) {
			const parent = path.basename(block.parent);
			runtimeDependencies.unshift(parent);
			parents.unshift(parent);
			getBlocks(parent, runtimeDependencies, compileDependencies, parents);
		}

		block.compileDependencies = new Set(compileDependencies);
		block.runtimeDependencies = new Set(runtimeDependencies);
		block.parents = new Set(parents);

		return runtimeDependencies;
	}

	function isNodeModule(url) {
		return !path.isAbsolute(url) && /^[^./\\]/.test(url);
	}

	// Returns dependencies from a file with entries
	function getDependencies(dir, file, arr = []) {
		$C(file.split(/\r?\n|\r/)).forEach((el) => {
			if (!/^import\s+('|")(.*?)\1;?/.test(el)) {
				return;
			}

			const
				url = RegExp.$2,
				nodeModule = isNodeModule(url);

			if (nodeModule && /\bentries\b/.test(url) || /^\.\//.test(url)) {
				const
					d = nodeModule ? lib : dir;

				let
					f = path.join(d, `${url}.js`);

				if (!fs.existsSync(f)) {
					f = path.join(d, `${url}/index.js`)
				}

				getDependencies(path.dirname(f), fs.readFileSync(f, 'utf-8'), arr);

			} else {
				arr.push(path.join(dir, url));
			}
		});

		return arr;
	}

	// Parses a file with entries
	function extractFile(dir, file) {
		let list = [];

		$C(getDependencies(dir, file)).forEach((el) => {
			const
				name = path.basename(el, '.js'),
				block = blockMap[name];

			if (!pzlr.validators.blockName(name) || !block) {
				list.push(el);
				return;
			}

			list = list.concat(getBlocks(name));
		});

		return [...new Set(list)];
	}

	const
		packs = {},
		weights = {};

	// Load entries
	$C(entriesList).forEach((el) => {
		const
			list = packs[path.basename(el, '.js')] = extractFile(path.dirname(el), fs.readFileSync(el, 'utf-8'));

		$C(list).forEach((block) => {
			if (block in weights) {
				weights[block]++;

			} else {
				weights[block] = 0;
			}
		});
	});

	// Find common modules (weight > 1)
	const commonPacks = [];
	$C(weights).forEach((i, key) => {
		if (i > 1) {
			const pos = entriesList.length - i - 1;
			(commonPacks[pos] = commonPacks[pos] || new Set()).add(key);
		}
	});

	// Remove empty modules
	$C(commonPacks).remove((el) => !el);

	// Configure dependencies for each entry point
	const dependencies = {};
	$C(packs).forEach((list, name) => {
		const
			dep = dependencies[name] = dependencies[name] || new Set(),
			newList = [];

		$C(list).forEach((block) => {
			const
				pos = $C(commonPacks).one.search((set) => set.has(block));

			if (pos === null) {
				newList.push(block);

			} else {
				dep.add(pos);
			}
		});

		dependencies[name] = [...dep].sort((a, b) => a - b).map((i) => getCommonName(i));
		packs[name] = newList;
	});

	// Generate new entry points
	const entry = {};
	$C(commonPacks).forEach((list, i) => entry[getCommonName(i)] = list);
	$C(packs).forEach((list, name) => entry[name] = new Set(list));

	// Temporary folder for webpack entry points
	const tmpEntries = path.join(entries, 'tmp');
	mkdirp.sync(tmpEntries);

	function getUrl(url) {
		const
			r = (s) => s.replace(/\\/g, '/');

		if (isNodeModule(url)) {
			return r(url);
		}

		return r(path.relative(tmpEntries, url));
	}

	// Generate webpack entry points
	const processes = [{}];
	$C(entry).forEach((list, name) => {
		delete entry[name];

		// JS

		const
			blackName = /^[iv]-/,
			logicTaskName = `${name}.js`,
			logicFile = path.join(tmpEntries, logicTaskName);

		fs.writeFileSync(logicFile, $C(list).reduce((str, name) => {
			const
				block = blockMap[name];

			if (block) {
				$C(block.libs).forEach((el) => str += `require('${el}');\n`);
			}

			if (!block || block && block.logic) {
				const url = block ? block.logic : isNodeModule(name) ? name : path.resolve(tmpEntries, '../', name);
				str += `require('${getUrl(url)}');\n`;
			}

			return str;
		}, ''));

		entry[logicTaskName] = logicFile;
		processes[0][logicTaskName] = logicFile;

		// CSS

		const
			styleTaskName = `${name}$style`,
			styleFile = path.join(tmpEntries, `${name}.styl`);

		fs.writeFileSync(styleFile, $C(list).reduce((str, name) => {
			const
				block = blockMap[name];

			if (block && block.style && !blackName.test(name)) {
				const setUrl = (url) => {
					str += `@import "${url}"\n`;
				};

				setUrl(getUrl(block.style));
				$C(block.styles).forEach((url) => setUrl(url));

				if (/^[bp]-/.test(name)) {
					str +=
						`
.${name}
	extends($${String.camelize(name, false)})

`;
				}
			}

			return str;
		}, ''));

		let union = processes[processes.length - 1];
		if (processes.length === 1 || $C(union).length() > IN_PROCESS) {
			processes.push(union = {});
		}

		entry[styleTaskName] = union[styleTaskName] = styleFile;

		// TEMPLATES

		const
			tplTaskName = `${name}_tpl.js`,
			tplFile = path.join(tmpEntries, `${name}.ss${!args.fast ? '.js' : ''}`);

		fs.writeFileSync(tplFile, $C(list).reduce((str, name) => {
			const
				block = blockMap[name];

			if (block && block.tpl && !blackName.test(name)) {
				const url = getUrl(block.tpl);
				str += args.fast ? `- include '${url}'\n` : `Object.assign(TPLS, require('./${url}'));\n`;
			}

			return str;
		}, !args.fast ? 'window.TPLS = window.TPLS || {};\n' : ''));

		if (args.fast) {
			const tplRequireFileUrl = path.join(tmpEntries, tplTaskName);
			fs.writeFileSync(tplRequireFileUrl, `Object.assign(window.TPLS = window.TPLS || {}, require('./${getUrl(tplFile)}'));\n`);
			entry[tplTaskName] = union[tplTaskName] = tplRequireFileUrl;

		} else {
			entry[tplTaskName] = union[tplTaskName] = tplFile;
		}

		// HTML

		const
			htmlTaskName = `${name}_view`,
			htmlFile = path.join(tmpEntries, `${htmlTaskName}.html.js`);

		fs.writeFileSync(htmlFile, $C(list).reduce((str, name) => {
			const
				block = blockMap[name];

			if (block && block.html && !blackName.test(name)) {
				str += `require('./${getUrl(block.html)}');\n`;
			}

			return str;
		}, ''));

		entry[htmlTaskName] = union[htmlTaskName] = htmlFile;
	});

	const finalCommonPacks = ['index.js'].concat($C(commonPacks).map((el, i) => `${getCommonName(i)}.js`));
	mkdirp.sync(path.dirname(output));

	// Generate dependence declarations
	$C(dependencies).forEach((el, key) => {
		if (key !== 'index' && !el.includes('index')) {
			el.unshift('index');
		}

		el.push(key);

		const
			content = `ModuleDependencies.add("${key}", ${JSON.stringify(el)});`,
			name = `${key}.dependencies`;

		const src = output
			.replace(/\[name]/g, `${name}.js`)
			.replace(/\[hash:?(\d*)]/, (str, length) => {
				const res = hasha(content, {algorithm: 'md5'});
				return length ? res.substr(0, Number(length)) : res;
			});

		fs.writeFileSync(src, content);

		let fd;
		try {
			fd = fs.openSync(assetsJSON, 'r+');

		} catch (_) {
			fd = fs.openSync(assetsJSON, 'w+');
		}

		const
			file = fs.readFileSync(fd, 'utf-8'),
			assets = file ? JSON.parse(file) : {};

		assets[name] = {js: src};
		fs.writeFileSync(fd, JSON.stringify(assets));
		fs.closeSync(fd);
	});

	const res = {
		processes,
		entry,
		dependencies,
		commonPacks: finalCommonPacks
	};

	if (cacheFile) {
		fs.writeFileSync(cacheFile, JSON.stringify(res, null, 2));
		process.env.FROM_CACHE = true;
	}

	return res;
};
<commit_msg>Improved build<commit_after>'use strict';

/*!
 * V4Fire Client Core
 * https://github.com/V4Fire/Client
 *
 * Released under the MIT license
 * https://github.com/V4Fire/Client/blob/master/LICENSE
 */

const
	$C = require('collection.js');

const
	{String} = require('sugar'),
	{args} = require('./helpers.webpack');

const
	fs = require('fs'),
	path = require('path'),
	findUp = require('find-up'),
	glob = require('glob'),
	mkdirp = require('mkdirp'),
	hasha = require('hasha'),
	pzlr = require('@pzlr/build-core');

const
	IN_PROCESS = 3;

/**
 * Builds entry points for WebPack by the specified parameters and returns a tree of dependencies
 *
 * @param {string} entries - path to base entry points
 * @param {string} blocks - path to a block folder
 * @param {string} lib - path to a lib folder
 * @param {string} output - output path
 * @param {string} cache - path to a cache folder
 * @param {string} assetsJSON - path to assets.json file
 * @returns {{processes, entry, dependencies, commonPacks}}
 */
module.exports = function ({entries, blocks, lib, output, cache, assetsJSON}) {
	let
		cacheFile;

	if (cache) {
		mkdirp.sync(cache);
		cacheFile = path.join(cache, 'graph.json');
		if (fs.existsSync(cacheFile)) {
			return require(cacheFile);
		}
	}

	function getCommonName(i) {
		return `common_${i}`;
	}

	const
		components = '!(core|models|libs|entries)/**/@(index|*.index).js';

	const files = [].concat(
		glob.sync(path.join(lib, '@v4fire/client/src', components)),
		glob.sync(path.join(blocks, components))
	);

	// Load index declarations of blocks
	const blockMap = $C(files).reduce((map, el) => {
		const
			decl = pzlr.declaration.parse(fs.readFileSync(el)),
			cwd = path.dirname(el);

		const
			nm = decl.name,
			url = (ext) => path.join(cwd, `${nm}.${ext}`);

		const
			logic = url('js'),
			style = url('styl'),
			tpl = url('ss'),
			html = url('ess');

		if (fs.existsSync(logic)) {
			decl.logic = logic;
		}

		if (fs.existsSync(style)) {
			decl.style = style;
		}

		if (fs.existsSync(tpl)) {
			decl.tpl = tpl;
		}

		if (fs.existsSync(html)) {
			decl.html = html;
		}

		if (decl.libs.length) {
			const root = findUp.sync('src', {cwd});
			$C(decl.libs).set((el) => path.join(root, el));
		}

		decl.src = el;

		const
			obj = map[nm];

		if (obj && decl.mixin) {
			obj.styles = (obj.styles || []);

			const
				baseStyle = path.join(cwd, `${nm}.styl`);

			if (fs.existsSync(baseStyle)) {
				obj.styles.push(baseStyle);

			} else {
				obj.styles = obj.styles.concat(glob.sync(path.join(cwd, `${nm}_*.styl`)));
			}

		} else {
			map[nm] = decl;
		}

		return map;
	}, {});

	// Parse --build arguments
	let entriesFilter;
	if (typeof args.build === 'string') {
		entriesFilter = $C(args.build.split(',')).reduce((map, el) => (map[el] = true, map), {});
		entriesFilter.index = true;
	}

	// Load entries
	const entriesList = $C(glob.sync(path.join(entries, '*.js'))).get((el) => {
		if (entriesFilter) {
			return entriesFilter[path.basename(el, '.js')];
		}

		return true;
	});

	// Build a graph with dependencies for each block
	function getBlocks(name, runtimeDependencies = [], compileDependencies = [], parents = []) {
		compileDependencies.unshift(name);
		runtimeDependencies.unshift(name);

		const
			block = blockMap[name];

		if (!block) {
			throw new Error(`Block "${name}" not exists!`);
		}

		$C(block.dependencies).forEach((block) => {
			block = path.basename(block.replace(/^@/, ''));
			compileDependencies.unshift(block);
			runtimeDependencies.unshift(block);
			getBlocks(block, runtimeDependencies, compileDependencies);
		});

		if (block.parent) {
			const parent = path.basename(block.parent);
			runtimeDependencies.unshift(parent);
			parents.unshift(parent);
			getBlocks(parent, runtimeDependencies, compileDependencies, parents);
		}

		block.compileDependencies = new Set(compileDependencies);
		block.runtimeDependencies = new Set(runtimeDependencies);
		block.parents = new Set(parents);

		return runtimeDependencies;
	}

	function isNodeModule(url) {
		return !path.isAbsolute(url) && /^[^./\\]/.test(url);
	}

	// Returns dependencies from a file with entries
	function getDependencies(dir, file, arr = []) {
		$C(file.split(/\r?\n|\r/)).forEach((el) => {
			if (!/^import\s+('|")(.*?)\1;?/.test(el)) {
				return;
			}

			const
				url = RegExp.$2,
				nodeModule = isNodeModule(url);

			if (nodeModule && /\bentries\b/.test(url) || /^\.\//.test(url)) {
				const
					d = nodeModule ? lib : dir;

				let
					f = path.join(d, `${url}.js`);

				if (!fs.existsSync(f)) {
					f = path.join(d, `${url}/index.js`)
				}

				getDependencies(path.dirname(f), fs.readFileSync(f, 'utf-8'), arr);

			} else {
				arr.push(path.join(dir, url));
			}
		});

		return arr;
	}

	// Parses a file with entries
	function extractFile(dir, file) {
		let list = [];

		$C(getDependencies(dir, file)).forEach((el) => {
			const
				name = path.basename(el, '.js'),
				block = blockMap[name];

			if (!pzlr.validators.blockName(name) || !block) {
				list.push(el);
				return;
			}

			list = list.concat(getBlocks(name));
		});

		return [...new Set(list)];
	}

	const
		packs = {},
		weights = {};

	// Load entries
	$C(entriesList).forEach((el) => {
		const
			list = packs[path.basename(el, '.js')] = extractFile(path.dirname(el), fs.readFileSync(el, 'utf-8'));

		$C(list).forEach((block) => {
			if (block in weights) {
				weights[block]++;

			} else {
				weights[block] = 0;
			}
		});
	});

	// Find common modules (weight > 1)
	const commonPacks = [];
	$C(weights).forEach((i, key) => {
		if (i > 1) {
			const pos = entriesList.length - i - 1;
			(commonPacks[pos] = commonPacks[pos] || new Set()).add(key);
		}
	});

	// Remove empty modules
	$C(commonPacks).remove((el) => !el);

	// Configure dependencies for each entry point
	const dependencies = {};
	$C(packs).forEach((list, name) => {
		const
			dep = dependencies[name] = dependencies[name] || new Set(),
			newList = [];

		$C(list).forEach((block) => {
			const
				pos = $C(commonPacks).one.search((set) => set.has(block));

			if (pos === null) {
				newList.push(block);

			} else {
				dep.add(pos);
			}
		});

		dependencies[name] = [...dep].sort((a, b) => a - b).map((i) => getCommonName(i));
		packs[name] = newList;
	});

	// Generate new entry points
	const entry = {};
	$C(commonPacks).forEach((list, i) => entry[getCommonName(i)] = list);
	$C(packs).forEach((list, name) => entry[name] = new Set(list));

	// Temporary folder for webpack entry points
	const tmpEntries = path.join(entries, 'tmp');
	mkdirp.sync(tmpEntries);

	function getUrl(url) {
		const
			r = (s) => s.replace(/\\/g, '/');

		if (isNodeModule(url)) {
			return r(url);
		}

		return r(path.relative(tmpEntries, url));
	}

	// Generate webpack entry points
	const processes = [{}];
	$C(entry).forEach((list, name) => {
		delete entry[name];

		// JS

		const
			blackName = /^[iv]-/,
			logicTaskName = `${name}.js`,
			logicFile = path.join(tmpEntries, logicTaskName);

		fs.writeFileSync(logicFile, $C(list).reduce((str, name) => {
			const
				block = blockMap[name];

			if (block) {
				$C(block.libs).forEach((el) => str += `require('${el}');\n`);
			}

			if (!block || block && block.logic) {
				const url = block ? block.logic : isNodeModule(name) ? name : path.resolve(tmpEntries, '../', name);
				str += `require('${getUrl(url)}');\n`;
			}

			return str;
		}, ''));

		entry[logicTaskName] = logicFile;
		processes[0][logicTaskName] = logicFile;

		// CSS

		const
			styleTaskName = `${name}$style`,
			styleFile = path.join(tmpEntries, `${name}.styl`);

		fs.writeFileSync(styleFile, $C(list).reduce((str, name) => {
			const
				block = blockMap[name];

			if (block && block.style && !blackName.test(name)) {
				const setUrl = (url) => {
					str += `@import "${url}"\n`;
				};

				setUrl(getUrl(block.style));
				$C(block.styles).forEach((url) => setUrl(url));

				if (/^[bp]-/.test(name)) {
					str +=
						`
.${name}
	extends($${String.camelize(name, false)})

`;
				}
			}

			return str;
		}, ''));

		let union = processes[processes.length - 1];
		if (processes.length === 1 || $C(union).length() > IN_PROCESS) {
			processes.push(union = {});
		}

		entry[styleTaskName] = union[styleTaskName] = styleFile;

		// TEMPLATES

		const
			tplTaskName = `${name}_tpl.js`,
			tplFile = path.join(tmpEntries, `${name}.ss${!args.fast ? '.js' : ''}`);

		fs.writeFileSync(tplFile, $C(list).reduce((str, name) => {
			const
				block = blockMap[name];

			if (block && block.tpl && !blackName.test(name)) {
				const url = getUrl(block.tpl);
				str += args.fast ? `- include '${url}'\n` : `Object.assign(TPLS, require('./${url}'));\n`;
			}

			return str;
		}, !args.fast ? 'window.TPLS = window.TPLS || {};\n' : ''));

		if (args.fast) {
			const tplRequireFileUrl = path.join(tmpEntries, tplTaskName);
			fs.writeFileSync(tplRequireFileUrl, `Object.assign(window.TPLS = window.TPLS || {}, require('./${getUrl(tplFile)}'));\n`);
			entry[tplTaskName] = union[tplTaskName] = tplRequireFileUrl;

		} else {
			entry[tplTaskName] = union[tplTaskName] = tplFile;
		}

		// HTML

		const
			htmlTaskName = `${name}_view`,
			htmlFile = path.join(tmpEntries, `${htmlTaskName}.html.js`);

		fs.writeFileSync(htmlFile, $C(list).reduce((str, name) => {
			const
				block = blockMap[name];

			if (block && block.html && !blackName.test(name)) {
				str += `require('./${getUrl(block.html)}');\n`;
			}

			return str;
		}, ''));

		entry[htmlTaskName] = union[htmlTaskName] = htmlFile;
	});

	const finalCommonPacks = ['index.js'].concat($C(commonPacks).map((el, i) => `${getCommonName(i)}.js`));
	mkdirp.sync(path.dirname(output));

	// Generate dependence declarations
	$C(dependencies).forEach((el, key) => {
		if (key !== 'index' && !el.includes('index')) {
			el.unshift('index');
		}

		el.push(key);

		const
			content = `ModuleDependencies.add("${key}", ${JSON.stringify(el)});`,
			name = `${key}.dependencies`;

		const src = output
			.replace(/\[name]/g, `${name}.js`)
			.replace(/\[hash:?(\d*)]/, (str, length) => {
				const res = hasha(content, {algorithm: 'md5'});
				return length ? res.substr(0, Number(length)) : res;
			});

		fs.writeFileSync(src, content);

		let fd;
		try {
			fd = fs.openSync(assetsJSON, 'r+');

		} catch (_) {
			fd = fs.openSync(assetsJSON, 'w+');
		}

		const
			file = fs.readFileSync(fd, 'utf-8'),
			assets = file ? JSON.parse(file) : {};

		assets[name] = {js: src};
		fs.writeFileSync(fd, JSON.stringify(assets));
		fs.closeSync(fd);
	});

	const res = {
		processes,
		entry,
		dependencies,
		commonPacks: finalCommonPacks
	};

	if (cacheFile) {
		fs.writeFileSync(cacheFile, JSON.stringify(res, null, 2));
		process.env.FROM_CACHE = true;
	}

	return res;
};
<|endoftext|>


<commit_before>/*
 * This is based on the blueprint `slack-echo-command' provided by AWS.
 */
var AWS = require('aws-sdk');

require('toml-require').install();

// token will be initialized later.
var token;

var lambda = new AWS.Lambda();

/*
 * Configuration via TOML
 */
var config = require('./aaa_lambda.toml');

exports.handler = function (event, context) {
  if (token) {
    // Container reuse, simply process the event with the key in memory
    processEvent(event, context);
  } else {
    var encryptedBuf = new Buffer(config.dispatcher.encrypted_slack_token, 'base64');
    var cipherText = {CiphertextBlob: encryptedBuf};

    var kms = new AWS.KMS();
    kms.decrypt(cipherText, function (err, data) {
      if (err) {
        console.log('Decrypt error: ' + err);
        context.fail(err);
      } else {
        token = data.Plaintext.toString('ascii');
        processEvent(event, context);
      }
    });
  }
};

var processEvent = function(event, context) {
  var requestToken = event.token;
  if (requestToken !== token) {
    console.error('Request token (' + requestToken + ') does not match exptected');
    context.fail('Invalid request token');
  }

  for (var i in event) {
    event[i] = decodeURIComponent(event[i].replace(/\+/g, '%20'));
  }

  var user = event.user_name;
  var command = event.command;
  var channel = event.channel_name;
  var commandText = event.text;

  console.log('text: ' + commandText);

  var commandArgs = commandText.split(' ');

  // /letsencrypt [command] [email] [domain...]
  // /letsencrypt [command] [domain...]

  if (commandArgs.length < 2) {
    context.fail('invalid command args received: ' + commandArgs);
    return;
  }

  var aaaCommand = commandArgs.shift();

  var aaaEmail;
  if (commandArgs[0].indexOf('@') > 0) {
    aaaEmail = commandArgs.shift();
  } else {
    aaaEmail = config.executor.default_email;
  }

  var aaaDomains = commandArgs;

  var aaaReq = {
    'command': aaaCommand,
    'email': aaaEmail,
    'domains': aaaDomains,
    'event': event,
  };

  console.log('aaa-request: ' + JSON.stringify(aaaReq));

  lambda.invoke({
    'FunctionName': config.lambda.executor_function_name,
    'InvocationType': 'Event',
    'Payload': JSON.stringify(aaaReq),
  }, function(error) {
    if (error) {
      context.fail(JSON.stringify(resp));
    }

    var resp = {
      'response_type': 'in_channel',
      'text': "@" + user + ": request accepted. Please wait a moment...",
    };

    context.succeed(resp);
  });
};
<commit_msg>get rid of aaa_dispatcher.js<commit_after><|endoftext|>


<commit_before>/* feedreader.js
 *
 * This is the spec file that Jasmine will read and contains
 * all of the tests that will be run against your application.
 */

/* We're placing all of our tests within the $() function,
 * since some of these tests may require DOM elements. We want
 * to ensure they don't run until the DOM is ready.
 */
$(function() {
    "use strict";
    /* This is our first test suite - a test suite just contains
    * a related set of tests. This suite is all about the RSS
    * feeds definitions, the allFeeds variable in our application.
    */
    describe('RSS Feeds', function() {
        /* This is our first test - it tests to make sure that the
         * allFeeds variable has been defined and that it is not
         * empty. Experiment with this before you get started on
         * the rest of this project. What happens when you change
         * allFeeds in app.js to be an empty array and refresh the
         * page?
         */
        it('allFeeds variable has been defined and that it is not empty', function() {
            expect(allFeeds).toBeDefined();
            expect(allFeeds.length).not.toBe(0);
        });

        /* TODO: Write a test that loops through each feed
         * in the allFeeds object and ensures it has a URL defined
         * and that the URL is not empty.
         */

         it('all have URLs', function() {
            for(var x = 0; x < allFeeds.length; x++) {
                expect(allFeeds[x].url).toBeDefined();
                expect(allFeeds[x].url).not.toBe('');
            }
         });

        /* TODO: Write a test that loops through each feed
         * in the allFeeds object and ensures it has a name defined
         * and that the name is not empty.
         */

         it('all have names', function() {
            for(var x = 0; x < allFeeds.length; x++) {
                expect(allFeeds[x].name).toBeDefined();
                expect(allFeeds[x].name).not.toBe('');
            }
         });
    });

    /* TODO: Write a new test suite named "The menu" */
    describe('The menu', function() {
    var icon;
    beforeEach(function() {
       icon = $('.menu-icon-link');
    });
        /* TODO: Write a test that ensures the menu element is
         * hidden by default. You'll have to analyze the HTML and
         * the CSS to determine how we're performing the
         * hiding/showing of the menu element.
         */
         it('is hidden by default', function() {
            expect($("body").hasClass("menu-hidden")).toBe(true);
        });

         /* TODO: Write a test that ensures the menu changes
          * visibility when the menu icon is clicked. This test
          * should have two expectations: does the menu display when
          * clicked and does it hide when clicked again.
          */
        it('appears when clicked and disappears when clicked again', function() {
            if ($("body").hasClass("menu-hidden")) {
                icon.click();

                expect($("body").hasClass("menu-hidden")).toBe(false);
            }

            if (!$("body").hasClass("menu-hidden")) {
                icon.click();

                expect($("body").hasClass("menu-hidden")).toBe(true);
            }
        });
    });

    /* TODO: Write a new test suite named "Initial Entries" */

        /* TODO: Write a test that ensures when the loadFeed
         * function is called and completes its work, there is at least
         * a single .entry element within the .feed container.
         * Remember, loadFeed() is asynchronous so this test wil require
         * the use of Jasmine's beforeEach and asynchronous done() function.
         */
         describe('Initial Entries', function() {

        beforeEach(function(done) {
            loadFeed(0, function() {
                done();
            });
        });

        it('.feed container has at least a single .entry', function(done) {
            var length = $('.feed .entry')[0];
            expect(length).toBeGreaterThan('');
            done();
        });
    });

    /* TODO: Write a new test suite named "New Feed Selection"

        /* TODO: Write a test that ensures when a new feed is loaded
         * by the loadFeed function that the content actually changes.
         * Remember, loadFeed() is asynchronous.
         */
         describe('New Feed Selection', function() {
        var one, two;
        beforeEach(function(done) {
            loadFeed(1, function() {
                one = $('.feed').html();
                loadFeed(2, function() {
                    done();
                });
            });
        });

        afterEach(function() {
            loadFeed(0);
        });

        it('content changes when a new feed is loaded.', function() {
            expect(one).toBeDefined();
            two = $('.feed').html();
            expect(two).toBeDefined();
            expect(one).not.toEqual(two);
        });
    });

}());
<commit_msg>Fixed The Menu Testing Function<commit_after>/* feedreader.js
 *
 * This is the spec file that Jasmine will read and contains
 * all of the tests that will be run against your application.
 */

/* We're placing all of our tests within the $() function,
 * since some of these tests may require DOM elements. We want
 * to ensure they don't run until the DOM is ready.
 */
$(function() {
    "use strict";
    /* This is our first test suite - a test suite just contains
    * a related set of tests. This suite is all about the RSS
    * feeds definitions, the allFeeds variable in our application.
    */
    describe('RSS Feeds', function() {
        /* This is our first test - it tests to make sure that the
         * allFeeds variable has been defined and that it is not
         * empty. Experiment with this before you get started on
         * the rest of this project. What happens when you change
         * allFeeds in app.js to be an empty array and refresh the
         * page?
         */
        it('allFeeds variable has been defined and that it is not empty', function() {
            expect(allFeeds).toBeDefined();
            expect(allFeeds.length).not.toBe(0);
        });

        /* TODO: Write a test that loops through each feed
         * in the allFeeds object and ensures it has a URL defined
         * and that the URL is not empty.
         */

         it('all have URLs', function() {
            for(var x = 0; x < allFeeds.length; x++) {
                expect(allFeeds[x].url).toBeDefined();
                expect(allFeeds[x].url).not.toBe('');
            }
         });

        /* TODO: Write a test that loops through each feed
         * in the allFeeds object and ensures it has a name defined
         * and that the name is not empty.
         */

         it('all have names', function() {
            for(var x = 0; x < allFeeds.length; x++) {
                expect(allFeeds[x].name).toBeDefined();
                expect(allFeeds[x].name).not.toBe('');
            }
         });
    });

    /* TODO: Write a new test suite named "The menu" */
    describe('The menu', function() {
    var icon;
    beforeEach(function() {
       icon = $('.menu-icon-link');
    });
        /* TODO: Write a test that ensures the menu element is
         * hidden by default. You'll have to analyze the HTML and
         * the CSS to determine how we're performing the
         * hiding/showing of the menu element.
         */
         it('is hidden by default', function() {
            expect($("body").hasClass("menu-hidden")).toBe(true);
        });

         /* TODO: Write a test that ensures the menu changes
          * visibility when the menu icon is clicked. This test
          * should have two expectations: does the menu display when
          * clicked and does it hide when clicked again.
          */
        it('appears when clicked and disappears when clicked again', function() {
             var menuHidden = $('.menu-icon-link');

           menuHidden.click();
           expect($('body').hasClass('menu-hidden')).toEqual(false);
           menuHidden.click();
           expect($('body').hasClass('menu-hidden')).toEqual(true);
       });
   });

    /* TODO: Write a new test suite named "Initial Entries" */

        /* TODO: Write a test that ensures when the loadFeed
         * function is called and completes its work, there is at least
         * a single .entry element within the .feed container.
         * Remember, loadFeed() is asynchronous so this test wil require
         * the use of Jasmine's beforeEach and asynchronous done() function.
         */
         describe('Initial Entries', function() {

        beforeEach(function(done) {
            loadFeed(0, function() {
                done();
            });
        });

        it('.feed container has at least a single .entry', function(done) {
            var length = $('.feed .entry')[0];
            expect(length).toBeGreaterThan('');
            done();
        });
    });

    /* TODO: Write a new test suite named "New Feed Selection"

        /* TODO: Write a test that ensures when a new feed is loaded
         * by the loadFeed function that the content actually changes.
         * Remember, loadFeed() is asynchronous.
         */
         describe('New Feed Selection', function() {
        var one, two;
        beforeEach(function(done) {
            loadFeed(1, function() {
                one = $('.feed').html();
                loadFeed(2, function() {
                    done();
                });
            });
        });

        afterEach(function() {
            loadFeed(0);
        });

        it('content changes when a new feed is loaded.', function() {
            expect(one).toBeDefined();
            two = $('.feed').html();
            expect(two).toBeDefined();
            expect(one).not.toEqual(two);
        });
    });

}());
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Animated, ART, StyleSheet, Text, View } from 'react-native';

import Arc from './Shapes/Arc';
import withAnimation from './withAnimation';

const CIRCLE = Math.PI * 2;

const AnimatedSurface = Animated.createAnimatedComponent(ART.Surface);
const AnimatedArc = Animated.createAnimatedComponent(Arc);

const styles = StyleSheet.create({
  container: {
    backgroundColor: 'transparent',
    overflow: 'hidden',
  },
});

export class ProgressCircle extends Component {
  static propTypes = {
    animated: PropTypes.bool,
    borderColor: PropTypes.string,
    borderWidth: PropTypes.number,
    color: PropTypes.string,
    children: PropTypes.node,
    direction: PropTypes.oneOf(['clockwise', 'counter-clockwise']),
    fill: PropTypes.string,
    formatText: PropTypes.func,
    indeterminate: PropTypes.bool,
    progress: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.instanceOf(Animated.Value),
    ]),
    rotation: PropTypes.instanceOf(Animated.Value),
    showsText: PropTypes.bool,
    size: PropTypes.number,
    style: PropTypes.any,
    strokeCap: PropTypes.oneOf(['butt', 'square', 'round']),
    textStyle: PropTypes.any,
    thickness: PropTypes.number,
    unfilledColor: PropTypes.string,
    endAngle: PropTypes.number,
  };

  static defaultProps = {
    borderWidth: 1,
    color: 'rgba(0, 122, 255, 1)',
    direction: 'clockwise',
    formatText: progress => `${Math.round(progress * 100)}%`,
    progress: 0,
    showsText: false,
    size: 40,
    thickness: 3,
    endAngle: 0.9,
  };

  constructor(props, context) {
    super(props, context);

    this.progressValue = 0;
  }

  componentWillMount() {
    if (this.props.animated) {
      this.props.progress.addListener(event => {
        this.progressValue = event.value;
        if (this.props.showsText || this.progressValue === 1) {
          this.forceUpdate();
        }
      });
    }
  }

  render() {
    const {
      animated,
      borderColor,
      borderWidth,
      color,
      children,
      direction,
      fill,
      formatText,
      indeterminate,
      progress,
      rotation,
      showsText,
      size,
      style,
      strokeCap,
      textStyle,
      thickness,
      unfilledColor,
      endAngle,
      ...restProps
    } = this.props;

    const border = borderWidth || (indeterminate ? 1 : 0);

    const radius = size / 2 - border;
    const offset = {
      top: border,
      left: border,
    };
    const textOffset = border + thickness;
    const textSize = size - textOffset * 2;

    const Surface = rotation ? AnimatedSurface : ART.Surface;
    const Shape = animated ? AnimatedArc : Arc;
    const progressValue = animated ? this.progressValue : progress;
    const angle = animated
      ? Animated.multiply(progress, CIRCLE)
      : progress * CIRCLE;

    return (
      <View style={[styles.container, style]} {...restProps}>
        <Surface
          width={size}
          height={size}
          style={{
            transform: [
              {
                rotate:
                  indeterminate && rotation
                    ? rotation.interpolate({
                        inputRange: [0, 1],
                        outputRange: ['0deg', '360deg'],
                      })
                    : '0deg',
              },
            ],
          }}
        >
          {unfilledColor && progressValue !== 1 ? (
            <Shape
              fill={fill}
              radius={radius}
              offset={offset}
              startAngle={angle}
              endAngle={CIRCLE}
              direction={direction}
              stroke={unfilledColor}
              strokeWidth={thickness}
            />
          ) : (
            false
          )}
          {!indeterminate ? (
            <Shape
              fill={fill}
              radius={radius}
              offset={offset}
              startAngle={0}
              endAngle={angle}
              direction={direction}
              stroke={color}
              strokeCap={strokeCap}
              strokeWidth={thickness}
            />
          ) : (
            false
          )}
          {border ? (
            <Arc
              radius={size / 2}
              startAngle={0}
              endAngle={(indeterminate ? endAngle * 2 : 2) * Math.PI}
              stroke={borderColor || color}
              strokeCap={strokeCap}
              strokeWidth={border}
            />
          ) : (
            false
          )}
        </Surface>
        {!indeterminate && showsText ? (
          <View
            style={{
              position: 'absolute',
              left: textOffset,
              top: textOffset,
              width: textSize,
              height: textSize,
              borderRadius: textSize / 2,
              alignItems: 'center',
              justifyContent: 'center',
            }}
          >
            <Text
              style={[
                {
                  color,
                  fontSize: textSize / 4.5,
                  fontWeight: '300',
                },
                textStyle,
              ]}
            >
              {formatText(progressValue)}
            </Text>
          </View>
        ) : (
          false
        )}
        {children}
      </View>
    );
  }
}

export default withAnimation(ProgressCircle);
<commit_msg>Add allowFontScaling flag (#149)<commit_after>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Animated, ART, StyleSheet, Text, View } from 'react-native';

import Arc from './Shapes/Arc';
import withAnimation from './withAnimation';

const CIRCLE = Math.PI * 2;

const AnimatedSurface = Animated.createAnimatedComponent(ART.Surface);
const AnimatedArc = Animated.createAnimatedComponent(Arc);

const styles = StyleSheet.create({
  container: {
    backgroundColor: 'transparent',
    overflow: 'hidden',
  },
});

export class ProgressCircle extends Component {
  static propTypes = {
    animated: PropTypes.bool,
    borderColor: PropTypes.string,
    borderWidth: PropTypes.number,
    color: PropTypes.string,
    children: PropTypes.node,
    direction: PropTypes.oneOf(['clockwise', 'counter-clockwise']),
    fill: PropTypes.string,
    formatText: PropTypes.func,
    indeterminate: PropTypes.bool,
    progress: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.instanceOf(Animated.Value),
    ]),
    rotation: PropTypes.instanceOf(Animated.Value),
    showsText: PropTypes.bool,
    size: PropTypes.number,
    style: PropTypes.any,
    strokeCap: PropTypes.oneOf(['butt', 'square', 'round']),
    textStyle: PropTypes.any,
    thickness: PropTypes.number,
    unfilledColor: PropTypes.string,
    endAngle: PropTypes.number,
    allowFontScaling: PropTypes.bool,
  };

  static defaultProps = {
    borderWidth: 1,
    color: 'rgba(0, 122, 255, 1)',
    direction: 'clockwise',
    formatText: progress => `${Math.round(progress * 100)}%`,
    progress: 0,
    showsText: false,
    size: 40,
    thickness: 3,
    endAngle: 0.9,
    allowFontScaling: true,
  };

  constructor(props, context) {
    super(props, context);

    this.progressValue = 0;
  }

  componentWillMount() {
    if (this.props.animated) {
      this.props.progress.addListener(event => {
        this.progressValue = event.value;
        if (this.props.showsText || this.progressValue === 1) {
          this.forceUpdate();
        }
      });
    }
  }

  render() {
    const {
      animated,
      borderColor,
      borderWidth,
      color,
      children,
      direction,
      fill,
      formatText,
      indeterminate,
      progress,
      rotation,
      showsText,
      size,
      style,
      strokeCap,
      textStyle,
      thickness,
      unfilledColor,
      endAngle,
      allowFontScaling,
      ...restProps
    } = this.props;

    const border = borderWidth || (indeterminate ? 1 : 0);

    const radius = size / 2 - border;
    const offset = {
      top: border,
      left: border,
    };
    const textOffset = border + thickness;
    const textSize = size - textOffset * 2;

    const Surface = rotation ? AnimatedSurface : ART.Surface;
    const Shape = animated ? AnimatedArc : Arc;
    const progressValue = animated ? this.progressValue : progress;
    const angle = animated
      ? Animated.multiply(progress, CIRCLE)
      : progress * CIRCLE;

    return (
      <View style={[styles.container, style]} {...restProps}>
        <Surface
          width={size}
          height={size}
          style={{
            transform: [
              {
                rotate:
                  indeterminate && rotation
                    ? rotation.interpolate({
                        inputRange: [0, 1],
                        outputRange: ['0deg', '360deg'],
                      })
                    : '0deg',
              },
            ],
          }}
        >
          {unfilledColor && progressValue !== 1 ? (
            <Shape
              fill={fill}
              radius={radius}
              offset={offset}
              startAngle={angle}
              endAngle={CIRCLE}
              direction={direction}
              stroke={unfilledColor}
              strokeWidth={thickness}
            />
          ) : (
            false
          )}
          {!indeterminate ? (
            <Shape
              fill={fill}
              radius={radius}
              offset={offset}
              startAngle={0}
              endAngle={angle}
              direction={direction}
              stroke={color}
              strokeCap={strokeCap}
              strokeWidth={thickness}
            />
          ) : (
            false
          )}
          {border ? (
            <Arc
              radius={size / 2}
              startAngle={0}
              endAngle={(indeterminate ? endAngle * 2 : 2) * Math.PI}
              stroke={borderColor || color}
              strokeCap={strokeCap}
              strokeWidth={border}
            />
          ) : (
            false
          )}
        </Surface>
        {!indeterminate && showsText ? (
          <View
            style={{
              position: 'absolute',
              left: textOffset,
              top: textOffset,
              width: textSize,
              height: textSize,
              borderRadius: textSize / 2,
              alignItems: 'center',
              justifyContent: 'center',
            }}
          >
            <Text
              style={[
                {
                  color,
                  fontSize: textSize / 4.5,
                  fontWeight: '300',
                },
                textStyle,
              ]}
              allowFontScaling={allowFontScaling}
            >
              {formatText(progressValue)}
            </Text>
          </View>
        ) : (
          false
        )}
        {children}
      </View>
    );
  }
}

export default withAnimation(ProgressCircle);
<|endoftext|>


<commit_before>
    //Vars determining the center of the graph
    var offsetScreenX = 510;
    var offsetScreenY = 265;

    var valuesHash = {};
    var nBubbles = 0;

    jQuery.easing.def = "easeInOutCubic";

    function initializeGraph() {
      $(".innerBubble").live({
        mouseenter: function () {
          $(this).css("backgroundColor","#000000");
        },
        mouseleave: function () {
          $(this).css("backgroundColor",valuesHash[$(this).parent().attr('id')]["color"]);
        },
        click: function() {
          var url = valuesHash[$(this).parent().attr('id')]["children_json_url"];
          if (url == null) {
            return false;
          } else {
            goDeeper(url);
          }
        }
      });
    }

    function restartGraph() {
      $('div#graph_container').empty();
      valuesHash = {};
      createBubbles("/json/generated_data/autonomies_"+normalization[compare]+"_"+year+".json");
    }

    function createBubbles(url){
      $.getJSON(url, function(data) {
        $.each(data, function(key, val) {
          valuesHash[key] = val;
          nBubbles = nBubbles+1;
          $('#graph_container').append("

");
          $('#'+key).css("left",(offsetScreenX).toString()+"px");
          $('#'+key).css("top",(offsetScreenY).toString()+"px");
          $('#'+key).css("opacity","0");
          $('#'+key).find('.innerBubble').css("backgroundColor",val["color"]);
        });
      });
      setValue(url);
    }

    //changes the values on the graph
    function setValue(url){
      //TODO: VER SI PUEDO EVITAR LEER EL JSON (AL MENOS AL PRINCIPIO)
      $.getJSON(url, function(data) {
        $.each(data, function(key, v) {
          valuesHash[key] = v;
          updateBubble('#'+key,offsetScreenX+parseInt(v["x_coordinate"]),offsetScreenY+parseInt(v["y_coordinate"]),v["radius"],v["color"]);
        });
      });
    }

    //Function for update the values of the bubbles that are being visualized
    function updateBubble (bubble,x,y,val,c){
      var offset = Math.abs(parseInt($(bubble).find('.outerBubble').css('top')) + (parseInt($(bubble).find('.outerBubble').css('height')) - val) / 2)*-1;

      $(bubble).animate({
        left: x.toString(),
        top: y.toString(),
        opacity: "1"
      }, 1000);

      $(bubble).find('.outerBubble').animate({
          height: val.toString(),
          width: val.toString(),
          top: offset.toString(),
          left: offset.toString(),
        }, 1000);

      $(bubble).find('.innerBubble').animate({
          height: (val-10).toString(),
          width: (val-10).toString(),
          top: (offset + 5).toString(),
          left: (offset + 5).toString(),
          backgroundColor: c.toString()
        }, 1000);
    }

    function goDeeper(url){
      console.log("Going deep to " + url);
      for (key in valuesHash){
        //Destroy Bubbles
        destroyBubble(key, url);
      }
    }

    function destroyBubble(b, url){
      (parseInt($("#"+b).css("left")) < offsetScreenX) ? displacementX = "-=30px" : displacementX = "+=30px";
      (parseInt($("#"+b).css("top")) < offsetScreenY) ? displacementY = "-=30px" : displacementY = "+=30px";
      $("#"+key).animate({
        left: displacementX,
        top: displacementY,
        opacity: "0"
      }, 500, function(){
          //console.log("Removing "+b);
          $("#"+b).remove();
          nBubbles=nBubbles-1;
          if(nBubbles==0){
            createBubbles(url);
          }
        }
      );
    }<commit_msg>Log when doing hover in a ball<commit_after>
    //Vars determining the center of the graph
    var offsetScreenX = 510;
    var offsetScreenY = 265;

    var valuesHash = {};
    var nBubbles = 0;

    jQuery.easing.def = "easeInOutCubic";

    function initializeGraph() {
      $(".innerBubble").live({
        mouseenter: function () {
          $(this).css("backgroundColor","#000000");
        },
        mouseleave: function () {
          $(this).css("backgroundColor",valuesHash[$(this).parent().attr('id')]["color"]);
        },
        click: function() {
          var url = valuesHash[$(this).parent().attr('id')]["children_json_url"];
          if (url == null) {
            return false;
          } else {
            goDeeper(url);
          }
        },
        hover: function() {
          console.log($(this).parent().attr('id') + ': ' + valuesHash[$(this).parent().attr('id')]["x_coordinate"] + ', ' + valuesHash[$(this).parent().attr('id')]["y_coordinate"]);
        }
      });
    }

    function restartGraph() {
      $('div#graph_container').empty();
      valuesHash = {};
      createBubbles("/json/generated_data/autonomies_"+normalization[compare]+"_"+year+".json");
    }

    function createBubbles(url){
      $.getJSON(url, function(data) {
        $.each(data, function(key, val) {
          valuesHash[key] = val;
          nBubbles = nBubbles+1;
          $('#graph_container').append("

");
          $('#'+key).css("left",(offsetScreenX).toString()+"px");
          $('#'+key).css("top",(offsetScreenY).toString()+"px");
          $('#'+key).css("opacity","0");
          $('#'+key).find('.innerBubble').css("backgroundColor",val["color"]);
        });
      });
      setValue(url);
    }

    //changes the values on the graph
    function setValue(url){
      //TODO: VER SI PUEDO EVITAR LEER EL JSON (AL MENOS AL PRINCIPIO)
      $.getJSON(url, function(data) {
        $.each(data, function(key, v) {
          valuesHash[key] = v;
          updateBubble('#'+key,offsetScreenX+parseInt(v["x_coordinate"]),offsetScreenY+parseInt(v["y_coordinate"]),v["radius"],v["color"]);
        });
      });
    }

    //Function for update the values of the bubbles that are being visualized
    function updateBubble (bubble,x,y,val,c){
      var offset = Math.abs(parseInt($(bubble).find('.outerBubble').css('top')) + (parseInt($(bubble).find('.outerBubble').css('height')) - val) / 2)*-1;

      $(bubble).animate({
        left: x.toString(),
        top: y.toString(),
        opacity: "1"
      }, 1000);

      $(bubble).find('.outerBubble').animate({
          height: val.toString(),
          width: val.toString(),
          top: offset.toString(),
          left: offset.toString(),
        }, 1000);

      $(bubble).find('.innerBubble').animate({
          height: (val-10).toString(),
          width: (val-10).toString(),
          top: (offset + 5).toString(),
          left: (offset + 5).toString(),
          backgroundColor: c.toString()
        }, 1000);
    }

    function goDeeper(url){
      console.log("Going deep to " + url);
      for (key in valuesHash){
        //Destroy Bubbles
        destroyBubble(key, url);
      }
    }

    function destroyBubble(b, url){
      (parseInt($("#"+b).css("left")) < offsetScreenX) ? displacementX = "-=30px" : displacementX = "+=30px";
      (parseInt($("#"+b).css("top")) < offsetScreenY) ? displacementY = "-=30px" : displacementY = "+=30px";
      $("#"+key).animate({
        left: displacementX,
        top: displacementY,
        opacity: "0"
      }, 500, function(){
          //console.log("Removing "+b);
          $("#"+b).remove();
          nBubbles=nBubbles-1;
          if(nBubbles==0){
            createBubbles(url);
          }
        }
      );
    }<|endoftext|>


<commit_before>scal.addMethods({
/*------------------------------- INTERNAL -------------------------------*/
    _setupPlanner: function(planner) {
        this.planner = {};
        this._eventIndex = {deleted: []};
        planner.each(function(plan) {
            this._setPlanner(plan);
        }.bind(this));
    },
	_setPlanner: function(plan) {
        var plannerPeriod = Object.isArray(plan.period) ? plan.period : (typeof plan.period) == 'object' ? [plan.period] : [new Date(plan.period)];
        plannerPeriod.each(function(planDate){
            if(!this.planner[planDate]) { this.planner[planDate] = []; }
            if(Object.isString(plan.cls) && Object.isString(plan.label)){
                this.planner[planDate].push({ cls: ['dayboxevent', plan.cls], val: plan.label });
                this._updateEindex(plan.label, planDate);
            } else {
                var cls = Object.isArray(plan.cls) ? plan.cls : [plan.cls];
                var labels = Object.isArray(plan.label) ? plan.label : [plan.label];
                while(cls.size() > labels.size()) {
                    labels.push(labels[0]);
                    }
                while(labels.size() > cls.size()) {
                    cls.push(cls[0]);
                }
                $A($R(0, cls.size() - 1)).each(function(v){
                    var label = labels.size() > 1 ? labels.shift() : labels[0];
                    this.planner[planDate].push({cls: ['dayboxevent', cls.shift()], val: label});
                    this._updateEindex(label, planDate);
                }.bind(this));
            }
       }.bind(this));
    },
    _updateEindex: function(val,dt) {
        if(!this._eventIndex[val]) { this._eventIndex[val] = []; }
        this._eventIndex[val].push(dt);
    },
    _updatePlanner: function(day, el) {
        el.innerHTML = '';
        if(this.planner[day]) {
            this.planner[day].each(function(plan) {
                plan.cls.push('dayboxevent');
                el.insert(new Element('p',{'class': plan.cls.join(' ') }).update(plan.val));
            });
        }
    },
    _compareMonthYear: function(date1,date2) {
        return Object.isUndefined(['getMonth','getFullYear'].find(function(n){ return date1[n]() != date2[n](); }));
    },
/*------------------------------- PUBLIC -------------------------------*/
    getDatesByEvent: function(evt) {
        var dates = [];
        if(this._eventIndex[evt]){
            this._eventIndex[evt].each(function(d){
                this._eventIndex.deleted = this._eventIndex.deleted.without(d);
                dates.push(d);
            }.bind(this));
            return dates;
        }
        return false;
    },
    getEventsByDate: function(d) {
        var pevents = [];
        if(this.planner[d]) {
            this.planner[d].each(function(p){
                pevents.push(p.val);
            });
            return pevents;
        }
        return false;
    },
    getCurrentEvents: function() {
        // determine if we want just the current month or everything in current calendar view
        var currentMonth = arguments[0] ? this.currentdate.getMonth() : false;
        var plannerCheck = function(d) {
            if(currentMonth) { return this.planner[d] && (d.getMonth() == currentMonth) ? true : false; }
            else { return this.planner[d] ? true : false; }
        }.bind(this);
        var evts = [];
        this.dateRange.each(function(d,i) {
            if(plannerCheck(d)) {
                evts.push({dt: d, target: this.cells[i]});
            }
        }.bind(this));
        return evts;
    },
    removeEventsByDate: function(d) {
        if(this.planner[d]) {
            var cellIndex = this._getCellIndexByDate(d);
            if(Object.isNumber(arguments[1])) {
                var index = arguments[1];
                if(this.planner[d][index]){
                    delete this._eventIndex[this.planner[d][index]['val']];
                    this.planner[d].splice(index,1);
                }
                this.cells[cellIndex].select('.dayboxvalue p')[index].remove();
             } else {
                delete this.planner[d];
                this._eventIndex.deleted.push(d);
                this.cells[cellIndex].select('.dayboxvalue').invoke('remove');
             }
        } else {
            return false;
        }
    },
    getEventElementsByDate: function(d) {
        return this.getElementByDate(d).select('p.dayboxevent');
    },
    getEventElementsByWeek: function(week) {
        return this.getElementsByWeek(week).collect(function(e){ return e.select('p.dayboxevent'); });
    },
    getSelectedEvents: function() {
        var selectedElement = this.getSelectedElement();
        return Object.isUndefined(selectedElement) ? false : selectedElement.select('p.dayboxevent');
    },
    getTodaysEvents: function() {
        return this.getTodaysElement().select('p.dayboxevent');
    },
    updateDayValue: function(week,day,value){
        var planvalues = Object.isArray(value) ? value : [value];
        var planclasses = arguments[3] ? Object.isString(arguments[3]) ? [arguments[3]] : arguments[3] : [];
        week -= 1;
        day -= 1;
        this.dateRange.eachSlice(7, function(wk,i) {
            if(i == week) {
                this._setPlanner({period: wk[day], cls: planclasses.clone(), label: planvalues.clone()});
                throw $break
            }
        }.bind(this));
        planclasses.push('dayboxevent');
        var cellvalue = '.cal_day_'+week+'_'+day+'_value';
        var el = this.element.select(cellvalue)[0];
        planvalues.each(function(val) {
            el.insert(new Element('p',{'class': planclasses.join(' ')}).update(value));
        });
        return el;
    },
    setPlannerValue: function(year,month,day,value){
        var planvalues = Object.isArray(value) ? value : [value];
        var plannerdate = new Date();
        plannerdate.setHours(0,0,0,0);
        plannerdate.setYear(year);
        plannerdate.setMonth(month-1);
        plannerdate.setDate(day);
        var planclasses = arguments[4] ? Object.isString(arguments[4]) ? [arguments[4]] : arguments[4] : [];
        this._setPlanner({period: plannerdate, cls: planclasses.clone(), label: planvalues.clone()});
        planclasses.push('dayboxevent');
        if(!this._compareMonthYear(this.currentdate,plannerdate)) {
            return; // return nothing if plannerdate isn't in the current month
        }
        var cellIndex = this._getCellIndexByDate(plannerdate);
        var el = this.cells[cellIndex].select('.dayboxvalue')[0];
        planvalues.each(function(val) {
            el.insert(new Element('p',{'class': planclasses.join(' ')}).update(val));
        });
        return el;
    }
});
<commit_msg>added better date check for in-month events on startup.<commit_after>scal.addMethods({
/*------------------------------- INTERNAL -------------------------------*/
    _setupPlanner: function(planner) {
        this.planner = {};
        this._eventIndex = {deleted: []};
        planner.each(function(plan) {
            this._setPlanner(plan);
        }.bind(this));
    },
	_setPlanner: function(plan) {
        var plannerPeriod = Object.isArray(plan.period) ? plan.period : (typeof plan.period) == 'object' ? [plan.period] : [new Date(plan.period)];
        plannerPeriod.each(function(planDate){
            if(!this.planner[planDate]) { this.planner[planDate] = []; }
            if(Object.isString(plan.cls) && Object.isString(plan.label)){
                this.planner[planDate].push({ cls: ['dayboxevent', plan.cls], val: plan.label });
                this._updateEindex(plan.label, planDate);
            } else {
                var cls = Object.isArray(plan.cls) ? plan.cls : [plan.cls];
                var labels = Object.isArray(plan.label) ? plan.label : [plan.label];
                while(cls.size() > labels.size()) {
                    labels.push(labels[0]);
                    }
                while(labels.size() > cls.size()) {
                    cls.push(cls[0]);
                }
                $A($R(0, cls.size() - 1)).each(function(v){
                    var label = labels.size() > 1 ? labels.shift() : labels[0];
                    this.planner[planDate].push({cls: ['dayboxevent', cls.shift()], val: label});
                    this._updateEindex(label, planDate);
                }.bind(this));
            }
       }.bind(this));
    },
    _updateEindex: function(val,dt) {
        if(!this._eventIndex[val]) { this._eventIndex[val] = []; }
        this._eventIndex[val].push(dt);
    },
    _updatePlanner: function(day, el) {
        el.innerHTML = '';
        if(this.planner[day]) {
            this.planner[day].each(function(plan) {
                plan.cls.push('dayboxevent');
                el.insert(new Element('p',{'class': plan.cls.join(' ') }).update(plan.val));
            });
        }
    },
    _compareMonthYear: function(date1,date2) {
        return Object.isUndefined(['getMonth','getFullYear'].find(function(n){ return date1[n]() != date2[n](); }));
    },
/*------------------------------- PUBLIC -------------------------------*/
    getDatesByEvent: function(evt) {
        var dates = [];
        if(this._eventIndex[evt]){
            this._eventIndex[evt].each(function(d){
                this._eventIndex.deleted = this._eventIndex.deleted.without(d);
                dates.push(d);
            }.bind(this));
            return dates;
        }
        return false;
    },
    getEventsByDate: function(d) {
        var pevents = [];
        if(this.planner[d]) {
            this.planner[d].each(function(p){
                pevents.push(p.val);
            });
            return pevents;
        }
        return false;
    },
    getCurrentEvents: function() {
        // determine if we want just the current month or everything in current calendar view
        var currentMonth = arguments[0] ? this.currentdate.getMonth() : false;
        var plannerCheck = function(d) {
            if(currentMonth) { return this.planner[d] && (d.getMonth() == currentMonth) ? true : false; }
            else { return this.planner[d] ? true : false; }
        }.bind(this);
        var evts = [];
        this.dateRange.each(function(d,i) {
            if(plannerCheck(d)) {
                evts.push({dt: d, target: this.cells[i]});
            }
        }.bind(this));
        return evts;
    },
    removeEventsByDate: function(d) {
        if(this.planner[d]) {
            var cellIndex = this._getCellIndexByDate(d);
            if(Object.isNumber(arguments[1])) {
                var index = arguments[1];
                if(this.planner[d][index]){
                    delete this._eventIndex[this.planner[d][index]['val']];
                    this.planner[d].splice(index,1);
                }
                this.cells[cellIndex].select('.dayboxvalue p')[index].remove();
             } else {
                delete this.planner[d];
                this._eventIndex.deleted.push(d);
                this.cells[cellIndex].select('.dayboxvalue').invoke('remove');
             }
        } else {
            return false;
        }
    },
    getEventElementsByDate: function(d) {
        return this.getElementByDate(d).select('p.dayboxevent');
    },
    getEventElementsByWeek: function(week) {
        return this.getElementsByWeek(week).collect(function(e){ return e.select('p.dayboxevent'); });
    },
    getSelectedEvents: function() {
        var selectedElement = this.getSelectedElement();
        return Object.isUndefined(selectedElement) ? false : selectedElement.select('p.dayboxevent');
    },
    getTodaysEvents: function() {
        return this.getTodaysElement().select('p.dayboxevent');
    },
    updateDayValue: function(week,day,value){
        var planvalues = Object.isArray(value) ? value : [value];
        var planclasses = arguments[3] ? Object.isString(arguments[3]) ? [arguments[3]] : arguments[3] : [];
        week -= 1;
        day -= 1;
        this.dateRange.eachSlice(7, function(wk,i) {
            if(i == week) {
                this._setPlanner({period: wk[day], cls: planclasses.clone(), label: planvalues.clone()});
                throw $break
            }
        }.bind(this));
        planclasses.push('dayboxevent');
        var cellvalue = '.cal_day_'+week+'_'+day+'_value';
        var el = this.element.select(cellvalue)[0];
        planvalues.each(function(val) {
            el.insert(new Element('p',{'class': planclasses.join(' ')}).update(value));
        });
        return el;
    },
    setPlannerValue: function(year,month,day,value){
        var planvalues = Object.isArray(value) ? value : [value];
        var plannerdate = new Date();
        plannerdate.setHours(0,0,0,0);
        plannerdate.setYear(year);
        plannerdate.setMonth(month-1);
        plannerdate.setDate(day);
        var planclasses = arguments[4] ? Object.isString(arguments[4]) ? [arguments[4]] : arguments[4] : [];
        this._setPlanner({period: plannerdate, cls: planclasses.clone(), label: planvalues.clone()});
        planclasses.push('dayboxevent');
        if(this.dateRange.first() > plannerdate || this.dateRange.last() < plannerdate){
            return; // return nothing if plannerdate isn't in the current month
        }
        var cellIndex = this._getCellIndexByDate(plannerdate);
        var el = this.cells[cellIndex].select('.dayboxvalue')[0];
        planvalues.each(function(val) {
            el.insert(new Element('p',{'class': planclasses.join(' ')}).update(val));
        });
        return el;
    }
});
<|endoftext|>