<commit_before>/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import {debounce} from "lodash";
import classNames from 'classnames';
import React from 'react';
import PropTypes from "prop-types";
import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import Field from '../../elements/Field';
import AccessibleButton from '../../elements/AccessibleButton';

import { _t } from '../../../../languageHandler';

// Maximum acceptable size of a key file. It's 59 characters including the spaces we encode,
// so this should be plenty and allow for people putting extra whitespace in the file because
// maybe that's a thing people would do?
const KEY_FILE_MAX_SIZE = 128;

// Don't shout at the user that their key is invalid every time they type a key: wait a short time
const VALIDATION_THROTTLE_MS = 200;

/*
 * Access Secure Secret Storage by requesting the user's passphrase.
 */
export default class AccessSecretStorageDialog extends React.PureComponent {
    static propTypes = {
        // { passphrase, pubkey }
        keyInfo: PropTypes.object.isRequired,
        // Function from one of { passphrase, recoveryKey } -> boolean
        checkPrivateKey: PropTypes.func.isRequired,
    }

    constructor(props) {
        super(props);

        this._fileUpload = React.createRef();

        this.state = {
            recoveryKey: "",
            recoveryKeyValid: null,
            recoveryKeyCorrect: null,
            recoveryKeyFileError: null,
            forceRecoveryKey: false,
            passPhrase: '',
            keyMatches: null,
        };
    }

    _onCancel = () => {
        this.props.onFinished(false);
    }

    _onUseRecoveryKeyClick = () => {
        this.setState({
            forceRecoveryKey: true,
        });
    }

    _validateRecoveryKeyOnChange = debounce(() => {
        this._validateRecoveryKey();
    }, VALIDATION_THROTTLE_MS);

    async _validateRecoveryKey() {
        if (this.state.recoveryKey === '') {
            this.setState({
                recoveryKeyValid: null,
                recoveryKeyCorrect: null,
            });
            return;
        }

        try {
            const cli = MatrixClientPeg.get();
            const decodedKey = cli.keyBackupKeyFromRecoveryKey(this.state.recoveryKey);
            const correct = await cli.checkSecretStorageKey(
                decodedKey, this.props.keyInfo,
            );
            this.setState({
                recoveryKeyValid: true,
                recoveryKeyCorrect: correct,
            });
        } catch (e) {
            this.setState({
                recoveryKeyValid: false,
                recoveryKeyCorrect: false,
            });
        }
    }

    _onRecoveryKeyChange = (e) => {
        this.setState({
            recoveryKey: e.target.value,
            recoveryKeyFileError: null,
        });

        // also clear the file upload control so that the user can upload the same file
        // the did before (otherwise the onchange wouldn't fire)
        if (this._fileUpload.current) this._fileUpload.current.value = null;

        // We don't use Field's validation here because a) we want it in a separate place rather
        // than in a tooltip and b) we want it to display feedback based on the uploaded file
        // as well as the text box. Ideally we would refactor Field's validation logic so we could
        // re-use some of it.
        this._validateRecoveryKeyOnChange();
    }

    _onRecoveryKeyFileChange = async e => {
        if (e.target.files.length === 0) return;

        const f = e.target.files[0];

        if (f.size > KEY_FILE_MAX_SIZE) {
            this.setState({
                recoveryKeyFileError: true,
                recoveryKeyCorrect: false,
                recoveryKeyValid: false,
            });
        } else {
            const contents = await f.text();
            // test it's within the base58 alphabet. We could be more strict here, eg. require the
            // right number of characters, but it's really just to make sure that what we're reading is
            // text because we'll put it in the text field.
            if (/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\s]+$/.test(contents)) {
                this.setState({
                    recoveryKeyFileError: null,
                    recoveryKey: contents.trim(),
                });
                this._validateRecoveryKey();
            } else {
                this.setState({
                    recoveryKeyFileError: true,
                    recoveryKeyCorrect: false,
                    recoveryKeyValid: false,
                    recoveryKey: '',
                });
            }
        }
    }

    _onRecoveryKeyFileUploadClick = () => {
        this._fileUpload.current.click();
    }

    _onPassPhraseNext = async (e) => {
        e.preventDefault();

        if (this.state.passPhrase.length <= 0) return;

        this.setState({ keyMatches: null });
        const input = { passphrase: this.state.passPhrase };
        const keyMatches = await this.props.checkPrivateKey(input);
        if (keyMatches) {
            this.props.onFinished(input);
        } else {
            this.setState({ keyMatches });
        }
    }

    _onRecoveryKeyNext = async (e) => {
        e.preventDefault();

        if (!this.state.recoveryKeyValid) return;

        this.setState({ keyMatches: null });
        const input = { recoveryKey: this.state.recoveryKey };
        const keyMatches = await this.props.checkPrivateKey(input);
        if (keyMatches) {
            this.props.onFinished(input);
        } else {
            this.setState({ keyMatches });
        }
    }

    _onPassPhraseChange = (e) => {
        this.setState({
            passPhrase: e.target.value,
            keyMatches: null,
        });
    }

    getKeyValidationText() {
        if (this.state.recoveryKeyFileError) {
            return _t("Wrong file type");
        } else if (this.state.recoveryKeyCorrect) {
            return _t("Looks good!");
        } else if (this.state.recoveryKeyValid) {
            return _t("Wrong Recovery Key");
        } else if (this.state.recoveryKeyValid === null) {
            return '';
        } else {
            return _t("Invalid Recovery Key");
        }
    }

    render() {
        const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');

        const hasPassphrase = (
            this.props.keyInfo &&
            this.props.keyInfo.passphrase &&
            this.props.keyInfo.passphrase.salt &&
            this.props.keyInfo.passphrase.iterations
        );

        let content;
        let title;
        let titleClass;
        if (hasPassphrase && !this.state.forceRecoveryKey) {
            const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
            const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
            title = _t("Security Phrase");
            titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_securePhraseTitle'];

            let keyStatus;
            if (this.state.keyMatches === false) {
                keyStatus =

                    {"\uD83D\uDC4E "}{_t(
                        "Unable to access secret storage. " +
                        "Please verify that you entered the correct recovery passphrase.",
                    )}

;
            } else {
                keyStatus =
;
            }

            content =

{_t(
                    "Enter your Security Phrase or <button>Use your Security Key</button> to continue.", {},
                    {
                        button: s => <AccessibleButton className="mx_linkButton"
                            element="span"
                            onClick={this._onUseRecoveryKeyClick}
                        >
                            {s}
                        </AccessibleButton>,
                    },
                )}

                <form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onPassPhraseNext}>
                    <input
                        type="password"
                        className="mx_AccessSecretStorageDialog_passPhraseInput"
                        onChange={this._onPassPhraseChange}
                        value={this.state.passPhrase}
                        autoFocus={true}
                        autoComplete="new-password"
                        placeholder={_t("Security Phrase")}
                    />
                    {keyStatus}
                    <DialogButtons
                        primaryButton={_t('Continue')}
                        onPrimaryButtonClick={this._onPassPhraseNext}
                        hasCancel={true}
                        onCancel={this._onCancel}
                        focus={false}
                        primaryDisabled={this.state.passPhrase.length === 0}
                    />
                </form>

;
        } else {
            title = _t("Security Key");
            titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_secureBackupTitle'];
            const DialogButtons = sdk.getComponent('views.elements.DialogButtons');

            const feedbackClasses = classNames({
                'mx_AccessSecretStorageDialog_recoveryKeyFeedback': true,
                'mx_AccessSecretStorageDialog_recoveryKeyFeedback_valid': this.state.recoveryKeyCorrect === true,
                'mx_AccessSecretStorageDialog_recoveryKeyFeedback_invalid': this.state.recoveryKeyCorrect === false,
            });
            const recoveryKeyFeedback =

                {this.getKeyValidationText()}

;

            content =

{_t("Use your Security Key to continue.")}

                <form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onRecoveryKeyNext} spellCheck={false}>

                            <Field
                                type="text"
                                label={_t('Security Key')}
                                value={this.state.recoveryKey}
                                onChange={this._onRecoveryKeyChange}
                                forceValidity={this.state.recoveryKeyCorrect}
                            />

                            {_t("or")}

                            <input type="file"
                                className="mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput"
                                ref={this._fileUpload}
                                onChange={this._onRecoveryKeyFileChange}
                            />
                            <AccessibleButton kind="primary" onClick={this._onRecoveryKeyFileUploadClick}>
                                {_t("Upload")}
                            </AccessibleButton>

                    {recoveryKeyFeedback}
                    <DialogButtons
                        primaryButton={_t('Continue')}
                        onPrimaryButtonClick={this._onRecoveryKeyNext}
                        hasCancel={true}
                        cancelButton={_t("Go Back")}
                        cancelButtonClass='danger'
                        onCancel={this._onCancel}
                        focus={false}
                        primaryDisabled={!this.state.recoveryKeyValid}
                    />
                </form>

