<commit_before>var article_ranges = [];

function match_ranges(sometext, re) {
    var matches = [];
    while ((m = re.exec(sometext)) !== null) {
      matches.push([re.lastIndex - m[0].length, re.lastIndex - 1]);
    }
    return matches;
};

function checkVol(){
    var table = $('#art_table').DataTable();
    var ret = table.$('tr.selected');
    var selvol = ret[0].cells[0].innerHTML;
    var query = new Parse.Query(VolObject);
    query.equalTo("vol", selvol)
    query.find({
        success: function(results) {
            //alert("Successfully" + results[0].get("text"))
            $("#col2text").html(results[0].get("text"));
        }
    })
}

var highlighter;

function highlightSelectedText() {
    highlighter.highlightSelection("highlight");
}

function removeHighlightFromSelectedText() {
    highlighter.unhighlightSelection();
}

function addArticle(){
    highlightSelectedText();
    var range = rangy.createRange();
    var highlight_range = rangy.getSelection().getRangeAt(0).toCharacterRange(document.getElementById('col2text'))
    var highlight_start = highlight_range.start;
    var highlight_end = highlight_range.end;
    article_ranges.push(([highlight_start, highlight_end]));
    //var highlight_range = range.toCharacterRange("col2text")
    window.alert(highlight_start + " " + highlight_end);
}

function removeArticle(){

}

function checkRange(element){
    var textarea1 = document.getElementById("col2text");
    //var click_position = textarea1.selectionStart;
    var highlight_range = rangy.getSelection().getRangeAt(0).toCharacterRange(textarea1)
    var highlight_start = highlight_range.start;
    var highlight_end = highlight_range.end;
    //window.alert(highlight_start, highlight_end);
    for(var a = 0; a < article_ranges.length; a++ ){
        //window.alert("Article Range: " + article_ranges[a][0] + " " + article_ranges[a][1]);
        if (highlight_start >= article_ranges[a][0] && highlight_start <= article_ranges[a][1]){
            window.alert("Clicked inside article");
            var range = rangy.createRange();
            range.selectNodeContents(element);
            //var node = element;
            //debugger;
            //range.selectNode($(this));
            //range.setStartAndEnd(textarea1, article_ranges[a][0], article_ranges[a][1]);
            var sel = rangy.getSelection();
            sel.setSingleRange(range);

        }
    }
}

function replaceRange(s, start, end, substitute) {
    return s.substring(0, start) + substitute + s.substring(end);
}

function init() {

    Parse.initialize("Dxi3BvGT3mHiDC7B1YjeEuiUQKtWIeQNofT5FIIx", "QG352rxcZvLrYeV4jOCsIZvM8mIeQyhvHzDNINAb");

    VolObject = Parse.Object.extend("Text");
    ArtObject = Parse.Object.extend("Article");
    volObject = new VolObject();
    artObject = new ArtObject();

    rangy.init();

    highlighter = rangy.createHighlighter();

    applier = rangy.createClassApplier("highlight");

    highlighter.addClassApplier(rangy.createClassApplier("highlight", {
        ignoreWhiteSpace: true,
        elementProperties: {onclick:"checkRange(this)"},
        tagNames: ["span", "a"]
    }));

}<commit_msg>fixed click listener on spans<commit_after>var article_ranges = [];

function match_ranges(sometext, re) {
    var matches = [];
    while ((m = re.exec(sometext)) !== null) {
      matches.push([re.lastIndex - m[0].length, re.lastIndex - 1]);
    }
    return matches;
};

function checkVol(){
    var table = $('#art_table').DataTable();
    var ret = table.$('tr.selected');
    var selvol = ret[0].cells[0].innerHTML;
    var query = new Parse.Query(VolObject);
    query.equalTo("vol", selvol)
    query.find({
        success: function(results) {
            //alert("Successfully" + results[0].get("text"))
            $("#col2text").html(results[0].get("text"));
        }
    })
}

var highlighter;

function highlightSelectedText() {
    highlighter.highlightSelection("highlight");
}

function removeHighlightFromSelectedText() {
    highlighter.unhighlightSelection();
}

function addArticle(){
    highlightSelectedText();
    var range = rangy.createRange();
    var highlight_range = rangy.getSelection().getRangeAt(0).toCharacterRange(document.getElementById('col2text'))
    var highlight_start = highlight_range.start;
    var highlight_end = highlight_range.end;
    article_ranges.push(([highlight_start, highlight_end]));
    //var highlight_range = range.toCharacterRange("col2text")
    window.alert(highlight_start + " " + highlight_end);
}

function removeArticle(){

}

function spanClick(element){
    window.alert("Clicked inside article");
    var range = rangy.createRange();
    range.selectNodeContents(element);
    var sel = rangy.getSelection();
    sel.setSingleRange(range);
}

function replaceRange(s, start, end, substitute) {
    return s.substring(0, start) + substitute + s.substring(end);
}

function init() {

    Parse.initialize("Dxi3BvGT3mHiDC7B1YjeEuiUQKtWIeQNofT5FIIx", "QG352rxcZvLrYeV4jOCsIZvM8mIeQyhvHzDNINAb");

    VolObject = Parse.Object.extend("Text");
    ArtObject = Parse.Object.extend("Article");
    volObject = new VolObject();
    artObject = new ArtObject();

    rangy.init();

    highlighter = rangy.createHighlighter();

    applier = rangy.createClassApplier("highlight");

    highlighter.addClassApplier(rangy.createClassApplier("highlight", {
        ignoreWhiteSpace: true,
        elementAttributes: {onclick:"spanClick(this)"},
        tagNames: ["span", "a"]
    }));

}<|endoftext|>


<commit_before>/**
 * @file MaterialEditableSelect component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';

import EditableSelect from '../EditableSelect';
import MaterialFieldSeparator from '../_MaterialFieldSeparator';
import Theme from '../Theme';

import Util from '../_vendors/Util';

export default class MaterialEditableSelect extends Component {

    static Theme = Theme;

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.state = {
            value: '',
            filter: props.filterInitValue,
            isFocus: false,
            isHover: false
        };

        this.triggerFocusHandler = ::this.triggerFocusHandler;
        this.popupClosedHandler = ::this.popupClosedHandler;
        this.triggerFilterChangeHandler = ::this.triggerFilterChangeHandler;
        this.triggerChangeHandler = ::this.triggerChangeHandler;
        this.triggerMouseOverHandler = ::this.triggerMouseOverHandler;
        this.triggerMouseOutHandler = ::this.triggerMouseOutHandler;
        this.closePopup = ::this.closePopup;

    }

    triggerFocusHandler(...args) {
        this.setState({
            isFocus: true
        }, () => {
            const {onFocus} = this.props;
            onFocus && onFocus(...args);
        });
    }

    triggerBlurHandler(...args) {
        this.setState({
            isFocus: false
        }, () => {
            const {onBlur} = this.props;
            onBlur && onBlur(...args);
        });
    }

    popupClosedHandler() {
        this.setState({
            isFocus: false
        });
    }

    triggerFilterChangeHandler(filter) {
        this.setState({
            filter
        }, () => {
            const {onFilterChange} = this.props;
            onFilterChange && onFilterChange(filter);
        });
    }

    triggerChangeHandler(value) {
        this.setState({
            value
        }, () => {
            const {onChange} = this.props;
            onChange && onChange(value);
        });
    }

    triggerMouseOverHandler(...args) {

        this.setState({
            isHover: true
        }, () => {
            const {onMouseOver} = this.props;
            onMouseOver && onMouseOver(...args);
        });
    }

    triggerMouseOutHandler(...args) {
        this.setState({
            isHover: false
        }, () => {
            const {onMouseOut} = this.props;
            onMouseOut && onMouseOut(...args);
        });
    }

    closePopup() {
        this.refs.editableSelect && this.refs.editableSelect.closePopup();
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.value !== this.state.value) {
            this.setState({
                value: nextProps.value
            });
        }
    }

    componentDidMount() {
        this.setState({
            value: this.props.value
        });
    }

    render() {

        const {
                className, style, theme, label, isLabelAnimated, popupClassName,
                ...restProps
            } = this.props,
            {isFocus, isHover, value} = this.state,

            wrapperClassName = (isLabelAnimated ? ' animated' : '') + (label ? ' has-label' : '')
                + (value ? ' has-value' : '') + (isFocus ? ' focused' : '') + (className ? ' ' + className : '');

        return (

                {
                    label ?

                            {label}

                        :
                        null
                }

                <EditableSelect {...restProps}
                                ref="editableSelect"
                                popupClassName={'material-editable-select-popup ' + popupClassName}
                                theme={theme}
                                value={value}
                                onFocus={this.triggerFocusHandler}
                                onBlur={this.triggerBlurHandler}
                                onPopupClosed={this.popupClosedHandler}
                                onTriggerMouseOver={this.triggerMouseOverHandler}
                                onTriggerMouseOut={this.triggerMouseOutHandler}
                                onFilterChange={this.triggerFilterChangeHandler}
                                onChange={this.triggerChangeHandler}/>

                <MaterialFieldSeparator theme={theme}
                                        isHover={isHover}
                                        isFocus={isFocus}/>

        );

    }
};

MaterialEditableSelect.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * The CSS class name of the popup element.
     */
    popupClassName: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * Override the styles of the popup element.
     */
    popupStyle: PropTypes.object,

    /**
     * The label of the text field.
     */
    label: PropTypes.any,

    /**
     * The animate of the text field.
     */
    isLabelAnimated: PropTypes.bool,

    /**
     * The name of the editableSelect.
     */
    name: PropTypes.string,

    /**
     * The value of the editableSelect.
     */
    value: PropTypes.any,

    /**
     * You can create a complicated renderer callback instead of value and desc prop.
     */
    renderer: PropTypes.func,

    /**
     * The placeholder of the editableSelect.
     */
    placeholder: PropTypes.string,

    /**
     * The options data.
     */
    data: PropTypes.oneOfType([

        // not grouped
        PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.shape({

            /**
             * The CSS class name of the list button.
             */
            className: PropTypes.string,

            /**
             * Override the styles of the list button.
             */
            style: PropTypes.object,

            /**
             * The theme of the list button.
             */
            theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

            /**
             * The text value of the list button.Type can be string or number.
             */
            value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

            /**
             * The desc value of the list button. Type can be string or number.
             */
            desc: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

            /**
             * If true, the list button will be disabled.
             */
            disabled: PropTypes.bool,

            /**
             * If true,the button will be have loading effect.
             */
            isLoading: PropTypes.bool,

            /**
             * If true,the element's ripple effect will be disabled.
             */
            disableTouchRipple: PropTypes.bool,

            /**
             * Use this property to display an icon. It will display on the left.
             */
            iconCls: PropTypes.string,

            /**
             * Use this property to display an icon. It will display on the right.
             */
            rightIconCls: PropTypes.string,

            /**
             * You can create a complicated renderer callback instead of value and desc prop.
             */
            renderer: PropTypes.func,

            /**
             * Callback function fired when a list item touch-tapped.
             */
            onTouchTap: PropTypes.func

        }), PropTypes.string, PropTypes.number])),

        // grouped
        PropTypes.array

    ]).isRequired,

    /**
     * The invalid message of editableSelect.
     */
    invalidMsg: PropTypes.string,

    /**
     * If true,the editableSelect will be disabled.
     */
    disabled: PropTypes.bool,

    /**
     * The value field name in data. (default: "value")
     */
    valueField: PropTypes.string,

    /**
     * The description field name in data. (default: "desc")
     */
    descriptionField: PropTypes.string,

    /**
     * The message of the editableSelect.
     */
    infoMsg: PropTypes.string,

    /**
     * Use this function to format the options's text.
     */
    textFormat: PropTypes.func,

    /**
     * If true,the drop-down box automatically closed after selection.
     */
    autoClose: PropTypes.bool,

    /**
     * If true,the drop-down box will have search input.
     */
    useFilter: PropTypes.bool,

    /**
     * The message of no matching option.
     */
    noMatchedMsg: PropTypes.string,

    /**
     * The theme of editableSelect.Can be primary,highlight,success,warning,error.
     */
    triggerTheme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    /**
     * If true,the drop-down box will be have group selection.
     */
    isGrouped: PropTypes.bool,

    /**
     * Callback function fired when the button is touch-tapped.
     */
    onItemTouchTap: PropTypes.func,

    /**
     * Callback function fired when a menu item is selected.
     */
    onChange: PropTypes.func

};

MaterialEditableSelect.defaultProps = {

    className: '',
    popupClassName: '',
    style: null,
    popupStyle: null,

    name: '',
    value: '',
    label: null,
    isLabelAnimated: true,
    placeholder: 'Please select ...',
    data: [],
    invalidMsg: '',
    disabled: false,

    valueField: 'value',
    displayField: 'text',
    descriptionField: 'desc',

    infoMsg: '',
    autoClose: true,
    useFilter: false,
    noMatchedMsg: '',
    triggerTheme: Theme.DEFAULT,
    isGrouped: false,

};<commit_msg>update MaterialEditableSelect<commit_after>/**
 * @file MaterialEditableSelect component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';

import EditableSelect from '../EditableSelect';
import MaterialFieldSeparator from '../_MaterialFieldSeparator';
import Theme from '../Theme';

import Util from '../_vendors/Util';

class MaterialEditableSelect extends Component {

    static Theme = Theme;

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.state = {
            value: '',
            filter: props.filterInitValue,
            isFocus: false,
            isHover: false
        };

        this.triggerFocusHandler = ::this.triggerFocusHandler;
        this.popupClosedHandler = ::this.popupClosedHandler;
        this.triggerFilterChangeHandler = ::this.triggerFilterChangeHandler;
        this.triggerChangeHandler = ::this.triggerChangeHandler;
        this.triggerMouseOverHandler = ::this.triggerMouseOverHandler;
        this.triggerMouseOutHandler = ::this.triggerMouseOutHandler;
        this.closePopup = ::this.closePopup;

    }

    triggerFocusHandler(...args) {
        this.setState({
            isFocus: true
        }, () => {
            const {onFocus} = this.props;
            onFocus && onFocus(...args);
        });
    }

    triggerBlurHandler(...args) {
        this.setState({
            isFocus: false
        }, () => {
            const {onBlur} = this.props;
            onBlur && onBlur(...args);
        });
    }

    popupClosedHandler() {
        this.setState({
            isFocus: false
        });
    }

    triggerFilterChangeHandler(filter) {
        this.setState({
            filter
        }, () => {
            const {onFilterChange} = this.props;
            onFilterChange && onFilterChange(filter);
        });
    }

    triggerChangeHandler(value) {
        this.setState({
            value
        }, () => {
            const {onChange} = this.props;
            onChange && onChange(value);
        });
    }

    triggerMouseOverHandler(...args) {

        this.setState({
            isHover: true
        }, () => {
            const {onMouseOver} = this.props;
            onMouseOver && onMouseOver(...args);
        });
    }

    triggerMouseOutHandler(...args) {
        this.setState({
            isHover: false
        }, () => {
            const {onMouseOut} = this.props;
            onMouseOut && onMouseOut(...args);
        });
    }

    closePopup() {
        this.refs.editableSelect && this.refs.editableSelect.closePopup();
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.value !== this.state.value) {
            this.setState({
                value: nextProps.value
            });
        }
    }

    componentDidMount() {
        this.setState({
            value: this.props.value
        });
    }

    render() {

        const {
                className, style, theme, label, isLabelAnimated, popupClassName,
                ...restProps
            } = this.props,
            {isFocus, isHover, value} = this.state,

            wrapperClassName = (isLabelAnimated ? ' animated' : '') + (label ? ' has-label' : '')
                + (value ? ' has-value' : '') + (isFocus ? ' focused' : '') + (className ? ' ' + className : '');

        return (

                {
                    label ?

                            {label}

                        :
                        null
                }

                <EditableSelect {...restProps}
                                ref="editableSelect"
                                popupClassName={'material-editable-select-popup ' + popupClassName}
                                theme={theme}
                                value={value}
                                onFocus={this.triggerFocusHandler}
                                onBlur={this.triggerBlurHandler}
                                onPopupClosed={this.popupClosedHandler}
                                onTriggerMouseOver={this.triggerMouseOverHandler}
                                onTriggerMouseOut={this.triggerMouseOutHandler}
                                onFilterChange={this.triggerFilterChangeHandler}
                                onChange={this.triggerChangeHandler}/>

                <MaterialFieldSeparator theme={theme}
                                        isHover={isHover}
                                        isFocus={isFocus}/>

        );

    }
};

MaterialEditableSelect.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * The CSS class name of the popup element.
     */
    popupClassName: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * Override the styles of the popup element.
     */
    popupStyle: PropTypes.object,

    /**
     * The label of the text field.
     */
    label: PropTypes.any,

    /**
     * The animate of the text field.
     */
    isLabelAnimated: PropTypes.bool,

    /**
     * The name of the editableSelect.
     */
    name: PropTypes.string,

    /**
     * The value of the editableSelect.
     */
    value: PropTypes.any,

    /**
     * You can create a complicated renderer callback instead of value and desc prop.
     */
    renderer: PropTypes.func,

    /**
     * The placeholder of the editableSelect.
     */
    placeholder: PropTypes.string,

    /**
     * The options data.
     */
    data: PropTypes.oneOfType([

        // not grouped
        PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.shape({

            /**
             * The CSS class name of the list button.
             */
            className: PropTypes.string,

            /**
             * Override the styles of the list button.
             */
            style: PropTypes.object,

            /**
             * The theme of the list button.
             */
            theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

            /**
             * The text value of the list button.Type can be string or number.
             */
            value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

            /**
             * The desc value of the list button. Type can be string or number.
             */
            desc: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

            /**
             * If true, the list button will be disabled.
             */
            disabled: PropTypes.bool,

            /**
             * If true,the button will be have loading effect.
             */
            isLoading: PropTypes.bool,

            /**
             * If true,the element's ripple effect will be disabled.
             */
            disableTouchRipple: PropTypes.bool,

            /**
             * Use this property to display an icon. It will display on the left.
             */
            iconCls: PropTypes.string,

            /**
             * Use this property to display an icon. It will display on the right.
             */
            rightIconCls: PropTypes.string,

            /**
             * You can create a complicated renderer callback instead of value and desc prop.
             */
            renderer: PropTypes.func,

            /**
             * Callback function fired when a list item touch-tapped.
             */
            onTouchTap: PropTypes.func

        }), PropTypes.string, PropTypes.number])),

        // grouped
        PropTypes.array

    ]).isRequired,

    /**
     * The invalid message of editableSelect.
     */
    invalidMsg: PropTypes.string,

    /**
     * If true,the editableSelect will be disabled.
     */
    disabled: PropTypes.bool,

    /**
     * The value field name in data. (default: "value")
     */
    valueField: PropTypes.string,

    /**
     * The description field name in data. (default: "desc")
     */
    descriptionField: PropTypes.string,

    /**
     * The message of the editableSelect.
     */
    infoMsg: PropTypes.string,

    /**
     * Use this function to format the options's text.
     */
    textFormat: PropTypes.func,

    /**
     * If true,the drop-down box automatically closed after selection.
     */
    autoClose: PropTypes.bool,

    /**
     * If true,the drop-down box will have search input.
     */
    useFilter: PropTypes.bool,

    /**
     * The message of no matching option.
     */
    noMatchedMsg: PropTypes.string,

    /**
     * The theme of editableSelect.Can be primary,highlight,success,warning,error.
     */
    triggerTheme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    /**
     * If true,the drop-down box will be have group selection.
     */
    isGrouped: PropTypes.bool,

    /**
     * Callback function fired when the button is touch-tapped.
     */
    onItemTouchTap: PropTypes.func,

    /**
     * Callback function fired when a menu item is selected.
     */
    onChange: PropTypes.func

};