;
        }

        return (
            <BaseDialog className='mx_AccessSecretStorageDialog'
                onFinished={this.props.onFinished}
                title={title}
                titleClass={titleClass}
            >

                {content}

            </BaseDialog>
        );
    }
}
<commit_msg>Disable autocompletion on security key input during login<commit_after>/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import {debounce} from "lodash";
import classNames from 'classnames';
import React from 'react';
import PropTypes from "prop-types";
import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import Field from '../../elements/Field';
import AccessibleButton from '../../elements/AccessibleButton';

import { _t } from '../../../../languageHandler';

// Maximum acceptable size of a key file. It's 59 characters including the spaces we encode,
// so this should be plenty and allow for people putting extra whitespace in the file because
// maybe that's a thing people would do?
const KEY_FILE_MAX_SIZE = 128;

// Don't shout at the user that their key is invalid every time they type a key: wait a short time
const VALIDATION_THROTTLE_MS = 200;

/*
 * Access Secure Secret Storage by requesting the user's passphrase.
 */
export default class AccessSecretStorageDialog extends React.PureComponent {
    static propTypes = {
        // { passphrase, pubkey }
        keyInfo: PropTypes.object.isRequired,
        // Function from one of { passphrase, recoveryKey } -> boolean
        checkPrivateKey: PropTypes.func.isRequired,
    }

    constructor(props) {
        super(props);

        this._fileUpload = React.createRef();

        this.state = {
            recoveryKey: "",
            recoveryKeyValid: null,
            recoveryKeyCorrect: null,
            recoveryKeyFileError: null,
            forceRecoveryKey: false,
            passPhrase: '',
            keyMatches: null,
        };
    }

    _onCancel = () => {
        this.props.onFinished(false);
    }

    _onUseRecoveryKeyClick = () => {
        this.setState({
            forceRecoveryKey: true,
        });
    }

    _validateRecoveryKeyOnChange = debounce(() => {
        this._validateRecoveryKey();
    }, VALIDATION_THROTTLE_MS);

    async _validateRecoveryKey() {
        if (this.state.recoveryKey === '') {
            this.setState({
                recoveryKeyValid: null,
                recoveryKeyCorrect: null,
            });
            return;
        }

        try {
            const cli = MatrixClientPeg.get();
            const decodedKey = cli.keyBackupKeyFromRecoveryKey(this.state.recoveryKey);
            const correct = await cli.checkSecretStorageKey(
                decodedKey, this.props.keyInfo,
            );
            this.setState({
                recoveryKeyValid: true,
                recoveryKeyCorrect: correct,
            });
        } catch (e) {
            this.setState({
                recoveryKeyValid: false,
                recoveryKeyCorrect: false,
            });
        }
    }

    _onRecoveryKeyChange = (e) => {
        this.setState({
            recoveryKey: e.target.value,
            recoveryKeyFileError: null,
        });

        // also clear the file upload control so that the user can upload the same file
        // the did before (otherwise the onchange wouldn't fire)
        if (this._fileUpload.current) this._fileUpload.current.value = null;

        // We don't use Field's validation here because a) we want it in a separate place rather
        // than in a tooltip and b) we want it to display feedback based on the uploaded file
        // as well as the text box. Ideally we would refactor Field's validation logic so we could
        // re-use some of it.
        this._validateRecoveryKeyOnChange();
    }