MaterialEditableSelect.defaultProps = {

    className: '',
    popupClassName: '',
    style: null,
    popupStyle: null,

    name: '',
    value: '',
    label: null,
    isLabelAnimated: true,
    placeholder: 'Please select ...',
    data: [],
    invalidMsg: '',
    disabled: false,

    valueField: 'value',
    displayField: 'text',
    descriptionField: 'desc',

    infoMsg: '',
    autoClose: true,
    useFilter: false,
    noMatchedMsg: '',
    triggerTheme: Theme.DEFAULT,
    isGrouped: false

};

export default MaterialEditableSelect;<|endoftext|>


<commit_before>'use strict';

const isNil = require('lodash.isnil');
const concat = require('lodash.concat');

const { util: { checkType, invalidParam }, consts: { MULTI_MATCH_TYPE } } = require('../../core');
const FullTextQueryBase = require('./full-text-query-base');
const { validateRewiteMethod } = require('../helper');

const ES_REF_URL =
    'https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html';

const invalidTypeParam = invalidParam(ES_REF_URL, 'type', MULTI_MATCH_TYPE);
const invalidOperatorParam = invalidParam(ES_REF_URL, 'operator', "'and' or 'or'");
const invalidBehaviorParam = invalidParam(ES_REF_URL, 'behavior', "'all' or 'one'");

/**
 * A `MultiMatchQuery` query builds further on top of the
 * `MultiMatchQuery` by allowing multiple fields to be specified.
 * The idea here is to allow to more easily build a concise match type query
 * over multiple fields instead of using a relatively more expressive query
 * by using multiple match queries within a bool query.
 *
 * [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html)
 *
 * @example
 * const qry = bob.multiMatchQuery(['subject', 'message'], 'this is a test');
 *
 * @param {Array<string>|string} fields The fields to be queried
 * @param {string=} queryString The query string
 *
 * @extends FullTextQueryBase
 */
class MultiMatchQuery extends FullTextQueryBase {
    // Extremely similar to match query.
    // mixins are one way to go about it.
    // repeating code for now

    // eslint-disable-next-line require-jsdoc
    constructor(fields, queryString) {
        super('multi_match', queryString);

        // This field is required
        // Avoid checking for key in `this.field`
        this._queryOpts.fields = [];

        if (!isNil(fields)) {
            if (Array.isArray(fields)) this.fields(fields);
            else this.field(fields);
        }
    }

    /**
     * Appends given field to the list of fields to search against.
     * Fields can be specified with wildcards.
     * Individual fields can be boosted with the caret (^) notation.
     * Example - `"subject^3"`
     *
     * @param {string} field One of the fields to be queried
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    field(field) {
        this._queryOpts.fields.push(field);
        return this;
    }

    /**
     * Appends given fields to the list of fields to search against.
     * Fields can be specified with wildcards.
     * Individual fields can be boosted with the caret (^) notation.
     *
     * @example
     * // Boost individual fields with caret `^` notation
     * const qry = bob.multiMatchQuery(['subject^3', 'message'], 'this is a test');
     *
     * @example
     * // Specify fields with wildcards
     *const qry = bob.multiMatchQuery(['title', '*_name'], 'Will Smith');
     *
     * @param {Array<string>} fields The fields to be queried
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    fields(fields) {
        checkType(fields, Array);

        this._queryOpts.fields = concat(this._queryOpts.fields, fields);
        return this;
    }

    /**
     * Sets the type of multi match query. Valid values are:
     * - `best_fields` - (default) Finds documents which match any field,
     * but uses the `_score` from the best field.
     *
     * - `most_fields` - Finds documents which match any field and combines
     * the `_score` from each field.
     *
     * - `cross_fields` - Treats fields with the same `analyzer` as though
     * they were one big field. Looks for each word in *any* field
     *
     * - `phrase` - Runs a `match_phrase` query on each field and combines
     * the `_score` from each field.
     *
     * - `phrase_prefix` - Runs a `match_phrase_prefix` query on each field
     * and combines the `_score` from each field.
     *
     * @example
     * // Find the single best matching field
     * const qry = bob.multiMatchQuery(['subject', 'message'], 'brown fox')
     *     .type('best_fields')
     *     .tieBreaker(0.3);
     *
     * @example
     * // Query multiple fields analyzed differently for the same text
     * const qry = bob.multiMatchQuery(
     *     ['title', 'title.original', 'title.shingles'],
     *     'quick brown fox'
     * ).type('most_fields');
     *
     * @example
     * // Run a `match_phrase_prefix` query on multiple fields
     * const qry = bob.multiMatchQuery(
     *     ['subject', 'message'],
     *     'quick brown f'
     * ).type('phrase_prefix');
     *
     * @example
     * // All terms must be present in at least one field for document to match
     * const qry = bob.multiMatchQuery(['first_name', 'last_name'], 'Will Smith')
     *     .type('cross_fields')
     *     .operator('and');
     *
     * @param {string} type Can be one of `best_fields`, `most_fields`,
     * `cross_fields`, `phrase`, and `phrase_prefix`. Default is `best_fields`.
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    type(type) {
        if (isNil(type)) invalidTypeParam(type);

        const typeLower = type.toLowerCase();
        if (!MULTI_MATCH_TYPE.has(typeLower)) invalidTypeParam(type);

        this._queryOpts.type = typeLower;
        return this;
    }

    /**
     * The tie breaker value. The tie breaker capability allows results
     * that include the same term in multiple fields to be judged better than
     * results that include this term in only the best of those multiple
     * fields, without confusing this with the better case of two different
     * terms in the multiple fields. Default: `0.0`.
     *
     * @param {number} factor
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    tieBreaker(factor) {
        this._queryOpts.tie_breaker = factor;
        return this;
    }

    /**
     * The operator to be used in the boolean query which is constructed
     * by analyzing the text provided. The `operator` flag can be set to `or` or
     * `and` to control the boolean clauses (defaults to `or`).
     *
     * @param {string} operator Can be `and`/`or`. Default is `or`.
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    operator(operator) {
        if (isNil(operator)) invalidOperatorParam(operator);

        const operatorLower = operator.toLowerCase();
        if (operatorLower !== 'and' && operatorLower !== 'or') {
            invalidOperatorParam(operator);
        }

        this._queryOpts.operator = operatorLower;
        return this;
    }

    /**
     * Sets the `lenient` parameter which allows to ignore exceptions caused
     * by data-type mismatches such as trying to query a numeric field with a
     * text query string when set to `true`.
     *
     * @param {boolean} enable Defaules to `false`
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    lenient(enable) {
        this._queryOpts.lenient = enable;
        return this;
    }

    // phrase_slop is a synonym for slop.
    // haven't added method for it..

    /**
     * Configures the `slop`(default is 0) for matching terms in any order.
     * Transposed terms have a slop of 2.
     *
     * @param {number} slop A positive integer value, defaults is 0.
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    slop(slop) {
        this._queryOpts.slop = slop;
        return this;
    }

    /**
     * Sets the `fuzziness` parameter which is interpreted as a Levenshtein Edit Distance —
     * the number of one character changes that need to be made to one string to make it
     * the same as another string.
     *
     * The `fuzziness` parameter cannot be used with the `phrase`, `phrase_prefix`
     * or `cross_fields` type.
     *
     * @param {number|string} factor Can be specified either as a number, or the maximum
     * number of edits, or as `AUTO` which generates an edit distance based on the length
     * of the term.
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    fuzziness(factor) {
        this._queryOpts.fuzziness = factor;
        return this;
    }

    /**
     * Sets the prefix length for a fuzzy prefix `MultiMatchQuery`
     *
     * @param {number} len
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    prefixLength(len) {
        this._queryOpts.prefix_length = len;
        return this;
    }

    /**
     * Sets the max expansions for a fuzzy prefix `MultiMatchQuery`
     *
     * @param {number} limit
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    maxExpansions(limit) {
        this._queryOpts.max_expansions = limit;
        return this;
    }

    /**
     * Sets the rewrite method. Valid values are:
     * - `constant_score` - tries to pick the best constant-score rewrite
     *  method based on term and document counts from the query.
     *  Synonyms - `constant_score_auto`, `constant_score_filter`
     *
     * - `scoring_boolean` - translates each term into boolean should and
     *  keeps the scores as computed by the query
     *
     * - `constant_score_boolean` - same as `scoring_boolean`, expect no scores
     *  are computed.
     *
     * - `constant_score_filter` - first creates a private Filter, by visiting
     *  each term in sequence and marking all docs for that term
     *
     * - `top_terms_boost_N` - first translates each term into boolean should
     *  and scores are only computed as the boost using the top N
     *  scoring terms. Replace N with an integer value.
     *
     * - `top_terms_N` - first translates each term into boolean should
     *  and keeps the scores as computed by the query. Only the top N
     *  scoring terms are used. Replace N with an integer value.
     *
     * Default is `constant_score`.
     *
     * This is an advanced option, use with care.
     *
     * @param {string} method The rewrite method as a string.
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     * @throws {Error} If the given `rewrite` method is not valid.
     */
    rewrite(method) {
        validateRewiteMethod(method, 'rewrite', ES_REF_URL);

        this._queryOpts.rewrite = method;
        return this;
    }

    /**
     * Sets the fuzzy rewrite method. Valid values are:
     * - `constant_score` - tries to pick the best constant-score rewrite
     *  method based on term and document counts from the query.
     *  Synonyms - `constant_score_auto`, `constant_score_filter`
     *
     * - `scoring_boolean` - translates each term into boolean should and
     *  keeps the scores as computed by the query
     *
     * - `constant_score_boolean` - same as `scoring_boolean`, expect no scores
     *  are computed.
     *
     * - `constant_score_filter` - first creates a private Filter, by visiting
     *  each term in sequence and marking all docs for that term
     *
     * - `top_terms_boost_N` - first translates each term into boolean should
     *  and scores are only computed as the boost using the top N
     *  scoring terms. Replace N with an integer value.
     *
     * - `top_terms_N` - first translates each term into boolean should
     *  and keeps the scores as computed by the query. Only the top N
     *  scoring terms are used. Replace N with an integer value.
     *
     * Default is `constant_score`.
     *
     * This is an advanced option, use with care.
     *
     * @param {string} method The rewrite method as a string.
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     * @throws {Error} If the given `fuzzy_rewrite` method is not valid.
     */
    fuzzyRewrite(method) {
        validateRewiteMethod(method, 'fuzzy_rewrite', ES_REF_URL);

        this._queryOpts.fuzzy_rewrite = method;
        return this;
    }

    /**
     * If the analyzer used removes all tokens in a query like a `stop` filter does,
     * the default behavior is to match no documents at all. In order to change that
     * the `zero_terms_query` option can be used, which accepts `none` (default) and `all`
     * which corresponds to a `match_all` query.
     *
     * @param {string} behavior A no match action, `all` or `none`. Default is `none`.
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    zeroTermsQuery(behavior) {
        if (isNil(behavior)) invalidBehaviorParam(behavior);

        const behaviorLower = behavior.toLowerCase();
        if (behaviorLower !== 'all' && behaviorLower !== 'none') {
            invalidBehaviorParam(behavior);
        }

        this._queryOpts.zero_terms_query = behavior;
        return this;
    }

    /**
     * Allows specifying an absolute or relative document frequency where high frequency
     * terms are moved into an optional subquery and are only scored if one of the
     * low frequency (below the cutoff) terms in the case of an `or` operator or
     * all of the low frequency terms in the case of an `and` operator match.
     *
     * @param {number} frequency It can either be relative to the total number of documents
     * if in the range `[0..1)` or absolute if greater or equal to `1.0`.
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    cutoffFrequency(frequency) {
        this._queryOpts.cutoff_frequency = frequency;
        return this;
    }
}

module.exports = MultiMatchQuery;
<commit_msg>docs(MultiMatchQuery): Annotate fields param to constructor as optional<commit_after>'use strict';

const isNil = require('lodash.isnil');
const concat = require('lodash.concat');

const { util: { checkType, invalidParam }, consts: { MULTI_MATCH_TYPE } } = require('../../core');
const FullTextQueryBase = require('./full-text-query-base');
const { validateRewiteMethod } = require('../helper');

const ES_REF_URL =
    'https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html';

const invalidTypeParam = invalidParam(ES_REF_URL, 'type', MULTI_MATCH_TYPE);
const invalidOperatorParam = invalidParam(ES_REF_URL, 'operator', "'and' or 'or'");
const invalidBehaviorParam = invalidParam(ES_REF_URL, 'behavior', "'all' or 'one'");

/**
 * A `MultiMatchQuery` query builds further on top of the
 * `MultiMatchQuery` by allowing multiple fields to be specified.
 * The idea here is to allow to more easily build a concise match type query
 * over multiple fields instead of using a relatively more expressive query
 * by using multiple match queries within a bool query.
 *
 * [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html)
 *
 * @example
 * const qry = bob.multiMatchQuery(['subject', 'message'], 'this is a test');
 *
 * @param {Array<string>|string=} fields The fields to be queried
 * @param {string=} queryString The query string
 *
 * @extends FullTextQueryBase
 */
class MultiMatchQuery extends FullTextQueryBase {
    // Extremely similar to match query.
    // mixins are one way to go about it.
    // repeating code for now