    _onRecoveryKeyFileChange = async e => {
        if (e.target.files.length === 0) return;

        const f = e.target.files[0];

        if (f.size > KEY_FILE_MAX_SIZE) {
            this.setState({
                recoveryKeyFileError: true,
                recoveryKeyCorrect: false,
                recoveryKeyValid: false,
            });
        } else {
            const contents = await f.text();
            // test it's within the base58 alphabet. We could be more strict here, eg. require the
            // right number of characters, but it's really just to make sure that what we're reading is
            // text because we'll put it in the text field.
            if (/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\s]+$/.test(contents)) {
                this.setState({
                    recoveryKeyFileError: null,
                    recoveryKey: contents.trim(),
                });
                this._validateRecoveryKey();
            } else {
                this.setState({
                    recoveryKeyFileError: true,
                    recoveryKeyCorrect: false,
                    recoveryKeyValid: false,
                    recoveryKey: '',
                });
            }
        }
    }

    _onRecoveryKeyFileUploadClick = () => {
        this._fileUpload.current.click();
    }

    _onPassPhraseNext = async (e) => {
        e.preventDefault();

        if (this.state.passPhrase.length <= 0) return;

        this.setState({ keyMatches: null });
        const input = { passphrase: this.state.passPhrase };
        const keyMatches = await this.props.checkPrivateKey(input);
        if (keyMatches) {
            this.props.onFinished(input);
        } else {
            this.setState({ keyMatches });
        }
    }

    _onRecoveryKeyNext = async (e) => {
        e.preventDefault();

        if (!this.state.recoveryKeyValid) return;

        this.setState({ keyMatches: null });
        const input = { recoveryKey: this.state.recoveryKey };
        const keyMatches = await this.props.checkPrivateKey(input);
        if (keyMatches) {
            this.props.onFinished(input);
        } else {
            this.setState({ keyMatches });
        }
    }

    _onPassPhraseChange = (e) => {
        this.setState({
            passPhrase: e.target.value,
            keyMatches: null,
        });
    }

    getKeyValidationText() {
        if (this.state.recoveryKeyFileError) {
            return _t("Wrong file type");
        } else if (this.state.recoveryKeyCorrect) {
            return _t("Looks good!");
        } else if (this.state.recoveryKeyValid) {
            return _t("Wrong Recovery Key");
        } else if (this.state.recoveryKeyValid === null) {
            return '';
        } else {
            return _t("Invalid Recovery Key");
        }
    }

    render() {
        const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');

        const hasPassphrase = (
            this.props.keyInfo &&
            this.props.keyInfo.passphrase &&
            this.props.keyInfo.passphrase.salt &&
            this.props.keyInfo.passphrase.iterations
        );

        let content;
        let title;
        let titleClass;
        if (hasPassphrase && !this.state.forceRecoveryKey) {
            const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
            const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
            title = _t("Security Phrase");
            titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_securePhraseTitle'];

            let keyStatus;
            if (this.state.keyMatches === false) {
                keyStatus =

                    {"\uD83D\uDC4E "}{_t(
                        "Unable to access secret storage. " +
                        "Please verify that you entered the correct recovery passphrase.",
                    )}

;
            } else {
                keyStatus =
;
            }

            content =

{_t(
                    "Enter your Security Phrase or <button>Use your Security Key</button> to continue.", {},
                    {
                        button: s => <AccessibleButton className="mx_linkButton"
                            element="span"
                            onClick={this._onUseRecoveryKeyClick}
                        >
                            {s}
                        </AccessibleButton>,
                    },
                )}

                <form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onPassPhraseNext}>
                    <input
                        type="password"
                        className="mx_AccessSecretStorageDialog_passPhraseInput"
                        onChange={this._onPassPhraseChange}
                        value={this.state.passPhrase}
                        autoFocus={true}
                        autoComplete="new-password"
                        placeholder={_t("Security Phrase")}
                    />
                    {keyStatus}
                    <DialogButtons
                        primaryButton={_t('Continue')}
                        onPrimaryButtonClick={this._onPassPhraseNext}
                        hasCancel={true}
                        onCancel={this._onCancel}
                        focus={false}
                        primaryDisabled={this.state.passPhrase.length === 0}
                    />
                </form>

;
        } else {
            title = _t("Security Key");
            titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_secureBackupTitle'];
            const DialogButtons = sdk.getComponent('views.elements.DialogButtons');

            const feedbackClasses = classNames({
                'mx_AccessSecretStorageDialog_recoveryKeyFeedback': true,
                'mx_AccessSecretStorageDialog_recoveryKeyFeedback_valid': this.state.recoveryKeyCorrect === true,
                'mx_AccessSecretStorageDialog_recoveryKeyFeedback_invalid': this.state.recoveryKeyCorrect === false,
            });
            const recoveryKeyFeedback =

                {this.getKeyValidationText()}

;

            content =

{_t("Use your Security Key to continue.")}

                <form
                    className="mx_AccessSecretStorageDialog_primaryContainer"
                    onSubmit={this._onRecoveryKeyNext}
                    spellCheck={false}
                    autoComplete="off"
                >

                            <Field
                                type="text"
                                label={_t('Security Key')}
                                value={this.state.recoveryKey}
                                onChange={this._onRecoveryKeyChange}
                                forceValidity={this.state.recoveryKeyCorrect}
                                autoComplete="off"
                            />

                            {_t("or")}

                            <input type="file"
                                className="mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput"
                                ref={this._fileUpload}
                                onChange={this._onRecoveryKeyFileChange}
                            />
                            <AccessibleButton kind="primary" onClick={this._onRecoveryKeyFileUploadClick}>
                                {_t("Upload")}
                            </AccessibleButton>

                    {recoveryKeyFeedback}
                    <DialogButtons
                        primaryButton={_t('Continue')}
                        onPrimaryButtonClick={this._onRecoveryKeyNext}
                        hasCancel={true}
                        cancelButton={_t("Go Back")}
                        cancelButtonClass='danger'
                        onCancel={this._onCancel}
                        focus={false}
                        primaryDisabled={!this.state.recoveryKeyValid}
                    />
                </form>

;
        }

        return (
            <BaseDialog className='mx_AccessSecretStorageDialog'
                onFinished={this.props.onFinished}
                title={title}
                titleClass={titleClass}
            >

                {content}

            </BaseDialog>
        );
    }
}
<|endoftext|>


<commit_before>var stats = function(dbot){
    var userStats = dbot.db.userStats;
    var chanStats = dbot.db.chanStats;

    //TODO(samstudio8) @reality is officially deprecating snippets.js,
    //cannot rely on filter or numberFormat
    var _ = require('underscore')._;
    var structure = require('./structures');

    this.listener = function(event){

        // Ignore command messages
        if(event.message[0] == "~"){
            return;
        }

        // Check server dbKeys exist
        if(!userStats.hasOwnProperty(event.server)){
            userStats[event.server] = {}
        }
        if(!chanStats.hasOwnProperty(event.server)){
            chanStats[event.server] = {}
        }

        var user = dbot.api.users.resolveUser(event.server, event.user, true).toLowerCase();
        var num_words = event.message.split(" ").filter(function(w, i, array) { return w.length > 0; }).length

        // User-centric Stats
        if(!userStats[event.server].hasOwnProperty(user)){
            userStats[event.server][user] = {}
        }
        if(!userStats[event.server][user].hasOwnProperty(event.channel)){
            userStats[event.server][user][event.channel] = {}
            _.defaults(userStats[event.server][user][event.channel], structure.fieldFactoryOutlet("user", this.api));
        }
        userStats[event.server][user][event.channel]["freq"].add({"day": event.time.getDay(),
                                                                  "hour": event.time.getHours(),
                                                                  "inc": 1
        });
        userStats[event.server][user][event.channel]["lines"].add(1);
        userStats[event.server][user][event.channel]["words"].add(num_words);

        // Channel-centric Stats
        if(!chanStats[event.server].hasOwnProperty(event.channel)){
            chanStats[event.server][event.channel] = {}
            _.defaults(chanStats[event.server][event.channel], structure.fieldFactoryOutlet("chan", this.api));
        }
        chanStats[event.server][event.channel]["freq"].add({"day": event.time.getDay(),
                                                            "hour": event.time.getHours(),
                                                            "inc": 1
        });
        chanStats[event.server][event.channel]["lines"].add(1);
        chanStats[event.server][event.channel]["words"].add(num_words);

        // Check whether the line includes any mentions
        if(dbot.db.hasOwnProperty("knownUsers")){
            var cat = dbot.db.knownUsers[event.server].users.concat(
                    Object.keys(dbot.db.knownUsers[event.server].aliases));
            for (var i = 0; i < cat.length; i++){
                var name = cat[i];
                var mentioned = dbot.api.users.resolveUser(event.server, name, true).toLowerCase();
                if(user != mentioned
                        && userStats[event.server].hasOwnProperty(mentioned)
                        && userStats[event.server][mentioned].hasOwnProperty(event.channel)){
                    var toMatch = "( |^)"+name.escape().toLowerCase()+":?(?=\\s|$)";
                    if(event.message.toLowerCase().search(toMatch) > -1){
                        userStats[event.server][user][event.channel]["out_mentions"].add({"mentioned": mentioned,
                                                                                "inc": 1});
                        userStats[event.server][mentioned][event.channel]["in_mentions"].add(1);
                    }
                }
            }
        }
    }.bind(this);
    this.on = 'PRIVMSG';

    this.onLoad = function(){
        //TODO(samstudio8): Also delete no longer required fields

        // For some reason the scope of this changes and prevents access to the
        // API inside of the underscore loops, I think it's because I need to
        // pass in a context, but this works anyway.
        var api = this.api;

        //TODO(samstudio8) There must be a nicer way to do this...
        _.each(userStats, function(server, serverName){
            _.each(userStats[serverName], function(user, userName){
                _.each(userStats[serverName][userName], function(chan, chanName){
                    var fieldFactoryUserProduct = structure.fieldFactoryOutlet("user", api);
                    _.defaults(userStats[serverName][userName][chanName], fieldFactoryUserProduct);
                    //TODO(samstudio8) Doom, it was going so well.
                    _.each(userStats[serverName][userName][chanName], function(field, fieldName){
                        _.defaults(field, fieldFactoryUserProduct[fieldName]);
                        field.toString = fieldFactoryUserProduct[fieldName].toString;
                        if(_.has(fieldFactoryUserProduct[fieldName], "time")){
                            _.defaults(field.time.init, fieldFactoryUserProduct[fieldName].time.init);
                            _.defaults(field.time.last, fieldFactoryUserProduct[fieldName].time.last);
                            field.time.init.toString = fieldFactoryUserProduct[fieldName].time.init.toString;
                            field.time.last.toString = fieldFactoryUserProduct[fieldName].time.last.toString;
                        }
                    });
                });
            });
        });

        _.each(chanStats, function(server, serverName){
            _.each(chanStats[serverName], function(chan, chanName){
                var fieldFactoryChanProduct = structure.fieldFactoryOutlet("chan", api);
                _.defaults(chanStats[serverName][chanName], fieldFactoryChanProduct);
                //TODO(samstudio8) Doom, not chan stats too!
                _.each(chanStats[serverName][chanName], function(field, fieldName){
                    _.defaults(chanStats[serverName][chanName][fieldName], fieldFactoryChanProduct[fieldName]);
                    chanStats[serverName][chanName][fieldName].toString = fieldFactoryChanProduct[fieldName].toString;
                    if(_.has(fieldFactoryChanProduct[fieldName], "time")){
                        _.defaults(chanStats[serverName][chanName][fieldName].time.init, fieldFactoryChanProduct[fieldName].time.init);
                        _.defaults(chanStats[serverName][chanName][fieldName].time.last, fieldFactoryChanProduct[fieldName].time.last);
                        field.time.init.toString = fieldFactoryChanProduct[fieldName].time.init.toString;
                        field.time.last.toString = fieldFactoryChanProduct[fieldName].time.last.toString;
                    }
                });
            });
        });
        dbot.save();

        // Add API Hooks
        dbot.api.command.addHook('~setaliasparent', this.api.renameStats);
        dbot.api.command.addHook('~mergeusers', this.api.mergeStats);

    }.bind(this);

    this.onDestroy = function() {
        // Destroy structures cache so it can be reloaded with changes applied
        var cacheKey = require.resolve('./structures');
        delete require.cache[cacheKey];
    }.bind(this);
};