    // eslint-disable-next-line require-jsdoc
    constructor(fields, queryString) {
        super('multi_match', queryString);

        // This field is required
        // Avoid checking for key in `this.field`
        this._queryOpts.fields = [];

        if (!isNil(fields)) {
            if (Array.isArray(fields)) this.fields(fields);
            else this.field(fields);
        }
    }

    /**
     * Appends given field to the list of fields to search against.
     * Fields can be specified with wildcards.
     * Individual fields can be boosted with the caret (^) notation.
     * Example - `"subject^3"`
     *
     * @param {string} field One of the fields to be queried
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    field(field) {
        this._queryOpts.fields.push(field);
        return this;
    }

    /**
     * Appends given fields to the list of fields to search against.
     * Fields can be specified with wildcards.
     * Individual fields can be boosted with the caret (^) notation.
     *
     * @example
     * // Boost individual fields with caret `^` notation
     * const qry = bob.multiMatchQuery(['subject^3', 'message'], 'this is a test');
     *
     * @example
     * // Specify fields with wildcards
     *const qry = bob.multiMatchQuery(['title', '*_name'], 'Will Smith');
     *
     * @param {Array<string>} fields The fields to be queried
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    fields(fields) {
        checkType(fields, Array);

        this._queryOpts.fields = concat(this._queryOpts.fields, fields);
        return this;
    }

    /**
     * Sets the type of multi match query. Valid values are:
     * - `best_fields` - (default) Finds documents which match any field,
     * but uses the `_score` from the best field.
     *
     * - `most_fields` - Finds documents which match any field and combines
     * the `_score` from each field.
     *
     * - `cross_fields` - Treats fields with the same `analyzer` as though
     * they were one big field. Looks for each word in *any* field
     *
     * - `phrase` - Runs a `match_phrase` query on each field and combines
     * the `_score` from each field.
     *
     * - `phrase_prefix` - Runs a `match_phrase_prefix` query on each field
     * and combines the `_score` from each field.
     *
     * @example
     * // Find the single best matching field
     * const qry = bob.multiMatchQuery(['subject', 'message'], 'brown fox')
     *     .type('best_fields')
     *     .tieBreaker(0.3);
     *
     * @example
     * // Query multiple fields analyzed differently for the same text
     * const qry = bob.multiMatchQuery(
     *     ['title', 'title.original', 'title.shingles'],
     *     'quick brown fox'
     * ).type('most_fields');
     *
     * @example
     * // Run a `match_phrase_prefix` query on multiple fields
     * const qry = bob.multiMatchQuery(
     *     ['subject', 'message'],
     *     'quick brown f'
     * ).type('phrase_prefix');
     *
     * @example
     * // All terms must be present in at least one field for document to match
     * const qry = bob.multiMatchQuery(['first_name', 'last_name'], 'Will Smith')
     *     .type('cross_fields')
     *     .operator('and');
     *
     * @param {string} type Can be one of `best_fields`, `most_fields`,
     * `cross_fields`, `phrase`, and `phrase_prefix`. Default is `best_fields`.
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    type(type) {
        if (isNil(type)) invalidTypeParam(type);

        const typeLower = type.toLowerCase();
        if (!MULTI_MATCH_TYPE.has(typeLower)) invalidTypeParam(type);

        this._queryOpts.type = typeLower;
        return this;
    }

    /**
     * The tie breaker value. The tie breaker capability allows results
     * that include the same term in multiple fields to be judged better than
     * results that include this term in only the best of those multiple
     * fields, without confusing this with the better case of two different
     * terms in the multiple fields. Default: `0.0`.
     *
     * @param {number} factor
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    tieBreaker(factor) {
        this._queryOpts.tie_breaker = factor;
        return this;
    }

    /**
     * The operator to be used in the boolean query which is constructed
     * by analyzing the text provided. The `operator` flag can be set to `or` or
     * `and` to control the boolean clauses (defaults to `or`).
     *
     * @param {string} operator Can be `and`/`or`. Default is `or`.
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    operator(operator) {
        if (isNil(operator)) invalidOperatorParam(operator);

        const operatorLower = operator.toLowerCase();
        if (operatorLower !== 'and' && operatorLower !== 'or') {
            invalidOperatorParam(operator);
        }

        this._queryOpts.operator = operatorLower;
        return this;
    }

    /**
     * Sets the `lenient` parameter which allows to ignore exceptions caused
     * by data-type mismatches such as trying to query a numeric field with a
     * text query string when set to `true`.
     *
     * @param {boolean} enable Defaules to `false`
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    lenient(enable) {
        this._queryOpts.lenient = enable;
        return this;
    }

    // phrase_slop is a synonym for slop.
    // haven't added method for it..

    /**
     * Configures the `slop`(default is 0) for matching terms in any order.
     * Transposed terms have a slop of 2.
     *
     * @param {number} slop A positive integer value, defaults is 0.
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    slop(slop) {
        this._queryOpts.slop = slop;
        return this;
    }

    /**
     * Sets the `fuzziness` parameter which is interpreted as a Levenshtein Edit Distance —
     * the number of one character changes that need to be made to one string to make it
     * the same as another string.
     *
     * The `fuzziness` parameter cannot be used with the `phrase`, `phrase_prefix`
     * or `cross_fields` type.
     *
     * @param {number|string} factor Can be specified either as a number, or the maximum
     * number of edits, or as `AUTO` which generates an edit distance based on the length
     * of the term.
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    fuzziness(factor) {
        this._queryOpts.fuzziness = factor;
        return this;
    }

    /**
     * Sets the prefix length for a fuzzy prefix `MultiMatchQuery`
     *
     * @param {number} len
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    prefixLength(len) {
        this._queryOpts.prefix_length = len;
        return this;
    }

    /**
     * Sets the max expansions for a fuzzy prefix `MultiMatchQuery`
     *
     * @param {number} limit
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    maxExpansions(limit) {
        this._queryOpts.max_expansions = limit;
        return this;
    }

    /**
     * Sets the rewrite method. Valid values are:
     * - `constant_score` - tries to pick the best constant-score rewrite
     *  method based on term and document counts from the query.
     *  Synonyms - `constant_score_auto`, `constant_score_filter`
     *
     * - `scoring_boolean` - translates each term into boolean should and
     *  keeps the scores as computed by the query
     *
     * - `constant_score_boolean` - same as `scoring_boolean`, expect no scores
     *  are computed.
     *
     * - `constant_score_filter` - first creates a private Filter, by visiting
     *  each term in sequence and marking all docs for that term
     *
     * - `top_terms_boost_N` - first translates each term into boolean should
     *  and scores are only computed as the boost using the top N
     *  scoring terms. Replace N with an integer value.
     *
     * - `top_terms_N` - first translates each term into boolean should
     *  and keeps the scores as computed by the query. Only the top N
     *  scoring terms are used. Replace N with an integer value.
     *
     * Default is `constant_score`.
     *
     * This is an advanced option, use with care.
     *
     * @param {string} method The rewrite method as a string.
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     * @throws {Error} If the given `rewrite` method is not valid.
     */
    rewrite(method) {
        validateRewiteMethod(method, 'rewrite', ES_REF_URL);

        this._queryOpts.rewrite = method;
        return this;
    }

    /**
     * Sets the fuzzy rewrite method. Valid values are:
     * - `constant_score` - tries to pick the best constant-score rewrite
     *  method based on term and document counts from the query.
     *  Synonyms - `constant_score_auto`, `constant_score_filter`
     *
     * - `scoring_boolean` - translates each term into boolean should and
     *  keeps the scores as computed by the query
     *
     * - `constant_score_boolean` - same as `scoring_boolean`, expect no scores
     *  are computed.
     *
     * - `constant_score_filter` - first creates a private Filter, by visiting
     *  each term in sequence and marking all docs for that term
     *
     * - `top_terms_boost_N` - first translates each term into boolean should
     *  and scores are only computed as the boost using the top N
     *  scoring terms. Replace N with an integer value.
     *
     * - `top_terms_N` - first translates each term into boolean should
     *  and keeps the scores as computed by the query. Only the top N
     *  scoring terms are used. Replace N with an integer value.
     *
     * Default is `constant_score`.
     *
     * This is an advanced option, use with care.
     *
     * @param {string} method The rewrite method as a string.
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     * @throws {Error} If the given `fuzzy_rewrite` method is not valid.
     */
    fuzzyRewrite(method) {
        validateRewiteMethod(method, 'fuzzy_rewrite', ES_REF_URL);

        this._queryOpts.fuzzy_rewrite = method;
        return this;
    }

    /**
     * If the analyzer used removes all tokens in a query like a `stop` filter does,
     * the default behavior is to match no documents at all. In order to change that
     * the `zero_terms_query` option can be used, which accepts `none` (default) and `all`
     * which corresponds to a `match_all` query.
     *
     * @param {string} behavior A no match action, `all` or `none`. Default is `none`.
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    zeroTermsQuery(behavior) {
        if (isNil(behavior)) invalidBehaviorParam(behavior);

        const behaviorLower = behavior.toLowerCase();
        if (behaviorLower !== 'all' && behaviorLower !== 'none') {
            invalidBehaviorParam(behavior);
        }

        this._queryOpts.zero_terms_query = behavior;
        return this;
    }

    /**
     * Allows specifying an absolute or relative document frequency where high frequency
     * terms are moved into an optional subquery and are only scored if one of the
     * low frequency (below the cutoff) terms in the case of an `or` operator or
     * all of the low frequency terms in the case of an `and` operator match.
     *
     * @param {number} frequency It can either be relative to the total number of documents
     * if in the range `[0..1)` or absolute if greater or equal to `1.0`.
     * @returns {MultiMatchQuery} returns `this` so that calls can be chained.
     */
    cutoffFrequency(frequency) {
        this._queryOpts.cutoff_frequency = frequency;
        return this;
    }
}

module.exports = MultiMatchQuery;
<|endoftext|>


<commit_before>export function getImageMetaDataFromApi(url) {
    return new Promise(async (resolve, reject) => {
        try {
            const response = await fetch(url, {
                method: 'HEAD'
            });
            const height = parseInt(response.headers.get('x-amz-meta-height'), 10);
            const width = parseInt(response.headers.get('x-amz-meta-width'), 10);
            if (height && width) {
                resolve({ height, width });
            } else {
                reject();
            }
        } catch (err) {
            reject(err);
        }
    });
}

export function getImageMetaDataFromPreview(url) {
    return new Promise(async (resolve, reject) => {
        try {
            const image = new Image();
            image.src = url;
            image.onload = () => {
                const height = image.naturalHeight;
                const width = image.naturalWidth;
                if (height && width) {
                    resolve({ height, width });
                } else {
                    reject();
                }
            };
        } catch (err) {
            reject(err);
        }
    });
}
<commit_msg>:ok_hand: Change promise to async-await<commit_after>export async function getImageMetaDataFromApi(url) {
    const response = await fetch(url, {
        method: 'HEAD',
    });

    const height = parseInt(response.headers.get('x-amz-meta-height'), 10);
    const width = parseInt(response.headers.get('x-amz-meta-width'), 10);

    if (height && width) {
        return {
            height,
            width,
        };
    }

    throw new Error();
}

export function getImageMetaDataFromPreview(url) {
    return new Promise(async (resolve, reject) => {
        try {
            const image = new Image();
            image.src = url;
            image.onload = () => {
                const height = image.naturalHeight;
                const width = image.naturalWidth;
                if (height && width) {
                    resolve({ height, width });
                } else {
                    reject();
                }
            };
        } catch (err) {
            reject(err);
        }
    });
}
<|endoftext|>


<commit_before>var SlackBot = require('slackbots');
var request = require('request');
var jsonfile = require('jsonfile')
var util = require('util')

var SLACKBOT_API_KEY = process.env.SLACKBOT_API_KEY;

// create a bot
var bot = new SlackBot({
    token: SLACKBOT_API_KEY,
    name: 'LOLtacle' // todo: load from env
});

var RIOT_API_KEY = process.env.RIOT_API_KEY;
var RIOT_API_ROOT_URL = 'https://na.api.pvp.net/';

///////////////////////
// riot api wrap
function makeCallURL(apicall, params) {
    var parametrizedCall = apicall;
    for (paramName in params) {
        parametrizedCall = parametrizedCall.replace('{' + paramName + '}', params[paramName]);
    }

    return RIOT_API_ROOT_URL + parametrizedCall + '?api_key=' + RIOT_API_KEY;
}

var SummonerByName = {
    url: 'api/lol/na/v1.4/summoner/by-name/{summonername}',
    parser: function(body) {
        players = JSON.parse(body);
        for (player in players) {
            return players[player];
        }
    }
}

var CurrentGame = {
    url: 'observer-mode/rest/consumer/getSpectatorGameInfo/NA1/{summonerId}',
    parser: function (body) {
        return JSON.parse(body);
    }
}

var LastGames = {
    url: '/api/lol/na/v1.3/game/by-summoner/{summonerId}/recent',
    parser: function (body) {
        return JSON.parse(body).games;
    }
}

var ChampionNameById = {
    url: '/api/lol/static-data/na/v1.2/champion/{id}',
    parser: function (body) {
        return JSON.parse(body).name;
    }
}

var workqueue = [];

var workdebugout = function(msg) {
    //console.log(msg);
}

var working = false;
var currentjob;
var pumpqueue = function () {

    workdebugout("queue has " + workqueue.length + " entries");

    if (workqueue.length && !currentjob) {
        currentjob = workqueue[0];
        workqueue.splice(0, 1);
        workdebugout("doing work for job " + currentjob.index);
        currentjob.run();
        workdebugout("running work for " + currentjob.index);
    }

    if (currentjob && currentjob.responded) {
        workdebugout("finished handling response for " + currentjob.index);
        currentjob = null;
    }
}

var workdelay = 2000;
setInterval(pumpqueue, workdelay);

var jobcounter = 0;
var doRequest = function (call, params, payloadFun, errorFun) {
    workdebugout("queueing " + makeCallURL(call.url, params));

    var job = {};
    job.index = jobcounter++;
    job.run = function () {
        var c = makeCallURL(call.url, params);
        workdebugout("requesting " + c);
        request(c,
            function(err, response, body) {
                if (err || response.statusCode != 200) {
                    if (response.statusCode == 429) {
                        console.log("too many requests");
                    }

                    workdebugout("error " + response.statusCode);

                    if (response.statusCode == 401) {
                        console.log("key error for RIOT API");
                    }

                    if (errorFun)
                        errorFun(err, response.statusCode);
                    job.responded = true;
                    return;
                }

                workdebugout("running " + c);
                payloadFun(call.parser(body));
                job.responded = true;
            }
        );
    };

    job.url = makeCallURL(call.url, params);

    workqueue.push(job);
}

///////////////////////
// slackbot api wrap

announceparams = {
    icon_url: 'https://avatars.slack-edge.com/2016-04-04/31985105652_fd9b150844870327c24b_48.png'
};

var announce = function(msg) {
    console.log(msg);

    bot.postMessageToChannel('leagueoflegends', msg, announceparams);
}

// lolbot api

var AnnounceSummonerState = function (summonerName) {
    doRequest(SummonerByName, { summonername: summonerName },
        function (summoner) {
            doRequest(CurrentGame, { summonerId: summoner.id },
                function (game) {
                    announce(summonerName + ' is currently playing a game of ' + game.gameMode);
                },
                function(error, response) {
                    if (response == 404) {
                        announce(summonerName + ' is not currently playing');
                    }
                }
            );
        }
    );
}

var summonersfile = 'summoners.json';

// should be loaded from persistence
var summoners = [];

var findSummonerByName = function (summonername) {
    for (s in summoners) {
        if (summoners[s].name.toLowerCase() === summonername.toLowerCase()) {
            return summoners[s];
        }
    }
}

var addSummoner = function(summoner) {
    summoners.push(summoner);
    SaveSummoners();
}

var removeSummonerByName = function (summonername) {
    var toRemove = -1;
    for (s in summoners) {
        if (summoners[s].name.toLowerCase() === summonername.toLowerCase()) {
            toRemove = s;
        }
    }

    summoners.splice(toRemove, 1);
    SaveSummoners();
}

var SaveSummoners = function () {
    jsonfile.writeFileSync(summonersfile, summoners);
}

var LoadSummoners = function (then) {
    summoners = jsonfile.readFileSync(summonersfile);

    var invalidSummoners = [];

    var donecount = 0;

    var didnothing = true;
    // refresh id's
    for (s in summoners) {

        if (!summoners[s].id) {
            didnothing = false;

            (function (summoner) {
                doRequest(SummonerByName, { summonername: summoner.name },
                    function(summoneronserver) {
                        summoner.id = summoneronserver.id;

                        SaveSummoners();

                        // the horror
                        donecount++;
                        if (then && donecount == summoners.length) {
                            then();
                        }
                    },
                    function(error, response) {
                        invalidSummoners.push(summoner);

                        // THE HORROR
                        donecount++;
                        if (then && donecount == summoners.length) {
                            then();
                        }
                    }
                );
            })(summoners[s]);
        }
    }

    if (didnothing) {
        then();
    }

    // todo : reconcile or remove invalidSummoners
}

var ObserveSummoner = function(summonername, then) {
    if (findSummonerByName(summonername)) {
        announce(summonername + " is already under observation");

        if (then)
            then();
    } else {
        doRequest(SummonerByName, { summonername: summonername },
            function(summoner) {
                var newSummoner = {
                    name: summoner.name,
                    id: summoner.id,
                    currentGame: null
                };

                announce("now observing " + summoner.name);

                doRequest(CurrentGame, { summonerId: summoner.id },
                    function(game) {
                        if (!summoner.currentGame || summoner.currentGame.gameId != game.gameId) {
                            announce(summoner.name + " is currently in a game");
                            summoner.currentGame = game;
                        }

                        addSummoner(newSummoner);
                    },
                    function(error, response) {
                        if (response === 404) {
                            // wasn't playing...
                        }

                        addSummoner(newSummoner);
                        console.log("error?" + summoner.id + " " + response);
                    }
                );

                if (then)
                    then();
            },
            function(error, response) {
                announce("unknown summoner " + summonername);

                if (then)
                    then();
            }
        );
    }
}

var DontObserveSummoner = function (summonername, then) {
    if (!findSummonerByName(summonername)) {
        announce(summonername + " is already not under observation");

        if (then)
            then();
    } else {
        removeSummonerByName(summonername);

        announce("not observing " + summonername + " anymore");

        if (then)
            then();
    }
}

var AnnounceCurrentGameStart = function (summoner) {
    if (summoner.gamestartsannounced && summoner.gamestartsannounced.indexOf(summoner.currentGame.gameId) != -1) {
        // already announced
        return;
    }
    if (!summoner.gamestartsannounced) {
        summoner.gamestartsannounced = [];
    }

    var champId;
    for (p in summoner.currentGame.participants) {
        if (summoner.currentGame.participants[p].summonerId == summoner.id) {
            champId = summoner.currentGame.participants[p].championId;
        }
    }

    summoner.gamestartsannounced.push(summoner.currentGame.gameId);
    SaveSummoners();

    doRequest(ChampionNameById, { id: champId },
        function (champname) {
            summoner.lastchamp = champname;
            announce(summoner.name + " has just started game mode " + summoner.currentGame.gameMode + " playing " + champname);
        });
}

var FormatGameTime = function(seconds){
    var m = Math.floor(seconds / 60);
    var s = Math.floor(seconds  % 60);
    return m + ':' + s;
}

var LastGame = function(summoner, games){

}

var AnnounceGameEnd = function (summoner, game) {
    var result = game.stats.win ? "won" : "lost";
    var score = game.stats.championsKilled + "/" + game.stats.numDeaths + "/" + game.stats.assists;

    announce(summoner.name + " has just " + result + " game mode " + game.gameMode + " of length " + FormatGameTime(game.stats.timePlayed) + " with score " + score + " on " + summoner.lastchamp);
}

var UpdateGameStates = function () {
    if (workqueue.length)
        return;

    for (s in summoners) {
        (function(summoner) {
            if (summoner.currentGame && !summoner.waitingforresult) {
                doRequest(CurrentGame, { summonerId: summoner.id },
                    function(game) {
                        // still playing
                        summoner.currentGame = game;
                    },
                    function(error, response) {
                        if (response === 404) {
                            // game ended
                            summoner.waitingforresult = true;
                        }
                    }
                )
            } else if (summoner.currentGame && summoner.waitingforresult) {
                doRequest(LastGames, { summonerId: summoner.id },
                    function (games) {
                        for (game in games) {
                            if (games[game].gameId == summoner.currentGame.gameId) {
                                AnnounceGameEnd(summoner, games[game]);
                                summoner.waitingforresult = false;
                                summoner.currentGame = false;
                            }
                        }
                    }
                )
            } else {
                doRequest(CurrentGame, { summonerId: summoner.id },
                    function (game) {
                        summoner.currentGame = game;
                        AnnounceCurrentGameStart(summoner);
                    },
                    function(error, response) {
                        if (response === 404) {
                            // wasn't playing...
                            //announce(summoner.name + " was not playing");
                        }
                    }
                )
            }
        })(summoners[s]);
    }
}

bot.on('start', function() {
    // load summoners
    LoadSummoners(function () { setInterval(UpdateGameStates, 10000); });

    //doRequest(LastGames, { summonerId: 19869001 },
    //    function(games) {
    //        AnnounceGameEnd(summoners[1], games[0]);
    //    });

});

bot.on('message', function (message) {
    if (message.type == 'message') {
        // todo : use an actual command parser
        var command = message.text.split(/[ ,]+/);

        if (command.length > 0 && command[0] == 'loltacle') {
            if (command[1] == 'observe') {
                var summonername = "";
                for (var i = 2; i < command.length; ++i) {
                    summonername += command[i] + " ";
                }
                summonername = summonername.trim();
                ObserveSummoner(summonername);
            }
            else if (command[1] == 'dontobserve') {
                var summonername = "";
                for (var i = 2; i < command.length; ++i) {
                    summonername += command[i] + " ";
                }
                summonername = summonername.trim();
                DontObserveSummoner(summonername);
            }
        }
    }
});
<commit_msg>updated with latest tweaks<commit_after>var SlackBot = require('slackbots');
var request = require('request');
var jsonfile = require('jsonfile')
var util = require('util')

var SLACKBOT_API_KEY = process.env.SLACKBOT_API_KEY;

// create a bot
var bot = new SlackBot({
    token: SLACKBOT_API_KEY,
    name: 'LOLtacle' // todo: load from env or config
});

var RIOT_API_KEY = process.env.RIOT_API_KEY;
var RIOT_API_ROOT_URL = 'https://na.api.pvp.net/';

///////////////////////
// riot api wrap
function makeCallURL(apicall, params) {
    var parametrizedCall = apicall;
    for (paramName in params) {
        parametrizedCall = parametrizedCall.replace('{' + paramName + '}', params[paramName]);
    }

    return RIOT_API_ROOT_URL + parametrizedCall + '?api_key=' + RIOT_API_KEY;
}

var SummonerByName = {
    url: 'api/lol/na/v1.4/summoner/by-name/{summonername}',
    parser: function(body) {
        players = JSON.parse(body);
        for (player in players) {
            return players[player];
        }
    }
}

var CurrentGame = {
    url: 'observer-mode/rest/consumer/getSpectatorGameInfo/NA1/{summonerId}',
    parser: function (body) {
        return JSON.parse(body);
    }
}

var LastGames = {
    url: '/api/lol/na/v1.3/game/by-summoner/{summonerId}/recent',
    parser: function (body) {
        return JSON.parse(body).games;
    }
}

var ChampionNameById = {
    url: '/api/lol/static-data/na/v1.2/champion/{id}',
    parser: function (body) {
        return JSON.parse(body).name;
    }
}

var workqueue = [];

var workdebugout = function(msg) {
    //console.log(msg);
}

var working = false;
var currentjob;
var pumpqueue = function () {

    workdebugout("queue has " + workqueue.length + " entries");

    if (workqueue.length && !currentjob) {
        currentjob = workqueue[0];
        workqueue.splice(0, 1);
        workdebugout("doing work for job " + currentjob.index);
        currentjob.run();
        workdebugout("running work for " + currentjob.index);
    }

    if (currentjob && currentjob.responded) {
        workdebugout("finished handling response for " + currentjob.index);
        currentjob = null;
    }
}

var workdelay = 2000;
setInterval(pumpqueue, workdelay);

var jobcounter = 0;
var doRequest = function (call, params, payloadFun, errorFun) {
    workdebugout("queueing " + makeCallURL(call.url, params));

    var job = {};
    job.index = jobcounter++;
    job.run = function () {
        var c = makeCallURL(call.url, params);
        workdebugout("requesting " + c);
        request(c,
            function(err, response, body) {
                if (err || response.statusCode != 200) {
                    if (err && response && response.statusCode == 429) {
                        console.log("too many requests");
                    }

                    workdebugout("error " + (response ? response.statusCode : "no response") );

                    var statuscode = response ? response.statusCode : 0;
                    if (statuscode == 401) {
                        console.log("key error for RIOT API");
                    }

                    if (errorFun)
                        errorFun(err, statuscode);
                    job.responded = true;
                    return;
                }

                workdebugout("running " + c);
                payloadFun(call.parser(body));
                job.responded = true;
            }
        );
    };

    job.url = makeCallURL(call.url, params);

    workqueue.push(job);
}

///////////////////////
// slackbot api wrap

announceparams = {
    icon_url: 'https://avatars.slack-edge.com/2016-04-17/35396496742_2e722be390b0de1bd9cc_48.png'
};

var announce = function(msg) {
    console.log(msg);

    bot.postMessageToChannel('leagueoflegends', msg, announceparams);
}

// lolbot api

var summonersfile = 'summoners.json';

// should be loaded from persistence
var summoners = [];

var findSummonerByName = function (summonername) {
    for (s in summoners) {
        if (summoners[s].name.toLowerCase() === summonername.toLowerCase()) {
            return summoners[s];
        }
    }
}

var addSummoner = function(summoner) {
    summoners.push(summoner);
    SaveSummoners();
}

var removeSummonerByName = function (summonername) {
    var toRemove = -1;
    for (s in summoners) {
        if (summoners[s].name.toLowerCase() === summonername.toLowerCase()) {
            toRemove = s;
        }
    }

    summoners.splice(toRemove, 1);
    SaveSummoners();
}

var SaveSummoners = function () {
    jsonfile.writeFileSync(summonersfile, summoners);
}

var LoadSummoners = function (then) {
    summoners = jsonfile.readFileSync(summonersfile);

    var invalidSummoners = [];

    var donecount = 0;

    var didnothing = true;
    // refresh id's
    for (s in summoners) {
        if (!summoners[s].id) {
            didnothing = false;

            (function (summoner) {
                doRequest(SummonerByName, { summonername: summoner.name },
                    function(summoneronserver) {
                        summoner.id = summoneronserver.id;

                        SaveSummoners();

                        // the horror
                        donecount++;
                        if (then && donecount == summoners.length) {
                            then();
                        }
                    },
                    function(error, response) {
                        invalidSummoners.push(summoner);

                        // THE HORROR
                        donecount++;
                        if (then && donecount == summoners.length) {
                            then();
                        }
                    }
                );
            })(summoners[s]);
        }
    }

    if (didnothing) {
        then();
    }

    // todo : reconcile or remove invalidSummoners
}

var ObserveSummoner = function(summonername, then) {
    if (findSummonerByName(summonername)) {
        announce(summonername + " is already under observation");

        if (then)
            then();
    } else {
        doRequest(SummonerByName, { summonername: summonername },
            function(summoner) {
                var newSummoner = {
                    name: summoner.name,
                    id: summoner.id,
                    currentGame: null
                };

                announce("now observing " + summoner.name);

                doRequest(CurrentGame, { summonerId: summoner.id },
                    function(game) {
                        if (!summoner.currentGame || summoner.currentGame.gameId != game.gameId) {
                            announce(summoner.name + " is currently in a game");
                            summoner.currentGame = game;
                        }

                        addSummoner(newSummoner);
                    },
                    function(error, response) {
                        if (response === 404) {
                            // wasn't playing...
                        }

                        addSummoner(newSummoner);
                        console.log("error?" + summoner.id + " " + response);
                    }
                );

                if (then)
                    then();
            },
            function(error, response) {
                announce("unknown summoner " + summonername);

                if (then)
                    then();
            }
        );
    }
}

var DontObserveSummoner = function (summonername, then) {
    if (!findSummonerByName(summonername)) {
        announce(summonername + " is already not under observation");

        if (then)
            then();
    } else {
        removeSummonerByName(summonername);

        announce("not observing " + summonername + " anymore");

        if (then)
            then();
    }
}

var AnnounceCurrentGameStart = function (summoner) {
    if (summoner.gamestartsannounced && summoner.gamestartsannounced.indexOf(summoner.currentGame.gameId) != -1) {
        // already announced
        return;
    }
    if (!summoner.gamestartsannounced) {
        summoner.gamestartsannounced = [];
    }

    var champId;
    for (p in summoner.currentGame.participants) {
        if (summoner.currentGame.participants[p].summonerId == summoner.id) {
            champId = summoner.currentGame.participants[p].championId;
        }
    }

    summoner.gamestartsannounced.push(summoner.currentGame.gameId);
    SaveSummoners();

    doRequest(ChampionNameById, { id: champId },
        function (champname) {
            summoner.lastchamp = champname;
            announce(summoner.name + " has just started game on " + Maps[summoner.currentGame.mapId] + " (" + summoner.currentGame.gameMode + ")" + " playing " + summoner.lastchamp);
        });
}

function twodigits(n){
    return n > 9 ? "" + n: "0" + n;
}

function FormatGameTime(seconds){
    var m = Math.floor(seconds / 60);
    var s = Math.floor(seconds  % 60);
    return twodigits(m) + ':' + twodigits(s);
}

var Maps = {};
Maps[8] = "Crystal Scar";
Maps[10] = "Twisted Treeline";
Maps[11] = "Summoner's Rift";
Maps[12] = "Howling Abyss"

var AnnounceGameEnd = function (summoner, game) {
    var result = game.stats.win ? "won " : "lost ";
    var formattedgame = "on " + Maps[game.mapId] + " (" + game.gameMode + ")";

    var numDeaths = game.stats.numDeaths ? game.stats.numDeaths : "0";
    var championsKilled = game.stats.championsKilled ? game.stats.championsKilled : "0";
    var assists = game.stats.assists ? game.stats.assists : "0";
    var score = championsKilled + "/" + numDeaths + "/" + assists;

    announce(summoner.name + " as " + summoner.lastchamp + " just scored " + score + " and " + result + formattedgame + " in " + FormatGameTime(game.stats.timePlayed) + " minutes");
}

var UpdateGameStates = function () {
    if (workqueue.length)
        return;

    for (s in summoners) {
        (function(summoner) {
            if (summoner.currentGame && !summoner.waitingforresult) {
                doRequest(CurrentGame, { summonerId: summoner.id },
                    function(game) {
                        // still playing
                        summoner.currentGame = game;
                    },
                    function(error, response) {
                        if (response === 404) {
                            // game ended
                            summoner.waitingforresult = true;
                        }
                    }
                )
            } else if (summoner.currentGame && summoner.waitingforresult) {
                doRequest(LastGames, { summonerId: summoner.id },
                    function (games) {
                        for (game in games) {
                            if (summoner.currentGame && games[game].gameId == summoner.currentGame.gameId) {
                                AnnounceGameEnd(summoner, games[game]);
                                summoner.waitingforresult = false;
                                summoner.currentGame = null;
                                SaveSummoners();
                            }
                        }
                    }
                )
            } else {
                doRequest(CurrentGame, { summonerId: summoner.id },
                    function (game) {
                        summoner.currentGame = game;
                        AnnounceCurrentGameStart(summoner);
                    },
                    function(error, response) {
                        if (response === 404) {
                            // wasn't playing...
                            //announce(summoner.name + " was not playing");
                        }
                    }
                )
            }
        })(summoners[s]);
    }
}

bot.on('start', function() {
    // load summoners
    LoadSummoners(function () { setInterval(UpdateGameStates, 10000); });

    //doRequest(LastGames, { summonerId: 19869001 },
    //    function(games) {
    //        AnnounceGameEnd(summoners[1], games[0]);
    //    });

});

bot.on('message', function (message) {
    if (message.type == 'message' && message.text) {
        // todo : use an actual command parser
        var command = message.text.split(/[ ,]+/);

        if (command.length > 0 && command[0] == 'loltacle') {
            if (command[1] == 'observe') {
                var summonername = "";
                for (var i = 2; i < command.length; ++i) {
                    summonername += command[i] + " ";
                }
                summonername = summonername.trim();
                ObserveSummoner(summonername);
            }
            else if (command[1] == 'dontobserve') {
                var summonername = "";
                for (var i = 2; i < command.length; ++i) {
                    summonername += command[i] + " ";
                }
                summonername = summonername.trim();
                DontObserveSummoner(summonername);
            }
        }
    }
});
<|endoftext|>