exports.fetch = function(dbot){
    return new stats(dbot);
};
<commit_msg>Stop recording PM stats [Close #73]<commit_after>var stats = function(dbot){
    var userStats = dbot.db.userStats;
    var chanStats = dbot.db.chanStats;

    //TODO(samstudio8) @reality is officially deprecating snippets.js,
    //cannot rely on filter or numberFormat
    var _ = require('underscore')._;
    var structure = require('./structures');

    this.listener = function(event){

        // Ignore command messages or PM
        if(event.message[0] == "~" || event.channel == event.user){
            return;
        }

        // Check server dbKeys exist
        if(!userStats.hasOwnProperty(event.server)){
            userStats[event.server] = {}
        }
        if(!chanStats.hasOwnProperty(event.server)){
            chanStats[event.server] = {}
        }

        var user = dbot.api.users.resolveUser(event.server, event.user, true).toLowerCase();
        var num_words = event.message.split(" ").filter(function(w, i, array) { return w.length > 0; }).length

        // User-centric Stats
        if(!userStats[event.server].hasOwnProperty(user)){
            userStats[event.server][user] = {}
        }
        if(!userStats[event.server][user].hasOwnProperty(event.channel)){
            userStats[event.server][user][event.channel] = {}
            _.defaults(userStats[event.server][user][event.channel], structure.fieldFactoryOutlet("user", this.api));
        }
        userStats[event.server][user][event.channel]["freq"].add({"day": event.time.getDay(),
                                                                  "hour": event.time.getHours(),
                                                                  "inc": 1
        });
        userStats[event.server][user][event.channel]["lines"].add(1);
        userStats[event.server][user][event.channel]["words"].add(num_words);

        // Channel-centric Stats
        if(!chanStats[event.server].hasOwnProperty(event.channel)){
            chanStats[event.server][event.channel] = {}
            _.defaults(chanStats[event.server][event.channel], structure.fieldFactoryOutlet("chan", this.api));
        }
        chanStats[event.server][event.channel]["freq"].add({"day": event.time.getDay(),
                                                            "hour": event.time.getHours(),
                                                            "inc": 1
        });
        chanStats[event.server][event.channel]["lines"].add(1);
        chanStats[event.server][event.channel]["words"].add(num_words);

        // Check whether the line includes any mentions
        if(dbot.db.hasOwnProperty("knownUsers")){
            var cat = dbot.db.knownUsers[event.server].users.concat(
                    Object.keys(dbot.db.knownUsers[event.server].aliases));
            for (var i = 0; i < cat.length; i++){
                var name = cat[i];
                var mentioned = dbot.api.users.resolveUser(event.server, name, true).toLowerCase();
                if(user != mentioned
                        && userStats[event.server].hasOwnProperty(mentioned)
                        && userStats[event.server][mentioned].hasOwnProperty(event.channel)){
                    var toMatch = "( |^)"+name.escape().toLowerCase()+":?(?=\\s|$)";
                    if(event.message.toLowerCase().search(toMatch) > -1){
                        userStats[event.server][user][event.channel]["out_mentions"].add({"mentioned": mentioned,
                                                                                "inc": 1});
                        userStats[event.server][mentioned][event.channel]["in_mentions"].add(1);
                    }
                }
            }
        }
    }.bind(this);
    this.on = 'PRIVMSG';

    this.onLoad = function(){
        //TODO(samstudio8): Also delete no longer required fields

        // For some reason the scope of this changes and prevents access to the
        // API inside of the underscore loops, I think it's because I need to
        // pass in a context, but this works anyway.
        var api = this.api;

        //TODO(samstudio8) There must be a nicer way to do this...
        _.each(userStats, function(server, serverName){
            _.each(userStats[serverName], function(user, userName){
                _.each(userStats[serverName][userName], function(chan, chanName){
                    var fieldFactoryUserProduct = structure.fieldFactoryOutlet("user", api);
                    _.defaults(userStats[serverName][userName][chanName], fieldFactoryUserProduct);
                    //TODO(samstudio8) Doom, it was going so well.
                    _.each(userStats[serverName][userName][chanName], function(field, fieldName){
                        _.defaults(field, fieldFactoryUserProduct[fieldName]);
                        field.toString = fieldFactoryUserProduct[fieldName].toString;
                        if(_.has(fieldFactoryUserProduct[fieldName], "time")){
                            _.defaults(field.time.init, fieldFactoryUserProduct[fieldName].time.init);
                            _.defaults(field.time.last, fieldFactoryUserProduct[fieldName].time.last);
                            field.time.init.toString = fieldFactoryUserProduct[fieldName].time.init.toString;
                            field.time.last.toString = fieldFactoryUserProduct[fieldName].time.last.toString;
                        }
                    });
                });
            });
        });

        _.each(chanStats, function(server, serverName){
            _.each(chanStats[serverName], function(chan, chanName){
                var fieldFactoryChanProduct = structure.fieldFactoryOutlet("chan", api);
                _.defaults(chanStats[serverName][chanName], fieldFactoryChanProduct);
                //TODO(samstudio8) Doom, not chan stats too!
                _.each(chanStats[serverName][chanName], function(field, fieldName){
                    _.defaults(chanStats[serverName][chanName][fieldName], fieldFactoryChanProduct[fieldName]);
                    chanStats[serverName][chanName][fieldName].toString = fieldFactoryChanProduct[fieldName].toString;
                    if(_.has(fieldFactoryChanProduct[fieldName], "time")){
                        _.defaults(chanStats[serverName][chanName][fieldName].time.init, fieldFactoryChanProduct[fieldName].time.init);
                        _.defaults(chanStats[serverName][chanName][fieldName].time.last, fieldFactoryChanProduct[fieldName].time.last);
                        field.time.init.toString = fieldFactoryChanProduct[fieldName].time.init.toString;
                        field.time.last.toString = fieldFactoryChanProduct[fieldName].time.last.toString;
                    }
                });
            });
        });
        dbot.save();

        // Add API Hooks
        dbot.api.command.addHook('~setaliasparent', this.api.renameStats);
        dbot.api.command.addHook('~mergeusers', this.api.mergeStats);

    }.bind(this);

    this.onDestroy = function() {
        // Destroy structures cache so it can be reloaded with changes applied
        var cacheKey = require.resolve('./structures');
        delete require.cache[cacheKey];
    }.bind(this);
};

exports.fetch = function(dbot){
    return new stats(dbot);
};
<|endoftext|>


<commit_before>var validateHTTP = require("./validateHTTP.js");
var restify = require('restify'),
    request = require('request'),
    rand = require('csprng'),
    sha1 = require('sha1'),
    uuid = require('node-uuid'),
    neo4j = require('node-neo4j');
module.exports.addOffer = function(req, res, next) {
    if((validateHTTP.validateHTTP(req, res, next, "business")) === true) {
        console.log('NEW OFFER!');
        console.log('PUT: ' + req.params.offer)
        db = new neo4j('http://localhost:7474');
        var rand = uuid.v1()
        var businessName = req.params.businessname
        var offertitle = req.params.offer + ' - ' + businessName
        var nodeid = 0;
        var url = 'http://localhost:5984/offers/' + offertitle;
        var description = req.params.description
        var REQ = req
        request.get(url, function(err, response, body) {
            if(err) {
                return next(new restify.InternalServerError('Error has occured'));
            }
            // if the document isnt found it will create it from sratch
            console.log('code' + response.statusCode)
            if(response.statusCode === 200) {
                return next(new restify.ConflictError('Offer already created'));
            } else if(response.statusCode === 404) {
                db.insertNode({
                    name: offertitle
                }, ['Offer', businessName], function(err, node) {
                    if(err) throw err;
                    // Output node properties.
                    console.log('New neo4j node created with name = ' + node.name);
                    // Output node id.
                    console.log(node._id);
                    nodeid = node._id
                    var d = new Date(),
                        date = d.toUTCString();
                    console.log(date);
                    console.log('Just above doc node id  ' + nodeid);
                    var doc = {
                        date_created: date,
                        last_modified: date,
                        uuid: rand,
                        offer_title: REQ.params.offer,
                        offer_description: description,
                        businessname: businessName,
                        redeems: [],
                        nodeid: nodeid
                    };
                    console.log('underneath node.id  ' + doc.nodeid);
                    var docStr = JSON.stringify(doc);
                    var params = {
                        uri: url,
                        body: JSON.stringify(doc)
                    };
                    request.put(params, function(err, response, body) {
                        if(err) {
                            return next(new restify.InternalServerError('Cant create document'));
                        }
                        // document has been inserted into database
                        body = JSON.parse(body);
                        res.send({
                            offer: req.params
                        });
                        res.end();
                    });
                });
            };
        });
        // if the document is found, that means the user is already created.
    };
};
module.exports.getAllOffers = function(req, res, next) {
    if((validateHTTP.validateHTTP(req, res, next, "users")) === true) {
        console.log('Get All OFFERS!');
        //Gets all offers from couchDB as JSON
        var url = 'http://api.adam-holt.co.uk:5984/offers/_design/offers/_view/all';
        request.get(url, function(err, response, body) {
            if(err) {
                return next(new restify.InternalServerError('Error has occured'));
            }
            if(response.statusCode === 200) {
                res.send(response);
            };
            else if(response.statusCode === 404) {
                return next(new restify.InternalServerError('No Offers Found'));
            };
        });
    };
};<commit_msg>Added get all offers<commit_after>var validateHTTP = require("./validateHTTP.js");
var restify = require('restify'),
    request = require('request'),
    rand = require('csprng'),
    sha1 = require('sha1'),
    uuid = require('node-uuid'),
    neo4j = require('node-neo4j');
module.exports.addOffer = function(req, res, next) {
    if((validateHTTP.validateHTTP(req, res, next, "business")) === true) {
        console.log('NEW OFFER!');
        console.log('PUT: ' + req.params.offer)
        db = new neo4j('http://localhost:7474');
        var rand = uuid.v1()
        var businessName = req.params.businessname
        var offertitle = req.params.offer + ' - ' + businessName
        var nodeid = 0;
        var url = 'http://localhost:5984/offers/' + offertitle;
        var description = req.params.description
        var REQ = req
        request.get(url, function(err, response, body) {
            if(err) {
                return next(new restify.InternalServerError('Error has occured'));
            }
            // if the document isnt found it will create it from sratch
            console.log('code' + response.statusCode)
            if(response.statusCode === 200) {
                return next(new restify.ConflictError('Offer already created'));
            } else if(response.statusCode === 404) {
                db.insertNode({
                    name: offertitle
                }, ['Offer', businessName], function(err, node) {
                    if(err) throw err;
                    // Output node properties.
                    console.log('New neo4j node created with name = ' + node.name);
                    // Output node id.
                    console.log(node._id);
                    nodeid = node._id
                    var d = new Date(),
                        date = d.toUTCString();
                    console.log(date);
                    console.log('Just above doc node id  ' + nodeid);
                    var doc = {
                        date_created: date,
                        last_modified: date,
                        uuid: rand,
                        offer_title: REQ.params.offer,
                        offer_description: description,
                        businessname: businessName,
                        redeems: [],
                        nodeid: nodeid
                    };
                    console.log('underneath node.id  ' + doc.nodeid);
                    var docStr = JSON.stringify(doc);
                    var params = {
                        uri: url,
                        body: JSON.stringify(doc)
                    };
                    request.put(params, function(err, response, body) {
                        if(err) {
                            return next(new restify.InternalServerError('Cant create document'));
                        }
                        // document has been inserted into database
                        body = JSON.parse(body);
                        res.send({
                            offer: req.params
                        });
                        res.end();
                    });
                });
            };
        });
        // if the document is found, that means the user is already created.
    };
};
module.exports.getAllOffers = function(req, res, next) {
    if((validateHTTP.validateHTTP(req, res, next, "users")) === true) {
        console.log('Get All OFFERS!');
        //Gets all offers from couchDB as JSON
        var url = 'http://api.adam-holt.co.uk:5984/offers/_design/offers/_view/all';
        request.get(url, function(err, response, body) {
            if(err) {
                return next(new restify.InternalServerError('Error has occured'));
            }
            if(response.statusCode === 200) {
                res.send(response);
            }
            else if(response.statusCode === 404) {
                return next(new restify.InternalServerError('No Offers Found'));
            };
        });
    };
};<|endoftext|>


<commit_before>//
// Copyright (c) 2016 Cisco Systems
// Licensed under the MIT License
//

/*
 * a Cisco Spark bot that computes stats for a room
 *
 * note : this example requires you set up a SPARK_TOKEN env variable for a real account (not a bot account),
 *     as this code reads past messages in the room
 *
 */

var debug = require("debug")("samples");
var fine = require("debug")("samples:fine");

// Starts your Bot with default configuration. The SPARK API access token is read from the SPARK_TOKEN env variable
var SparkBot = require("node-sparkbot");
var bot = new SparkBot();

// Change command prefix to #
bot.interpreter.prefix = "#";

var SparkClient = require("node-sparky");
var spark = new SparkClient({ token: process.env.SPARK_TOKEN });

bot.onCommand("about", function (command) {
    spark.messageSendRoom(command.message.roomId, {
        markdown: "```\n{\n   'author':'Stève Sfartz <stsfartz@cisco.com>',\n   'code':'https://github.com/ObjectIsAdvantag/sparkbot-webhook-samples/blob/master/examples/room-stats.js',\n   'description':'computes the top contributors in a spark room',\n   'healthcheck':'GET https://sparkbot-room-stats.herokuapp.com',\n   'webhook':'POST https://sparkbot-room-stats.herokuapp.com'\n}\n```"
    });
});

bot.onCommand("fallback", function (command) {
    // so happy to join
    spark.messageSendRoom(command.message.roomId, {
        text: "sorry, I did not understand"
    })
        .then(function (message) {
            // show how to use
            showHelp(command.message.roomId);
        });
});
bot.onCommand("help", function (command) {
    showHelp(command.message.roomId);
});
function showHelp(roomId) {
    spark.messageSendRoom(roomId, {
        markdown: "I am all about Stats for your Spark rooms\n- "
        + "\\" + bot.interpreter.prefix + "about\n- "
        + "\\" + bot.interpreter.prefix + "help\n- "
        + "\\" + bot.interpreter.prefix + "stats [#messages] : computes stats from past messages, defaults to 100"
    });
}

bot.onCommand("stats", function (command) {

    // Max number of fetched messages, default is 100
    var max = command.args[0];
    if (!max) {
        max = 100;
    }

    // As computing stats takes time, let's acknowledge we received the order
    spark.messageSendRoom(command.message.roomId, {
        markdown: "_heard you ! now computing stats from past " + max + " messages..._"
    });

    // Build a map of participations by participant email
    var participants = {};
    var totalMessages = 0; // used to get %ages of participation
    spark.messagesGet(command.message.roomId, max)
        .then(function (messages) {
            // Process messages
            messages.forEach(function (message) {
                totalMessages++;

                // [WORKAROUND] Remove incoming integrations as they are not supported in mentions
                if (!isIncomingIntegration(message)) {
                    var current = participants[message.personEmail];
                    if (!current) {
                        participants[message.personEmail] = 1;
                    }
                    else {
                        participants[message.personEmail] = current + 1;
                    }
                }
            });

            // Sort participants by participation DESC
            var top = Object.keys(participants) //Create a list from the keys of your map.
                .sort( //Sort it ...
                function (a, b) { // using a custom sort function that...
                    // compares (the keys) by their respective values.
                    return participants[b] - participants[a]; // DESC order
                });

            // Display top 10 participants
            var length = top.length;
            var limit = Math.min(length, 10);
            switch (limit) {
                case 0:
                    spark.messageSendRoom(command.message.roomId, {
                        text: "did not find any participant! is the room active?"
                    });
                    break;
                case 1:
                    spark.messageSendRoom(command.message.roomId, {
                        markdown: "**kudos to <@personEmail:" + top[0] + ">" + ", the only 1 active participant in here !**"
                    });
                    break;
                default:
                    var stats = "**kudos to the top participants**";
                    for (var i = 0; i < limit; i++) {
                        var email = top[i];
                        var number = participants[email];
                        var pourcentage = Math.round(number * 100 / totalMessages);

                        // Display only relevant contributors
                        if (pourcentage >= 2) {
                            stats += "\n\n" + (i + 1) + ". <@personEmail:" + email + ">, " + pourcentage + "% (" + number + ")";
                        }
                    }
                    spark.messageSendRoom(command.message.roomId, {
                        markdown: stats
                    });
                    break;
            }
        });

});

bot.onEvent("memberships", "created", function (trigger) {
    var newMembership = trigger.data; // see specs here: https://developer.ciscospark.com/endpoint-memberships-get.html
    if (newMembership.personId == bot.interpreter.person.id) {
        debug("bot's just added to room: " + trigger.data.roomId);

        // so happy to join
        spark.messageSendRoom(trigger.data.roomId, {
            text: "Hi, I am so happy to join !"
        })
            .then(function (message) {
                showHelp(trigger.data.roomId);
            });
    }
});

// Filter for incoming integration as these are automatically created by Spark back-end by creating a fake account
// This fake account has an email built from the owner email and suffixed with a number
// email: <owner-email>-<suffix-digits>@<owner-domain>
function isIncomingIntegration(message) {
    var matched = message.personEmail.match(/-\d+@/);
    if (!matched) {
        return false;
    }

    fine("identified as integration: " + message.personEmail);
    return true;
}

<commit_msg>Delete room-stats.js<commit_after><|endoftext|>


<commit_before>var inherits = require('util').inherits

var bitcoin = require('../cclib').bitcoin
var _ = require('lodash')
var LRU = require('lru-cache')
var Q = require('q')
var request = require('request')

var BlockchainBase = require('./BlockchainBase')

/**
 * BlockchainState that uses [Chain.com API]{@link https://chain.com/docs}
 *
 * @class Chain
 * @extends BlockchainBase
 *
 * @param {Object} opts
 * @param {boolean} opts.testnet
 * @param {string} [opts.apiKeyId=DEMO-4a5e1e4]
 * @param {number} [opts.requestTimeout=5*1000]
 * @param {number} [opts.maxCacheSize=500]
 * @param {number} [opts.maxCacheAge=10*1000] Cache live in ms
 * @param {BaseTxDb} [opts.txDb] Get transactions from TxDb, optional
 */
function Chain(opts) {
  opts = _.extend({
    testnet: false,
    apiKeyId: 'DEMO-4a5e1e4',
    requestTimeout: 5*1000,
    maxCacheSize: 500,
    maxCacheAge: 10*1000
  }, opts)

  BlockchainBase.call(this)

  this.isTestnet = opts.testnet
  this.baseURL = 'https://api.chain.com/v1/' + (this.isTestnet ? 'testnet3' : 'bitcoin')
  this.apiKeyId = opts.apiKeyId

  this.cache = LRU({
    max: opts.maxCacheSize,
    maxAge: opts.maxCacheAge
  })

  this.requestTimeout = opts.requestTimeout + 25
  this.requestPathCache = LRU({ maxAge: this.requestTimeout })

  if (!_.isUndefined(opts.txDb))
    this.txDb = opts.txDb
}

inherits(Chain, BlockchainBase)

/**
 * @callback Chain~request
 * @param {?Error} error
 * @param {string} response
 */

/**
 * Make request to the server
 *
 * @param {string} path Path to resource
 * @param {Object} [data=null] Data for POST request, may be missed
 * @param {Chain~request} cb
 */
Chain.prototype.request = function(path, data, cb) {
  if (_.isFunction(data) && _.isUndefined(cb)) {
    cb = data
    data = null
  }

  var self = this

  /** check in cache */
  var cachedValue = self.cache.get(path)
  if (!_.isUndefined(cachedValue)) {
    process.nextTick(function() { cb(null, cachedValue) })
    return
  }

  /** check already requested */
  if (!_.isUndefined(self.requestPathCache.get(path))) {
    setTimeout(function() { self.request(path, data, cb) }, 25)
    return
  }

  /** make request */
  self.requestPathCache.set(path, true)
  var requestOpts = {
    method: data === null ? 'GET' : 'POST',
    uri: self.baseURL + path + '?api-key-id=' + self.apiKeyId,
    body: JSON.stringify(data),
    timeout: self.requestTimeout,
    json: true
  }

  Q.nfcall(request, requestOpts).spread(function(response, body) {
    if (response.statusCode !== 200)
      throw new Error('Request error: ' + response.statusMessage)

    return body

  }).done(function(data) { cb(null, data) }, function(error) { cb(error) })
}

/**
 * @callback Chain~getBlockCount
 * @param {?Error} error
 * @param {number} blockCount
 */

/**
 * Get block count in blockchain
 *
 * @param {Chain~getBlockCount} cb
 */
Chain.prototype.getBlockCount = function(cb) {
  Q.ninvoke(this, 'request', '/blocks/latest').then(function(response) {
    return response.height

  }).done(function(blockCount) { cb(null, blockCount) }, function(error) { cb(error) })
}

/**
 * @callback Chain~getTx
 * @param {?Error} error
 * @param {bitcoinjs-lib.Transaction} tx
 */

/**
 * Get transaction by txId
 *
 * @param {string} txId bitcoinjs-lib.Transaction id
 * @param {Chain~getTx} cb
 */
Chain.prototype.getTx = function(txId, cb) {
  var self = this

  Q.fcall(function() {
    if (!_.isUndefined(self.txDb)) {
      var tx = self.txDb.getTxById(txId)
      if (tx !== null)
        return tx
    }

    return Q.ninvoke(self, 'request', '/transactions/' + txId + '/hex').then(function(response) {
      return bitcoin.Transaction.fromHex(response.hex)
    })

  }).then(function(tx) {
    if (tx.getId() !== txId)
      throw new Error('Received tx is incorrect')

    return tx

  }).done(function(tx) { cb(null, tx) }, function(error) { cb(error) })
}

/**
 * @callback Chain~getTxBlockHash
 * @param {?Error} error
 * @param {string} blockHash
 */

/**
 * @param {string} txId
 * @param {Chain~getTxBlockHash} cb
 */
Chain.prototype.getTxBlockHash = function(txId, cb) {
  Q.ninvoke(this, 'request', '/transactions/' + txId).then(function(response) {
    return response.block_hash

  }).done(function(blockHash) { cb(null, blockHash) }, function(error) { cb(error) })
}

/**
 * @callback Chain~getBlockHeight
 * @param {?Error} error
 * @param {number} height
 */

/**
 * @param {string} blockHash
 * @param {Chain~getBlockHeight} cb
 */
Chain.prototype.getBlockHeight = function(blockHash, cb) {
  Q.ninvoke(this, 'request', '/blocks/' + blockHash).then(function(response) {
    return response.height

  }).done(function(blockHash) { cb(null, blockHash) }, function(error) { cb(error) })
}

/**
 * @callback BlockchainBase~getBlockTime
 * @param {?Error} error
 * @param {number} timestamp
 */

/**
 * @param {string} blockHash
 * @param {BlockchainBase~getBlockTime} cb
 */
Chain.prototype.getBlockTime = function(blockHash, cb) {
  Q.ninvoke(this, 'request', '/blocks/' + blockHash).then(function(response) {
    return Math.round(Date.parse(response.time)/1000)

  }).done(function(height) { cb(null, height) }, function(error) { cb(error) })
}

/**
 * @callback Chain~sendTx
 * @param {?Error} error
 * @param {string} txId
 */

/**
 * Send transaction tx to server which broadcast tx to network
 *
 * @param {bitcoinjs-lib.Transaction} tx
 * @param {Chain~sendTx} cb
 */
Chain.prototype.sendTx = function(tx, cb) {
  Q.ninvoke(this, 'request', '/transactions', { 'hex': tx.toHex() }).then(function(response) {
    return response.transaction_hash

  }).done(function(txId) { cb(null, txId) }, function(error) { cb(error) })
}

/**
 * @typedef CoinObject
 * @type {Object}
 * @property {string} txId
 * @property {number} outIndex
 * @property {number} value Coin value in satoshi
 * @property {string} script
 * @property {string} address
 * @property {number} confrimations
 */

/**
 * @callback Chain~getUTXO
 * @param {?Error} error
 * @param {CoinObject[]} utxo
 */

/**
 * Get UTXO for given address
 * @abstract
 * @param {string} address
 * @param {Chain~getUTXO} cb
 */
Chain.prototype.getUTXO = function(address, cb) {
  Q.ninvoke(this, 'request', '/addresses/' + address + '/unspents').then(function(rawCoins) {
    return rawCoins.map(function(rawCoin) {
      return {
        txId: rawCoin.transaction_hash,
        outIndex: rawCoin.output_index,
        value: rawCoin.value,
        script: rawCoin.script_hex,
        address: address,
        confirmations: rawCoin.confirmations
      }
    })

  }).done(function(coins) { cb(null, coins) }, function(error) { cb(error) })
}

/**
 * @typedef HistoryObject
 * @type {Object}
 * @property {string} txId
 * @property {number} confirmations
 */

/**
 * @callback Chain~getHistory
 * @param {?Error} error
 * @param {HistoryObject[]} records
 */

/**
 * Get transaction Ids for given address
 * @abstract
 * @param {string} address
 * @param {Chain~getHistory} cb
 */
Chain.prototype.getHistory = function(address, cb) {
  Q.ninvoke(this, 'request', '/addresses/' + address + '/transactions').then(function(records) {
    return records.map(function(record) {
      return { txId: record.hash, confirmations: record.confirmations }
    })

  }).done(function(records) { cb(null, records) }, function(error) { cb(error) })
}

module.exports = Chain
<commit_msg>Make Chain requests faster for fullScans setTimeout 25 on repeated calls cause several seconds long idle times Changed so you now have an array of listeners waiting for a request to finish<commit_after>var inherits = require('util').inherits

var bitcoin = require('../cclib').bitcoin
var _ = require('lodash')
var LRU = require('lru-cache')
var Q = require('q')
var request = require('request')
//request.debug=true

var BlockchainBase = require('./BlockchainBase')

/**
 * BlockchainState that uses [Chain.com API]{@link https://chain.com/docs}
 *
 * @class Chain
 * @extends BlockchainBase
 *
 * @param {Object} opts
 * @param {boolean} opts.testnet
 * @param {string} [opts.apiKeyId=DEMO-4a5e1e4]
 * @param {number} [opts.requestTimeout=5*1000]
 * @param {number} [opts.maxCacheSize=500]
 * @param {number} [opts.maxCacheAge=10*1000] Cache live in ms
 * @param {BaseTxDb} [opts.txDb] Get transactions from TxDb, optional
 */
function Chain(opts) {
  opts = _.extend({
    testnet: false,
    apiKeyId: 'DEMO-4a5e1e4',
    requestTimeout: 5*1000,
    maxCacheSize: 500,
    maxCacheAge: 10*1000
  }, opts)

  BlockchainBase.call(this)

  this.isTestnet = opts.testnet
  this.baseURL = 'https://api.chain.com/v1/' + (this.isTestnet ? 'testnet3' : 'bitcoin')
  this.apiKeyId = opts.apiKeyId

  this.cache = LRU({
    max: opts.maxCacheSize,
    maxAge: opts.maxCacheAge
  })

  this.requestTimeout = opts.requestTimeout + 25

  this.requestPathMap = {}

  if (!_.isUndefined(opts.txDb))
    this.txDb = opts.txDb
}

inherits(Chain, BlockchainBase)

/**
 * @callback Chain~request
 * @param {?Error} error
 * @param {string} response
 */

/**
 * Make request to the server
 *
 * @param {string} path Path to resource
 * @param {Object} [data=null] Data for POST request, may be missed
 * @param {Chain~request} cb
 */
Chain.prototype.request = function(path, data, cb) {
  if (_.isFunction(data) && _.isUndefined(cb)) {
    cb = data
    data = null
  }

  var self = this

  /** check in cache */
  var cachedValue = self.cache.get(path)
  if (!_.isUndefined(cachedValue)) {
    process.nextTick(function() { cb(null, cachedValue) })
    return
  }

  /** check already requested */
  var requestPathCallbacks = self.requestPathMap[path]
  if (_.isUndefined(requestPathCallbacks)) {
    requestPathCallbacks = []
  }
  requestPathCallbacks.push(cb)

  self.requestPathMap[path] = requestPathCallbacks;

  var doCallbacks = function(param1, param2) {
    var requestPathCallbacks = self.requestPathMap[path]
    delete self.requestPathMap[path]

    _.each(requestPathCallbacks, function (cb) {
      cb(param1, param2)
    })
  }

  /** make request */

  var requestOpts = {
    method: data === null ? 'GET' : 'POST',
    uri: self.baseURL + path + '?api-key-id=' + self.apiKeyId,
    body: JSON.stringify(data),
    timeout: self.requestTimeout,
    zip: true,
    json: true
  }

  Q.nfcall(request, requestOpts).spread(function(response, body) {

    if (response.statusCode !== 200)
      throw new Error('Request error: ' + response.statusMessage)

    return body

  }).done(function(data) {
    self.cache.set(path, data)
    doCallbacks(null, data)
  }, function(error) { doCallbacks(error) })
}

/**
 * @callback Chain~getBlockCount
 * @param {?Error} error
 * @param {number} blockCount
 */

/**
 * Get block count in blockchain
 *
 * @param {Chain~getBlockCount} cb
 */
Chain.prototype.getBlockCount = function(cb) {
  Q.ninvoke(this, 'request', '/blocks/latest').then(function(response) {
    return response.height

  }).done(function(blockCount) { cb(null, blockCount) }, function(error) { cb(error) })
}

/**
 * @callback Chain~getTx
 * @param {?Error} error
 * @param {bitcoinjs-lib.Transaction} tx
 */

/**
 * Get transaction by txId
 *
 * @param {string} txId bitcoinjs-lib.Transaction id
 * @param {Chain~getTx} cb
 */
Chain.prototype.getTx = function(txId, cb) {
  var self = this

  Q.fcall(function() {
    if (!_.isUndefined(self.txDb)) {
      var tx = self.txDb.getTxById(txId)
      if (tx !== null)
        return tx
    }

    return Q.ninvoke(self, 'request', '/transactions/' + txId + '/hex').then(function(response) {
      return bitcoin.Transaction.fromHex(response.hex)
    })

  }).then(function(tx) {
    if (tx.getId() !== txId)
      throw new Error('Received tx is incorrect')

    return tx

  }).done(function(tx) { cb(null, tx) }, function(error) { cb(error) })
}

/**
 * @callback Chain~getTxBlockHash
 * @param {?Error} error
 * @param {string} blockHash
 */

/**
 * @param {string} txId
 * @param {Chain~getTxBlockHash} cb
 */
Chain.prototype.getTxBlockHash = function(txId, cb) {
  Q.ninvoke(this, 'request', '/transactions/' + txId).then(function(response) {
    return response.block_hash

  }).done(function(blockHash) { cb(null, blockHash) }, function(error) { cb(error) })
}

/**
 * @callback Chain~getBlockHeight
 * @param {?Error} error
 * @param {number} height
 */

/**
 * @param {string} blockHash
 * @param {Chain~getBlockHeight} cb
 */
Chain.prototype.getBlockHeight = function(blockHash, cb) {
  Q.ninvoke(this, 'request', '/blocks/' + blockHash).then(function(response) {
    return response.height

  }).done(function(blockHash) { cb(null, blockHash) }, function(error) { cb(error) })
}

/**
 * @callback BlockchainBase~getBlockTime
 * @param {?Error} error
 * @param {number} timestamp
 */

/**
 * @param {string} blockHash
 * @param {BlockchainBase~getBlockTime} cb
 */
Chain.prototype.getBlockTime = function(blockHash, cb) {
  Q.ninvoke(this, 'request', '/blocks/' + blockHash).then(function(response) {
    return Math.round(Date.parse(response.time)/1000)

  }).done(function(height) { cb(null, height) }, function(error) { cb(error) })
}

/**
 * @callback Chain~sendTx
 * @param {?Error} error
 * @param {string} txId
 */

/**
 * Send transaction tx to server which broadcast tx to network
 *
 * @param {bitcoinjs-lib.Transaction} tx
 * @param {Chain~sendTx} cb
 */
Chain.prototype.sendTx = function(tx, cb) {
  Q.ninvoke(this, 'request', '/transactions', { 'hex': tx.toHex() }).then(function(response) {
    return response.transaction_hash

  }).done(function(txId) { cb(null, txId) }, function(error) { cb(error) })
}

/**
 * @typedef CoinObject
 * @type {Object}
 * @property {string} txId
 * @property {number} outIndex
 * @property {number} value Coin value in satoshi
 * @property {string} script
 * @property {string} address
 * @property {number} confrimations
 */

/**
 * @callback Chain~getUTXO
 * @param {?Error} error
 * @param {CoinObject[]} utxo
 */

/**
 * Get UTXO for given address
 * @abstract
 * @param {string} address
 * @param {Chain~getUTXO} cb
 */
Chain.prototype.getUTXO = function(address, cb) {
  Q.ninvoke(this, 'request', '/addresses/' + address + '/unspents').then(function(rawCoins) {
    return rawCoins.map(function(rawCoin) {
      return {
        txId: rawCoin.transaction_hash,
        outIndex: rawCoin.output_index,
        value: rawCoin.value,
        script: rawCoin.script_hex,
        address: address,
        confirmations: rawCoin.confirmations
      }
    })

  }).done(function(coins) { cb(null, coins) }, function(error) { cb(error) })
}

/**
 * @typedef HistoryObject
 * @type {Object}
 * @property {string} txId
 * @property {number} confirmations
 */

/**
 * @callback Chain~getHistory
 * @param {?Error} error
 * @param {HistoryObject[]} records
 */

/**
 * Get transaction Ids for given address
 * @abstract
 * @param {string} address
 * @param {Chain~getHistory} cb
 */
Chain.prototype.getHistory = function(address, cb) {
  Q.ninvoke(this, 'request', '/addresses/' + address + '/transactions').then(function(records) {
    return records.map(function(record) {
      return { txId: record.hash, confirmations: record.confirmations }
    })

  }).done(function(records) { cb(null, records) }, function(error) { cb(error) })
}

module.exports = Chain
<|endoftext|>