<commit_before>import _ from 'lodash';
import moment from 'moment';
import React, {Component, PropTypes} from 'react';
import {connect} from 'react-redux'
import DateTimePicker from 'react-widgets/lib/DateTimePicker';
import {Content} from '../../components/layout';
import {Header, HeaderLeft, HeaderRight, GoBack, Title, AvatarView, TextLabel, Labels, MarkdownText, IconButton} from '../../components/widgets';
import {Edit as EditPerson, Preview as PersonPreview} from '../../components/person/widgets';
import {Edit as EditCompany, Preview as CompanyPreview} from '../../components/company/widgets';
import {Edit, Delete, OpenClose} from '../../components/mission/widgets';
import {missionsActions} from '../../actions/missions';
import {personsActions} from '../../actions/persons';
import {companiesActions} from '../../actions/companies';
import {agendaActions} from '../../actions/agenda';
import {replaceRoute, goBack} from '../../actions/routes'
import sitemap from '../../routes';
import Notes from '../../containers/notes';
import {viewMissionSelector} from '../../selectors/missions'
import routes from '../../routes';
import {pushRoute} from '../../actions/routes';

class ViewMission extends Component {

  goBack = () => {
    this.props.dispatch(goBack())
  }

  handleClick = () => {
    this.props.dispatch(agendaActions.changeFilter({missionIds: [this.props.mission.get('_id')], workerIds: []}))
    this.props.dispatch(pushRoute(routes.agenda.view))
  }

  componentWillMount() {
    const {dispatch, mission} = this.props
    if(!mission) { dispatch(replaceRoute(sitemap.mission.list)) ; return }

    dispatch(missionsActions.load({ids: [mission.get('_id')]}))
    dispatch(companiesActions.load({ids: [mission.get('clientId')]}))
    dispatch(personsActions.load({
      ids: [mission.get('managerId'), ...mission.get('workerIds').toArray()]
    }))
  }

  render(){
    const {mission, client, manager, workers, isLoading} = this.props
    if (!mission) return false

    return (
      <Content>

          <HeaderLeft>
            <GoBack goBack={this.goBack} isLoading={isLoading}/>
            <AvatarView style={{paddingRight: '10px'}} obj={client}/>
            <Title title={mission.get('name')}/>
          </HeaderLeft>
          <HeaderRight>

<IconButton name={'calendar'} label={'View Calendar'} onClick={this.handleClick}/>

            <Edit mission={mission}/>
            <OpenClose mission={mission}/>
            <Delete mission={mission} postAction={this.goBack}/>
          </HeaderRight>

        <Card
          mission={mission}
          client={client}
          manager={manager}
          workers={workers}  />
      </Content>
    )
  }
}

ViewMission.propTypes = {
  mission:     PropTypes.object,
  client:      PropTypes.object,
  manager:     PropTypes.object,
  workers:     PropTypes.object,
  isLoading:   PropTypes.bool,
  dispatch:    PropTypes.func.isRequired

}

const Card = ({mission, client, manager, workers}) =>  {

  const styles={
    container:{
      marginTop: '3rem',
    },
  }

  return (

        <Date label="Start Date" date={mission.get("startDate")}/>

        <Date label="End Date" date={mission.get("endDate")}/>

        <Client label="Client" workers={workers} client={client}/>

        <Manager label="Manager" manager={manager}/>

        <Workers
          label="Workers"
          workers={workers}
          mission={mission}/>

        <Notes entity={mission}/>

  )
}

Card.propTypes = {
  mission:     PropTypes.object.isRequired,
  client:      PropTypes.object.isRequired,
  manager:     PropTypes.object.isRequired,
  workers:     PropTypes.object.isRequired,
}

const Date = ({label, date}) => {
  return (
    <fieldset className="form-group">
       {label}
      <DateTimePicker
        defaultValue={date}
        time={false}
        readOnly={true}
        />
    </fieldset>
  )
}

Date.propTypes = {
  label:    PropTypes.string.isRequired,
  date:     PropTypes.object
}

const Client = ({label, client, workers}) => {
 const styles={
    container:{
      //marginBottom: '50px',
      marginLeft: 'auto',
      marginRight: 'auto',
    },
    item:{
      height: '80px',
    }
  };

  if(!client)return
;
  return (
    <fieldset className="form-group">
       {label}

          <CompanyPreview workers={workers} company={client}>
            <EditCompany company={client}/>
          </CompanyPreview>

    </fieldset>
  )
}

Client.propTypes = {
  label:    PropTypes.string.isRequired,
  client:   PropTypes.object.isRequired,
  workers:  PropTypes.object.isRequired
}

const Manager = ({label, manager}) => {
 const styles={
    container:{
      //marginBottom: '50px',
      marginLeft: 'auto',
      marginRight: 'auto',
    },
    item:{
      height: '80px',
    }
  };

  if(!manager)return
;
  return (
    <fieldset className="form-group">
       {label}

          <PersonPreview person={manager}>
            <EditPerson person={manager}/>
          </PersonPreview>

    </fieldset>
  )

}

Manager.propTypes = {
  label:    PropTypes.string.isRequired,
  manager:  PropTypes.object.isRequired
}

const Workers = ({label, workers, mission}) => {
 const styles={
    container:{
      //marginBottom: '50px',
      marginLeft: 'auto',
      marginRight: 'auto',
    },
    item:{
      height: '80px',
    }
  };

  const className = workers.size > 1 ? "col-md-6 tm list-item" : "col-md-12 tm list-item";
  const data = _.chain(workers.toArray())
    .sortBy( person => person.get('name') )
    .map( person => {
      return (

          <PersonPreview person={person}>
            <EditPerson person={person}/>
          </PersonPreview>

        )
      })
    .value();

  if(!data.length) return
;

  return (
    <fieldset className="form-group">
       {label}

        {data}

    </fieldset>
  )
}

Workers.propTypes = {
  label:    PropTypes.string.isRequired,
  mission:  PropTypes.object.isRequired,
  workers:  PropTypes.object.isRequired
}

export default connect(viewMissionSelector)(ViewMission)
<commit_msg>CSS: add some padding (p-r-1)<commit_after>import _ from 'lodash';
import moment from 'moment';
import React, {Component, PropTypes} from 'react';
import {connect} from 'react-redux'
import DateTimePicker from 'react-widgets/lib/DateTimePicker';
import {Content} from '../../components/layout';
import {Header, HeaderLeft, HeaderRight, GoBack, Title, AvatarView, TextLabel, Labels, MarkdownText, IconButton} from '../../components/widgets';
import {Edit as EditPerson, Preview as PersonPreview} from '../../components/person/widgets';
import {Edit as EditCompany, Preview as CompanyPreview} from '../../components/company/widgets';
import {Edit, Delete, OpenClose} from '../../components/mission/widgets';
import {missionsActions} from '../../actions/missions';
import {personsActions} from '../../actions/persons';
import {companiesActions} from '../../actions/companies';
import {agendaActions} from '../../actions/agenda';
import {replaceRoute, goBack} from '../../actions/routes'
import sitemap from '../../routes';
import Notes from '../../containers/notes';
import {viewMissionSelector} from '../../selectors/missions'
import routes from '../../routes';
import {pushRoute} from '../../actions/routes';

class ViewMission extends Component {

  goBack = () => {
    this.props.dispatch(goBack())
  }

  handleClick = () => {
    this.props.dispatch(agendaActions.changeFilter({missionIds: [this.props.mission.get('_id')], workerIds: []}))
    this.props.dispatch(pushRoute(routes.agenda.view))
  }

  componentWillMount() {
    const {dispatch, mission} = this.props
    if(!mission) { dispatch(replaceRoute(sitemap.mission.list)) ; return }

    dispatch(missionsActions.load({ids: [mission.get('_id')]}))
    dispatch(companiesActions.load({ids: [mission.get('clientId')]}))
    dispatch(personsActions.load({
      ids: [mission.get('managerId'), ...mission.get('workerIds').toArray()]
    }))
  }

  render(){
    const {mission, client, manager, workers, isLoading} = this.props
    if (!mission) return false

    return (
      <Content>

          <HeaderLeft>
            <GoBack goBack={this.goBack} isLoading={isLoading}/>
            <AvatarView style={{paddingRight: '10px'}} obj={client}/>
            <Title title={mission.get('name')}/>
          </HeaderLeft>
          <HeaderRight>

<Edit mission={mission}/>

<OpenClose mission={mission}/>

<Delete mission={mission} postAction={this.goBack}/>

          </HeaderRight>

        <Card
          mission={mission}
          client={client}
          manager={manager}
          workers={workers}  />
      </Content>
    )
  }
}

ViewMission.propTypes = {
  mission:     PropTypes.object,
  client:      PropTypes.object,
  manager:     PropTypes.object,
  workers:     PropTypes.object,
  isLoading:   PropTypes.bool,
  dispatch:    PropTypes.func.isRequired

}

const Card = ({mission, client, manager, workers}) =>  {

  const styles={
    container:{
      marginTop: '3rem',
    },
  }

  return (

        <Date label="Start Date" date={mission.get('startDate')}/>

        <Date label="End Date" date={mission.get('endDate')}/>

        <Client label="Client" workers={workers} client={client}/>

        <Manager label="Manager" manager={manager}/>

        <Workers
          label="Workers"
          workers={workers}
          mission={mission}/>

        <Notes entity={mission}/>

  )
}

Card.propTypes = {
  mission:     PropTypes.object.isRequired,
  client:      PropTypes.object.isRequired,
  manager:     PropTypes.object.isRequired,
  workers:     PropTypes.object.isRequired,
}

const Date = ({label, date}) => {
  return (
    <fieldset className="form-group">
       {label}
      <DateTimePicker
        defaultValue={date}
        time={false}
        readOnly={true}
        />
    </fieldset>
  )
}

Date.propTypes = {
  label:    PropTypes.string.isRequired,
  date:     PropTypes.object
}

const Client = ({label, client, workers}) => {
 const styles={
    container:{
      //marginBottom: '50px',
      marginLeft: 'auto',
      marginRight: 'auto',
    },
    item:{
      height: '80px',
    }
  };

  if(!client)return
;
  return (
    <fieldset className="form-group">
       {label}

          <CompanyPreview workers={workers} company={client}>
            <EditCompany company={client}/>
          </CompanyPreview>

    </fieldset>
  )
}

Client.propTypes = {
  label:    PropTypes.string.isRequired,
  client:   PropTypes.object.isRequired,
  workers:  PropTypes.object.isRequired
}

const Manager = ({label, manager}) => {
 const styles={
    container:{
      //marginBottom: '50px',
      marginLeft: 'auto',
      marginRight: 'auto',
    },
    item:{
      height: '80px',
    }
  };

  if(!manager)return
;
  return (
    <fieldset className="form-group">
       {label}

          <PersonPreview person={manager}>
            <EditPerson person={manager}/>
          </PersonPreview>

    </fieldset>
  )

}

Manager.propTypes = {
  label:    PropTypes.string.isRequired,
  manager:  PropTypes.object.isRequired
}

const Workers = ({label, workers, mission}) => {
 const styles={
    container:{
      //marginBottom: '50px',
      marginLeft: 'auto',
      marginRight: 'auto',
    },
    item:{
      height: '80px',
    }
  };

  const className = workers.size > 1 ? 'col-md-6 tm list-item' : 'col-md-12 tm list-item';
  const data = _.chain(workers.toArray())
    .sortBy( person => person.get('name') )
    .map( person => {
      return (

          <PersonPreview person={person}>
            <EditPerson person={person}/>
          </PersonPreview>

        )
      })
    .value();

  if(!data.length) return
;

  return (
    <fieldset className="form-group">
       {label}

        {data}

    </fieldset>
  )
}

Workers.propTypes = {
  label:    PropTypes.string.isRequired,
  mission:  PropTypes.object.isRequired,
  workers:  PropTypes.object.isRequired
}

export default connect(viewMissionSelector)(ViewMission)
<|endoftext|>


<commit_before>$(function initSearchBar() {
    var methodNames = new Bloodhound({
        datumTokenizer: Bloodhound.tokenizers.whitespace,
        queryTokenizer: Bloodhound.tokenizers.whitespace,
        prefetch: './methodNames.json'
    });

    var sourceFiles = new Bloodhound({
        datumTokenizer: Bloodhound.tokenizers.whitespace,
        queryTokenizer: Bloodhound.tokenizers.whitespace,
        prefetch: './sourceFiles.json'
    });

    var githubIssues = new Bloodhound({
        datumTokenizer: Bloodhound.tokenizers.whitespace,
        queryTokenizer: Bloodhound.tokenizers.whitespace,
        remote: {
            url: 'https://api.github.com/search/issues?q=%QUERY+repo:caolan/async',
            cache: false,
            wildcard: '%QUERY',
            transform: function(response) {
                return $.map(response.items, function(issue) {
                    // if (issue.state !== 'open') {
                    //     return null;
                    // }
                    return {
                        url: issue.html_url,
                        name: issue.number + ': ' + issue.title,
                        number: issue.number
                    };
                }).sort(function(a, b) {
                    return b.number - a.number;
                });
            }
        }
    });

    $('.typeahead').typeahead({
        hint: true,
        highlight: true,
        minLength: 1
    },
    {
        name: 'Methods',
        source: methodNames,
        templates: {
            header: '
Methods
'
        }
    },
    {
        name: 'Files',
        source: sourceFiles,
        templates: {
            header: '
Source Files
'
        }
    },
    {
        name: 'Issues',
        source: githubIssues,
        display: 'name',
        templates: {
            header: '
Issues
'
        }
    }).on('typeahead:select', function(ev, suggestion) {
        var protocol = window.location.protocol
        var host = window.location.host;
        var currentPath = window.location.pathname;
        // handle issues
        if (typeof suggestion !== 'string') {
            window.location.href = suggestion.url;
        // handle source files
        } else if (suggestion.indexOf('.html') !== -1) {
            window.location.href = protocol + '//' + host + '/' + suggestion;
        // handle searching from one of the source files or the home page
        } else if (currentPath !== '/docs.html') {
            window.location.href = protocol + '//' + host + '/docs.html#.' + suggestion;
        } else {
            var $el = document.getElementById('.' + suggestion);
            $('#main').animate({ scrollTop: $el.offsetTop }, 500);
        }
    });
});
<commit_msg>jsdoc: added substr matching, closes #9, @megawac<commit_after>function matchSubstrs(methodName) {
    var tokens = [];

    var len = methodName.length;

    for (var size = 1; size <= len; size++){
        for (var i = 0; i+size<= len; i++){
            tokens.push(methodName.substr(i, size));
        }
    }

    return tokens;
}

$(function initSearchBar() {
    var methodNames = new Bloodhound({
        datumTokenizer: matchSubstrs,
        queryTokenizer: Bloodhound.tokenizers.whitespace,
        prefetch: './methodNames.json'
    });

    var sourceFiles = new Bloodhound({
        datumTokenizer: matchSubstrs,
        queryTokenizer: Bloodhound.tokenizers.whitespace,
        prefetch: './sourceFiles.json'
    });

    var githubIssues = new Bloodhound({
        datumTokenizer: Bloodhound.tokenizers.whitespace,
        queryTokenizer: Bloodhound.tokenizers.whitespace,
        remote: {
            url: 'https://api.github.com/search/issues?q=%QUERY+repo:caolan/async',
            cache: true,
            wildcard: '%QUERY',
            transform: function(response) {
                return $.map(response.items, function(issue) {
                    // if (issue.state !== 'open') {
                    //     return null;
                    // }
                    return {
                        url: issue.html_url,
                        name: issue.number + ': ' + issue.title,
                        number: issue.number
                    };
                }).sort(function(a, b) {
                    return b.number - a.number;
                });
            }
        }
    });

    $('.typeahead').typeahead({
        hint: true,
        highlight: true,
        minLength: 1
    },
    {
        name: 'Methods',
        source: methodNames,
        templates: {
            header: '
Methods
'
        }
    },
    {
        name: 'Files',
        source: sourceFiles,
        templates: {
            header: '
Source Files
'
        }
    },
    {
        name: 'Issues',
        source: githubIssues,
        display: 'name',
        templates: {
            header: '
Issues
'
        }
    }).on('typeahead:select', function(ev, suggestion) {
        var protocol = window.location.protocol
        var host = window.location.host;
        var currentPath = window.location.pathname;
        // handle issues
        if (typeof suggestion !== 'string') {
            window.location.href = suggestion.url;
        // handle source files
        } else if (suggestion.indexOf('.html') !== -1) {
            window.location.href = protocol + '//' + host + '/' + suggestion;
        // handle searching from one of the source files or the home page
        } else if (currentPath !== '/docs.html') {
            window.location.href = protocol + '//' + host + '/docs.html#.' + suggestion;
        } else {
            var $el = document.getElementById('.' + suggestion);
            $('#main').animate({ scrollTop: $el.offsetTop }, 500);
        }
    });
});
<|endoftext|>


<commit_before>var fs = require('fs');
var url = require('url');
var path = require('path');
var request = require('request');
var crypto = require('crypto');
var Step = require('step');

command = Bones.Command.extend();

command.description = 'export project';
command.usage = '<project> <export>';

command.options['format'] = {
    'title': 'format=[format]',
    'description': 'Export format (png|pdf|svg|mbtiles).'
};

command.options['bbox'] = {
    'title': 'bbox=[bbox]',
    'description': 'Comma separated coordinates of bounding box to export.'
};

command.options['minzoom'] = {
    'title': 'minzoom=[zoom]',
    'description': 'MBTiles: minimum zoom level to export.'
};

command.options['maxzoom'] = {
    'title': 'maxzoom=[zoom]',
    'description': 'MBTiles: maximum zoom level to export.'
};

command.options['width'] = {
    'title': 'width=[width]',
    'description': 'Image: image width in pixels.',
    'default': 400
};

command.options['height'] = {
    'title': 'height=[height]',
    'description': 'Image: image height in pixels.',
    'default': 400
};

command.options['url'] = {
    'title': 'url=[url]',
    'description': 'URL to PUT updates to.'
};

command.options['log'] = {
    'title': 'log',
    'description': 'Write crash logs to destination directory.'
};

command.prototype.initialize = function(plugin, callback) {
    var opts = plugin.config;
    if (process.env.tilemillConfig)
        _(opts).extend(JSON.parse(process.env.tilemillConfig));
    opts.files = path.resolve(opts.files);
    opts.project = plugin.argv._[1];
    opts.filepath = path.resolve(plugin.argv._[2]);
    callback = callback || function() {};
    this.opts = opts;

    // Write crash log
    if (opts.log) {
        process.on('uncaughtException', function(err) {
            fs.writeFileSync(opts.filepath + '.crashlog', err.stack || err.toString());
            process.exit(1);
        });
    }

    // Validation.
    if (!opts.project || !opts.filepath) return plugin.help();
    if (!path.existsSync(path.dirname(opts.filepath)))
        return this.error(new Error('Export path does not exist: ' + path.dirname(opts.filepath)));

    // Format.
    if (!opts.format) opts.format = path.extname(opts.filepath).split('.').pop();
    if (!_(['pdf', 'svg', 'png', 'mbtiles', 'upload']).include(opts.format))
        return this.error(new Error('Invalid format: ' + opts.format));

    // Convert string params into numbers.
    if (!_(opts.bbox).isUndefined())
        opts.bbox = _(opts.bbox.split(',')).map(parseFloat);
    if (!_(opts.minzoom).isUndefined())
        opts.minzoom = parseInt(opts.minzoom, 10);
    if (!_(opts.maxzoom).isUndefined())
        opts.maxzoom = parseInt(opts.maxzoom, 10);
    if (!_(opts.width).isUndefined())
        opts.width = parseInt(opts.width, 10);
    if (!_(opts.height).isUndefined())
        opts.height = parseInt(opts.height, 10);

    // Rename the output filepath using a random hash if file already exists.
    if (path.existsSync(opts.filepath) && opts.format !== 'upload') {
        var hash = crypto.createHash('md5')
            .update(+new Date + '')
            .digest('hex')
            .substring(0, 6);
        var ext = path.extname(opts.filepath);
        opts.filepath = opts.filepath.replace(ext, '_' + hash + ext);

        // Update filename in TileMill.
        this.put({ filename: path.basename(opts.filepath), status: 'processing' });
    }

    // Set process title.
    process.title = 'tm-' + path.basename(opts.filepath);

    // Catch SIGINT.
    process.on('SIGINT', function () {
        console.log('Got SIGINT. Run "kill ' + process.pid + '" to terminate.');
    });
    process.on('SIGUSR1', process.exit);

    // Upload format does not require loaded project.
    if (opts.format === 'upload') return this[opts.format](callback);

    // Load project, localize and call export function.
    var cmd = this;
    var model = new models.Project({id:opts.project});
    Step(function() {
        Bones.utils.fetch({model:model}, this);
    }, function(err) {
        if (err) throw err;
        model.localize(model.toJSON(), this);
    }, function(err) {
        if (err) return cmd.error(err);

        model.mml = _(model.mml).extend({
            name: model.mml.name || model.id,
            version: model.mml.version || '1.0.0',
            minzoom: !_(opts.minzoom).isUndefined() ? opts.minzoom : model.get('minzoom'),
            maxzoom: !_(opts.maxzoom).isUndefined() ? opts.maxzoom : model.get('maxzoom'),
            bounds: !_(opts.bbox).isUndefined() ? opts.bbox : model.get('bounds')
        });

        // Unset map center if outside bounds.
        var validCenter = (function(center, bounds, minzoom, maxzoom) {
            if (center[0] < bounds[0] ||
                center[0] > bounds[2] ||
                center[1] < bounds[1] ||
                center[1] > bounds[3]) return false;
            if (center[2] < minzoom) return false;
            if (center[2] > maxzoom) return false;
            return true;
        })(model.mml.center, model.mml.bounds, model.mml.minzoom, model.mml.maxzoom);
        if (!validCenter) delete model.mml.center;

        cmd[opts.format](model, callback);
    });
};

command.prototype.error = function(err) {
    this.put({
        status: 'error',
        error: err.toString(),
        updated: +new Date()
    });
    console.error(err.toString());
};

command.prototype.remaining = function(progress, started) {
    return Math.floor(
        (Date.now() - started) * (1 / progress) -
        (Date.now() - started)
    );
};

command.prototype.put = function(data, callback) {
    callback = callback || function() {};
    if (!this.opts.url) return callback();
    request.put({
        uri: this.opts.url,
        headers: {
            'Host': url.parse(this.opts.url).host,
            'Cookie': 'bones.token=token'
        },
        json: _(data).extend({'bones.token': 'token'})
    }, callback);
};

command.prototype.png =
command.prototype.svg =
command.prototype.pdf = function(project, callback) {
    var mapnik = require('mapnik');
    var sm = new (require('sphericalmercator'))();
    var map = new mapnik.Map(this.opts.width, this.opts.height);

    map.fromStringSync(project.xml, {
        strict: false,
        base: path.join(this.opts.files, 'project', project.id) + '/'
    });
    map.bufferSize = this.opts.bufferSize;
    map.extent = sm.convert(project.mml.bounds, '900913');
    try {
        map.renderFileSync(this.opts.filepath, { format: this.opts.format });
        this.put({
            status: 'complete',
            progress: 1,
            updated: +new Date()
        }, process.exit);
    } catch(err) {
        this.error(err);
    }
};

command.prototype.mbtiles = function (project, callback) {
    var cmd = this;
    var tilelive = require('tilelive');
    require('mbtiles').registerProtocols(tilelive);
    require('tilelive-mapnik').registerProtocols(tilelive);

    var uri = {
        protocol: 'mapnik:',
        slashes: true,
        xml: project.xml,
        mml: project.mml,
        pathname: path.join(this.opts.files, 'project', project.id, project.id + '.xml'),
        query: { bufferSize: this.opts.bufferSize }
    };
    var source;
    var sink;

    Step(function() {
        tilelive.load(uri, this);
    }, function(err, s) {
        if (err) throw err;
        source = s;
        tilelive.load('mbtiles://' + cmd.opts.filepath, this);
    }, function(err, s) {
        if (err) throw err;
        sink = s;
        sink.startWriting(this);
    }, function(err) {
        if (err) throw err;
        sink.putInfo(project.mml, this);
    }, function(err) {
        if (err) return cmd.error(err);

        var copy = tilelive.copy({
            source: source,
            sink: sink,
            bbox: project.mml.bounds,
            minZoom: project.mml.minzoom,
            maxZoom: project.mml.maxzoom,
            concurrency: 100,
            tiles: true,
            grids: !!project.mml.interactivity
        });

        var timeout = setInterval(function progress() {
            var progress = (copy.copied + copy.failed) / copy.total;
            cmd.put({
                status: progress < 1 ? 'processing' : 'complete',
                progress: progress,
                remaining: cmd.remaining(progress, copy.started),
                updated: +new Date(),
            });
        }, 5000);

        copy.on('warning', function(err) {
            console.log(err);
        });
        copy.on('finished', function() {
            clearTimeout(timeout);
            cmd.put({
                status: 'complete',
                progress: 1,
                updated: +new Date()
            }, process.exit);
        });
        copy.on('error', function(err) {
            clearTimeout(timeout);
            cmd.error(err);
        });
    });
};

command.prototype.upload = function (callback) {
    var cmd = this;
    var hash = crypto.createHash('md5')
        .update(+new Date + '')
        .digest('hex')
        .substring(0, 6);
    var policyEndpoint = url.format({
        protocol: 'http:',
        host: this.opts.mapboxHost || 'api.tiles.mapbox.com',
        pathname: '/v2/'+ hash + '/upload.json'
    });

    Step(function() {
        request.get({
            uri: policyEndpoint,
            headers: { 'Host': url.parse(policyEndpoint).host }
        }, this);
    }, function(err, resp, body) {
        if (err) throw err;
        if (resp.statusCode !== 200)
            throw new Error('MapBox Hosting is not available. Status ' + resp.statusCode + '.');
        try {
            var uploadArgs = JSON.parse(body);
        } catch (e) {
            throw new Error('Failed to parse policy from MapBox Hosting.');
        }
        try {
            var stat = fs.statSync(cmd.opts.filepath);
        } catch(err) { throw err }

        var bucket = uploadArgs.bucket;
        delete uploadArgs.bucket;
        delete uploadArgs.filename;
        var boundary = '----TileMill' + crypto.createHash('md5')
            .update(+new Date + '')
            .digest('hex')
            .substring(0, 6);
        var filename = path.basename(cmd.opts.filepath);
        var multipartBody = new Buffer(_(uploadArgs).map(function(value, key) {
            return '--' + boundary + '\r\n'
                + 'Content-Disposition: form-data; name="' + key + '"\r\n'
                + '\r\n' + value + '\r\n';
            })
            .concat(['--' + boundary + '\r\n'
                + 'Content-Disposition: form-data; name="file"; filename="' + filename + '"\r\n'
                + 'Content-Type: application/octet-stream\r\n\r\n'])
            .join(''));
        var terminate = new Buffer('\r\n--' + boundary + '--', 'ascii');

        var dest = http.request({
            host: bucket + '.s3.amazonaws.com',
            path: '/',
            method: 'POST',
            headers: {
                'Content-Type': 'multipart/form-data; boundary=' + boundary,
                'Content-Length': stat.size + multipartBody.length + terminate.length,
                'X_FILE_NAME': filename
            }
        }, function(resp) {
            if (resp.statusCode !== 303) {
                this(new Error('S3 is not available. Status ' + resp.statusCode + '.'));
            } else {
                this(null, resp.headers.location.split('?')[0]);
            }
        }.bind(this))

        // Write multipart values from memory.
        dest.write(multipartBody, 'ascii');

        // Set up read for MBTiles file and start the upload.
        var bytesWritten = 0;
        var started = Date.now();
        var updated = Date.now();
        fs.createReadStream(cmd.opts.filepath)
            .on('data', function(chunk) {
                bytesWritten += chunk.length;

                if (Date.now() < updated + 5000) return;

                var progress = bytesWritten / stat.size;
                updated = Date.now();
                cmd.put({
                    status: 'complete',
                    progress: progress,
                    status: progress < 1 ? 'processing' : 'complete',
                    remaining: cmd.remaining(progress, started),
                    updated: updated
                });
            })
            .on('end', function() {
                dest.write(terminate);
                dest.end();
            })
            .pipe(dest, {end: false});
    }, function(err, url) {
        if (err) return cmd.error(err);
        cmd.put({
            status: 'complete',
            progress: 1,
            url: url,
            updated: +new Date()
        }, process.exit);
    });
};
<commit_msg>Reimplement account uploads.<commit_after>var fs = require('fs');
var url = require('url');
var path = require('path');
var request = require('request');
var crypto = require('crypto');
var Step = require('step');

command = Bones.Command.extend();

command.description = 'export project';
command.usage = '<project> <export>';

command.options['format'] = {
    'title': 'format=[format]',
    'description': 'Export format (png|pdf|svg|mbtiles).'
};

command.options['bbox'] = {
    'title': 'bbox=[bbox]',
    'description': 'Comma separated coordinates of bounding box to export.'
};

command.options['minzoom'] = {
    'title': 'minzoom=[zoom]',
    'description': 'MBTiles: minimum zoom level to export.'
};

command.options['maxzoom'] = {
    'title': 'maxzoom=[zoom]',
    'description': 'MBTiles: maximum zoom level to export.'
};

command.options['width'] = {
    'title': 'width=[width]',
    'description': 'Image: image width in pixels.',
    'default': 400
};

command.options['height'] = {
    'title': 'height=[height]',
    'description': 'Image: image height in pixels.',
    'default': 400
};

command.options['url'] = {
    'title': 'url=[url]',
    'description': 'URL to PUT updates to.'
};

command.options['log'] = {
    'title': 'log',
    'description': 'Write crash logs to destination directory.'
};

command.prototype.initialize = function(plugin, callback) {
    var opts = plugin.config;
    if (process.env.tilemillConfig)
        _(opts).extend(JSON.parse(process.env.tilemillConfig));
    opts.files = path.resolve(opts.files);
    opts.project = plugin.argv._[1];
    opts.filepath = path.resolve(plugin.argv._[2]);
    callback = callback || function() {};
    this.opts = opts;

    // Write crash log
    if (opts.log) {
        process.on('uncaughtException', function(err) {
            fs.writeFileSync(opts.filepath + '.crashlog', err.stack || err.toString());
            process.exit(1);
        });
    }

    // Validation.
    if (!opts.project || !opts.filepath) return plugin.help();
    if (!path.existsSync(path.dirname(opts.filepath)))
        return this.error(new Error('Export path does not exist: ' + path.dirname(opts.filepath)));

    // Format.
    if (!opts.format) opts.format = path.extname(opts.filepath).split('.').pop();
    if (!_(['pdf', 'svg', 'png', 'mbtiles', 'upload']).include(opts.format))
        return this.error(new Error('Invalid format: ' + opts.format));

    // Convert string params into numbers.
    if (!_(opts.bbox).isUndefined())
        opts.bbox = _(opts.bbox.split(',')).map(parseFloat);
    if (!_(opts.minzoom).isUndefined())
        opts.minzoom = parseInt(opts.minzoom, 10);
    if (!_(opts.maxzoom).isUndefined())
        opts.maxzoom = parseInt(opts.maxzoom, 10);
    if (!_(opts.width).isUndefined())
        opts.width = parseInt(opts.width, 10);
    if (!_(opts.height).isUndefined())
        opts.height = parseInt(opts.height, 10);

    // Rename the output filepath using a random hash if file already exists.
    if (path.existsSync(opts.filepath) && opts.format !== 'upload') {
        var hash = crypto.createHash('md5')
            .update(+new Date + '')
            .digest('hex')
            .substring(0, 6);
        var ext = path.extname(opts.filepath);
        opts.filepath = opts.filepath.replace(ext, '_' + hash + ext);

        // Update filename in TileMill.
        this.put({ filename: path.basename(opts.filepath), status: 'processing' });
    }

    // Set process title.
    process.title = 'tm-' + path.basename(opts.filepath);

    // Catch SIGINT.
    process.on('SIGINT', function () {
        console.log('Got SIGINT. Run "kill ' + process.pid + '" to terminate.');
    });
    process.on('SIGUSR1', process.exit);

    // Upload format does not require loaded project.
    if (opts.format === 'upload') return this[opts.format](callback);

    // Load project, localize and call export function.
    var cmd = this;
    var model = new models.Project({id:opts.project});
    Step(function() {
        Bones.utils.fetch({model:model}, this);
    }, function(err) {
        if (err) throw err;
        model.localize(model.toJSON(), this);
    }, function(err) {
        if (err) return cmd.error(err);

        model.mml = _(model.mml).extend({
            name: model.mml.name || model.id,
            version: model.mml.version || '1.0.0',
            minzoom: !_(opts.minzoom).isUndefined() ? opts.minzoom : model.get('minzoom'),
            maxzoom: !_(opts.maxzoom).isUndefined() ? opts.maxzoom : model.get('maxzoom'),
            bounds: !_(opts.bbox).isUndefined() ? opts.bbox : model.get('bounds')
        });

        // Unset map center if outside bounds.
        var validCenter = (function(center, bounds, minzoom, maxzoom) {
            if (center[0] < bounds[0] ||
                center[0] > bounds[2] ||
                center[1] < bounds[1] ||
                center[1] > bounds[3]) return false;
            if (center[2] < minzoom) return false;
            if (center[2] > maxzoom) return false;
            return true;
        })(model.mml.center, model.mml.bounds, model.mml.minzoom, model.mml.maxzoom);
        if (!validCenter) delete model.mml.center;

        cmd[opts.format](model, callback);
    });
};

command.prototype.error = function(err) {
    this.put({
        status: 'error',
        error: err.toString(),
        updated: +new Date()
    });
    console.error(err.toString());
};

command.prototype.remaining = function(progress, started) {
    return Math.floor(
        (Date.now() - started) * (1 / progress) -
        (Date.now() - started)
    );
};

command.prototype.put = function(data, callback) {
    callback = callback || function() {};
    if (!this.opts.url) return callback();
    request.put({
        uri: this.opts.url,
        headers: {
            'Host': url.parse(this.opts.url).host,
            'Cookie': 'bones.token=token'
        },
        json: _(data).extend({'bones.token': 'token'})
    }, callback);
};

command.prototype.png =
command.prototype.svg =
command.prototype.pdf = function(project, callback) {
    var mapnik = require('mapnik');
    var sm = new (require('sphericalmercator'))();
    var map = new mapnik.Map(this.opts.width, this.opts.height);

    map.fromStringSync(project.xml, {
        strict: false,
        base: path.join(this.opts.files, 'project', project.id) + '/'
    });
    map.bufferSize = this.opts.bufferSize;
    map.extent = sm.convert(project.mml.bounds, '900913');
    try {
        map.renderFileSync(this.opts.filepath, { format: this.opts.format });
        this.put({
            status: 'complete',
            progress: 1,
            updated: +new Date()
        }, process.exit);
    } catch(err) {
        this.error(err);
    }
};

command.prototype.mbtiles = function (project, callback) {
    var cmd = this;
    var tilelive = require('tilelive');
    require('mbtiles').registerProtocols(tilelive);
    require('tilelive-mapnik').registerProtocols(tilelive);

    var uri = {
        protocol: 'mapnik:',
        slashes: true,
        xml: project.xml,
        mml: project.mml,
        pathname: path.join(this.opts.files, 'project', project.id, project.id + '.xml'),
        query: { bufferSize: this.opts.bufferSize }
    };
    var source;
    var sink;

    Step(function() {
        tilelive.load(uri, this);
    }, function(err, s) {
        if (err) throw err;
        source = s;
        tilelive.load('mbtiles://' + cmd.opts.filepath, this);
    }, function(err, s) {
        if (err) throw err;
        sink = s;
        sink.startWriting(this);
    }, function(err) {
        if (err) throw err;
        sink.putInfo(project.mml, this);
    }, function(err) {
        if (err) return cmd.error(err);

        var copy = tilelive.copy({
            source: source,
            sink: sink,
            bbox: project.mml.bounds,
            minZoom: project.mml.minzoom,
            maxZoom: project.mml.maxzoom,
            concurrency: 100,
            tiles: true,
            grids: !!project.mml.interactivity
        });

        var timeout = setInterval(function progress() {
            var progress = (copy.copied + copy.failed) / copy.total;
            cmd.put({
                status: progress < 1 ? 'processing' : 'complete',
                progress: progress,
                remaining: cmd.remaining(progress, copy.started),
                updated: +new Date(),
            });
        }, 5000);

        copy.on('warning', function(err) {
            console.log(err);
        });
        copy.on('finished', function() {
            clearTimeout(timeout);
            cmd.put({
                status: 'complete',
                progress: 1,
                updated: +new Date()
            }, process.exit);
        });
        copy.on('error', function(err) {
            clearTimeout(timeout);
            cmd.error(err);
        });
    });
};

command.prototype.upload = function (callback) {
    var cmd = this;
    var key;
    var bucket;
    var mapURL = '';
    var freeURL = '';
    var modelURL = '';
    var hash = crypto.createHash('md5')
        .update(+new Date + '')
        .digest('hex')
        .substring(0, 6);
    var policyEndpoint = url.format({
        protocol: 'http:',
        host: this.opts.mapboxHost || 'api.tiles.mapbox.com',
        pathname: '/v2/'+ hash + '/upload.json'
    });

    // Set URLs for account uploads.
    if (this.opts.syncAccount && this.opts.syncAccessToken) {
        mapURL = _('<%=base%>/<%=account%>/map/<%=handle%>')
            .template({
                base: this.opts.syncURL,
                account: this.opts.syncAccount,
                handle: this.opts.project
            });
        modelURL = _('<%=base%>/api/Map/<%=account%>.<%=handle%>?access_token=<%=token%>')
            .template({
                base: this.opts.syncURL,
                account: this.opts.syncAccount,
                handle: this.opts.project,
                token: this.opts.syncAccessToken
            });
    }

    Step(function() {
        request.get({
            uri: policyEndpoint,
            headers: { 'Host': url.parse(policyEndpoint).host }
        }, this);
    }, function(err, resp, body) {
        if (err) throw err;
        if (resp.statusCode !== 200)
            throw new Error('MapBox Hosting is not available. Status ' + resp.statusCode + '.');

        // Let Step catch thrown errors here.
        uploadArgs = JSON.parse(body);
        key = uploadArgs.key;
        bucket = uploadArgs.bucket;
        delete uploadArgs.bucket;
        delete uploadArgs.filename;

        var stat = fs.statSync(cmd.opts.filepath);
        var boundary = '----TileMill' + crypto.createHash('md5')
            .update(+new Date + '')
            .digest('hex')
            .substring(0, 6);
        var filename = path.basename(cmd.opts.filepath);
        var multipartBody = new Buffer(_(uploadArgs).map(function(value, key) {
            return '--' + boundary + '\r\n'
                + 'Content-Disposition: form-data; name="' + key + '"\r\n'
                + '\r\n' + value + '\r\n';
            })
            .concat(['--' + boundary + '\r\n'
                + 'Content-Disposition: form-data; name="file"; filename="' + filename + '"\r\n'
                + 'Content-Type: application/octet-stream\r\n\r\n'])
            .join(''));
        var terminate = new Buffer('\r\n--' + boundary + '--', 'ascii');

        var dest = http.request({
            host: bucket + '.s3.amazonaws.com',
            path: '/',
            method: 'POST',
            headers: {
                'Content-Type': 'multipart/form-data; boundary=' + boundary,
                'Content-Length': stat.size + multipartBody.length + terminate.length,
                'X_FILE_NAME': filename
            }
        }, function(resp) {
            if (resp.statusCode !== 303)
                return this(new Error('S3 is not available. Status ' + resp.statusCode + '.'));

            freeURL = resp.headers.location.split('?')[0];
            this();
        }.bind(this))

        // Write multipart values from memory.
        dest.write(multipartBody, 'ascii');

        // Set up read for MBTiles file and start the upload.
        var bytesWritten = 0;
        var started = Date.now();
        var updated = Date.now();
        fs.createReadStream(cmd.opts.filepath)
            .on('data', function(chunk) {
                bytesWritten += chunk.length;

                if (Date.now() < updated + 5000) return;

                var progress = bytesWritten / stat.size;
                updated = Date.now();
                cmd.put({
                    status: 'complete',
                    progress: progress,
                    status: progress < 1 ? 'processing' : 'complete',
                    remaining: cmd.remaining(progress, started),
                    updated: updated
                });
            })
            .on('end', function() {
                dest.write(terminate);
                dest.end();
            })
            .pipe(dest, {end: false});
    }, function(err) {
        if (err) throw err;
        if (!modelURL) return this(); // Free

        request.get(modelURL, this);
    }, function(err, res, body) {
        if (err) throw err;
        if (!modelURL) return this(); // Free

        // Let Step catch thrown errors here.
        var model = _(res.statusCode === 404 ? {} : JSON.parse(body)).extend({
            id: cmd.opts.syncAccount + '.' + cmd.opts.project,
            _type: 'tileset',
            created: +new Date,
            status: 'pending',
            url: 'http://' + bucket + '.s3.amazonaws.com/' + key
        });
        request.put({ url:modelURL, json:model }, this);
    }, function(err, res, body) {
        if (err) return cmd.error(err);
        if (modelURL && res.statusCode !== 200)
            return cmd.error('Map publish failed: ' + res.statusCode);
        cmd.put({
            status: 'complete',
            progress: 1,
            url: modelURL ? mapURL : freeURL,
            updated: +new Date()
        }, process.exit);
    });
};
<|endoftext|>


<commit_before>var fs = require('fs');
var url = require('url');
var path = require('path');
var request = require('request');
var crypto = require('crypto');
var Step = require('step');

command = Bones.Command.extend();

command.description = 'export project';
command.usage = '<project> <export>';

command.options['format'] = {
    'title': 'format=[format]',
    'description': 'Export format (png|pdf|svg|mbtiles).'
};

command.options['bbox'] = {
    'title': 'bbox=[bbox]',
    'description': 'Comma separated coordinates of bounding box to export.'
};

command.options['minzoom'] = {
    'title': 'minzoom=[zoom]',
    'description': 'MBTiles: minimum zoom level to export.'
};

command.options['maxzoom'] = {
    'title': 'maxzoom=[zoom]',
    'description': 'MBTiles: maximum zoom level to export.'
};

command.options['width'] = {
    'title': 'width=[width]',
    'description': 'Image: image width in pixels.',
    'default': 400
};

command.options['height'] = {
    'title': 'height=[height]',
    'description': 'Image: image height in pixels.',
    'default': 400
};

command.options['url'] = {
    'title': 'url=[url]',
    'description': 'URL to PUT updates to.'
};

command.options['log'] = {
    'title': 'log',
    'description': 'Write crash logs to destination directory.'
};

command.prototype.initialize = function(plugin, callback) {
    var opts = plugin.config;
    if (process.env.tilemillConfig)
        _(opts).extend(JSON.parse(process.env.tilemillConfig));
    opts.files = path.resolve(opts.files);
    opts.project = plugin.argv._[1];
    opts.filepath = path.resolve(plugin.argv._[2]);
    callback = callback || function() {};
    this.opts = opts;

    // Write crash log
    if (opts.log) {
        process.on('uncaughtException', function(err) {
            fs.writeFileSync(opts.filepath + '.crashlog', err.stack || err.toString());
            process.exit(1);
        });
    }

    // Validation.
    if (!opts.project || !opts.filepath) return plugin.help();
    if (!path.existsSync(path.dirname(opts.filepath)))
        return this.error(new Error('Export path does not exist: ' + path.dirname(opts.filepath)));

    // Format.
    if (!opts.format) opts.format = path.extname(opts.filepath).split('.').pop();
    if (!_(['pdf', 'svg', 'png', 'mbtiles', 'upload']).include(opts.format))
        return this.error(new Error('Invalid format: ' + opts.format));

    // Convert string params into numbers.
    if (!_(opts.bbox).isUndefined())
        opts.bbox = _(opts.bbox.split(',')).map(parseFloat);
    if (!_(opts.minzoom).isUndefined())
        opts.minzoom = parseInt(opts.minzoom, 10);
    if (!_(opts.maxzoom).isUndefined())
        opts.maxzoom = parseInt(opts.maxzoom, 10);
    if (!_(opts.width).isUndefined())
        opts.width = parseInt(opts.width, 10);
    if (!_(opts.height).isUndefined())
        opts.height = parseInt(opts.height, 10);

    // Rename the output filepath using a random hash if file already exists.
    if (path.existsSync(opts.filepath) && opts.format !== 'upload') {
        var hash = crypto.createHash('md5')
            .update(+new Date + '')
            .digest('hex')
            .substring(0, 6);
        var ext = path.extname(opts.filepath);
        opts.filepath = opts.filepath.replace(ext, '_' + hash + ext);

        // Update filename in TileMill.
        this.put({ filename: path.basename(opts.filepath), status: 'processing' });
    }

    // Set process title.
    process.title = 'tm-' + path.basename(opts.filepath);

    // Catch SIGINT.
    process.on('SIGINT', function () {
        console.log('Got SIGINT. Run "kill ' + process.pid + '" to terminate.');
    });
    process.on('SIGUSR1', process.exit);

    // Upload format does not require loaded project.
    if (opts.format === 'upload') return this[opts.format](callback);

    // Load project, localize and call export function.
    var cmd = this;
    var model = new models.Project({id:opts.project});
    Step(function() {
        Bones.utils.fetch({model:model}, this);
    }, function(err) {
        if (err) throw err;
        model.localize(model.toJSON(), this);
    }, function(err) {
        if (err) return cmd.error(err);

        model.mml = _(model.mml).extend({
            name: model.mml.name || model.id,
            version: model.mml.version || '1.0.0',
            minzoom: !_(opts.minzoom).isUndefined() ? opts.minzoom : model.get('minzoom'),
            maxzoom: !_(opts.maxzoom).isUndefined() ? opts.maxzoom : model.get('maxzoom'),
            bounds: !_(opts.bbox).isUndefined() ? opts.bbox : model.get('bounds')
        });

        // Unset map center if outside bounds.
        var validCenter = (function(center, bounds, minzoom, maxzoom) {
            if (center[0] < bounds[0] ||
                center[0] > bounds[2] ||
                center[1] < bounds[1] ||
                center[1] > bounds[3]) return false;
            if (center[2] < minzoom) return false;
            if (center[2] > maxzoom) return false;
            return true;
        })(model.mml.center, model.mml.bounds, model.mml.minzoom, model.mml.maxzoom);
        if (!validCenter) delete model.mml.center;

        cmd[opts.format](model, callback);
    });
};

command.prototype.error = function(err) {
    this.put({
        status: 'error',
        error: err.toString(),
        updated: +new Date()
    });
    console.error(err.toString());
};

command.prototype.remaining = function(progress, started) {
    return Math.floor(
        (Date.now() - started) * (1 / progress) -
        (Date.now() - started)
    );
};

command.prototype.put = function(data, callback) {
    callback = callback || function() {};
    if (!this.opts.url) return callback();
    request.put({
        uri: this.opts.url,
        headers: {
            'Host': url.parse(this.opts.url).host,
            'Cookie': 'bones.token=token'
        },
        json: _(data).extend({'bones.token': 'token'})
    }, callback);
};

command.prototype.png =
command.prototype.svg =
command.prototype.pdf = function(project, callback) {
    var mapnik = require('mapnik');
    var sm = new (require('sphericalmercator'))();
    var map = new mapnik.Map(this.opts.width, this.opts.height);

    map.fromStringSync(project.xml, {
        strict: false,
        base: path.join(this.opts.files, 'project', project.id) + '/'
    });
    map.bufferSize = this.opts.bufferSize;
    map.extent = sm.convert(project.mml.bounds, '900913');
    try {
        map.renderFileSync(this.opts.filepath, { format: this.opts.format });
        this.put({
            status: 'complete',
            progress: 1,
            updated: +new Date()
        }, process.exit);
    } catch(err) {
        this.error(err);
    }
};

command.prototype.mbtiles = function (project, callback) {
    var cmd = this;
    var tilelive = require('tilelive');
    require('mbtiles').registerProtocols(tilelive);
    require('tilelive-mapnik').registerProtocols(tilelive);

    var uri = {
        protocol: 'mapnik:',
        slashes: true,
        xml: project.xml,
        mml: project.mml,
        pathname: path.join(this.opts.files, 'project', project.id, project.id + '.xml'),
        query: { bufferSize: this.opts.bufferSize }
    };
    var source;
    var sink;

    Step(function() {
        tilelive.load(uri, this);
    }, function(err, s) {
        if (err) throw err;
        source = s;
        tilelive.load('mbtiles://' + cmd.opts.filepath, this);
    }, function(err, s) {
        if (err) throw err;
        sink = s;
        sink.startWriting(this);
    }, function(err) {
        if (err) throw err;
        sink.putInfo(project.mml, this);
    }, function(err) {
        if (err) return cmd.error(err);

        var copy = tilelive.copy({
            source: source,
            sink: sink,
            bbox: project.mml.bounds,
            minZoom: project.mml.minzoom,
            maxZoom: project.mml.maxzoom,
            concurrency: 100,
            tiles: true,
            grids: !!project.mml.interactivity
        });

        var timeout = setInterval(function progress() {
            var progress = (copy.copied + copy.failed) / copy.total;
            cmd.put({
                status: progress < 1 ? 'processing' : 'complete',
                progress: progress,
                remaining: cmd.remaining(progress, copy.started),
                updated: +new Date(),
            });
        }, 5000);

        copy.on('warning', function(err) {
            console.log(err);
        });
        copy.on('finished', function() {
            clearTimeout(timeout);
            cmd.put({
                status: 'complete',
                progress: 1,
                updated: +new Date()
            }, process.exit);
        });
        copy.on('error', function(err) {
            clearTimeout(timeout);
            cmd.error(err);
        });
    });
};

command.prototype.upload = function (callback) {
    var cmd = this;
    var key;
    var bucket;
    var mapURL = '';
    var freeURL = '';
    var modelURL = '';
    var hash = crypto.createHash('md5')
        .update(+new Date + '')
        .digest('hex')
        .substring(0, 6);
    var policyEndpoint = url.format({
        protocol: 'http:',
        host: this.opts.mapboxHost || 'api.tiles.mapbox.com',
        pathname: '/v2/'+ hash + '/upload.json'
    });

    // Set URLs for account uploads.
    if (this.opts.syncAccount && this.opts.syncAccessToken) {
        mapURL = _('<%=base%>/<%=account%>/map/<%=handle%>')
            .template({
                base: this.opts.syncURL,
                account: this.opts.syncAccount,
                handle: this.opts.project
            });
        modelURL = _('<%=base%>/api/Map/<%=account%>.<%=handle%>?access_token=<%=token%>')
            .template({
                base: this.opts.syncURL,
                account: this.opts.syncAccount,
                handle: this.opts.project,
                token: this.opts.syncAccessToken
            });
    }

    Step(function() {
        request.get({
            uri: policyEndpoint,
            headers: { 'Host': url.parse(policyEndpoint).host }
        }, this);
    }, function(err, resp, body) {
        if (err) throw err;
        if (resp.statusCode !== 200)
            throw new Error('MapBox Hosting is not available. Status ' + resp.statusCode + '.');

        // Let Step catch thrown errors here.
        uploadArgs = JSON.parse(body);
        key = uploadArgs.key;
        bucket = uploadArgs.bucket;
        delete uploadArgs.bucket;
        delete uploadArgs.filename;

        var stat = fs.statSync(cmd.opts.filepath);
        var boundary = '----TileMill' + crypto.createHash('md5')
            .update(+new Date + '')
            .digest('hex')
            .substring(0, 6);
        var filename = path.basename(cmd.opts.filepath);
        var multipartBody = new Buffer(_(uploadArgs).map(function(value, key) {
            return '--' + boundary + '\r\n'
                + 'Content-Disposition: form-data; name="' + key + '"\r\n'
                + '\r\n' + value + '\r\n';
            })
            .concat(['--' + boundary + '\r\n'
                + 'Content-Disposition: form-data; name="file"; filename="' + filename + '"\r\n'
                + 'Content-Type: application/octet-stream\r\n\r\n'])
            .join(''));
        var terminate = new Buffer('\r\n--' + boundary + '--', 'ascii');

        var dest = http.request({
            host: bucket + '.s3.amazonaws.com',
            path: '/',
            method: 'POST',
            headers: {
                'Content-Type': 'multipart/form-data; boundary=' + boundary,
                'Content-Length': stat.size + multipartBody.length + terminate.length,
                'X_FILE_NAME': filename
            }
        }, function(resp) {
            if (resp.statusCode !== 303)
                return this(new Error('S3 is not available. Status ' + resp.statusCode + '.'));

            freeURL = resp.headers.location.split('?')[0];
            this();
        }.bind(this))

        // Write multipart values from memory.
        dest.write(multipartBody, 'ascii');

        // Set up read for MBTiles file and start the upload.
        var bytesWritten = 0;
        var started = Date.now();
        var updated = Date.now();
        fs.createReadStream(cmd.opts.filepath)
            .on('data', function(chunk) {
                bytesWritten += chunk.length;

                if (Date.now() < updated + 5000) return;

                var progress = bytesWritten / stat.size;
                updated = Date.now();
                cmd.put({
                    status: 'complete',
                    progress: progress,
                    status: progress < 1 ? 'processing' : 'complete',
                    remaining: cmd.remaining(progress, started),
                    updated: updated
                });
            })
            .on('end', function() {
                dest.write(terminate);
                dest.end();
            })
            .pipe(dest, {end: false});
    }, function(err) {
        if (err) throw err;
        if (!modelURL) return this(); // Free

        request.get(modelURL, this);
    }, function(err, res, body) {
        if (err) throw err;
        if (!modelURL) return this(); // Free

        // Let Step catch thrown errors here.
        var model = _(res.statusCode === 404 ? {} : JSON.parse(body)).extend({
            id: cmd.opts.syncAccount + '.' + cmd.opts.project,
            _type: 'tileset',
            created: +new Date,
            status: 'pending',
            url: 'http://' + bucket + '.s3.amazonaws.com/' + key
        });
        request.put({ url:modelURL, json:model }, this);
    }, function(err, res, body) {
        if (err) return cmd.error(err);
        if (modelURL && res.statusCode !== 200)
            return cmd.error('Map publish failed: ' + res.statusCode);
        cmd.put({
            status: 'complete',
            progress: 1,
            url: modelURL ? mapURL : freeURL,
            updated: +new Date()
        }, process.exit);
    });
};
<commit_msg>Add better logging / error reporting to CL. Return with error code where appropriate. Closes #1013.<commit_after>var fs = require('fs');
var url = require('url');
var path = require('path');
var request = require('request');
var crypto = require('crypto');
var Step = require('step');

command = Bones.Command.extend();

command.description = 'export project';
command.usage = '<project> <export>';

command.options['format'] = {
    'title': 'format=[format]',
    'description': 'Export format (png|pdf|svg|mbtiles).'
};

command.options['bbox'] = {
    'title': 'bbox=[bbox]',
    'description': 'Comma separated coordinates of bounding box to export.'
};

command.options['minzoom'] = {
    'title': 'minzoom=[zoom]',
    'description': 'MBTiles: minimum zoom level to export.'
};

command.options['maxzoom'] = {
    'title': 'maxzoom=[zoom]',
    'description': 'MBTiles: maximum zoom level to export.'
};

command.options['width'] = {
    'title': 'width=[width]',
    'description': 'Image: image width in pixels.',
    'default': 400
};

command.options['height'] = {
    'title': 'height=[height]',
    'description': 'Image: image height in pixels.',
    'default': 400
};

command.options['url'] = {
    'title': 'url=[url]',
    'description': 'URL to PUT updates to.'
};

command.options['log'] = {
    'title': 'log',
    'description': 'Write crash logs to destination directory.'
};

command.prototype.initialize = function(plugin, callback) {
    var opts = plugin.config;
    if (process.env.tilemillConfig)
        _(opts).extend(JSON.parse(process.env.tilemillConfig));
    opts.files = path.resolve(opts.files);
    opts.project = plugin.argv._[1];
    opts.filepath = path.resolve(plugin.argv._[2]);
    callback = callback || function() {};
    this.opts = opts;

    // Write crash log
    if (opts.log) {
        process.on('uncaughtException', function(err) {
            fs.writeFileSync(opts.filepath + '.crashlog', err.stack || err.toString());
            process.exit(1);
        });
    }

    // Validation.
    if (!opts.project || !opts.filepath) return plugin.help();
    if (!path.existsSync(path.dirname(opts.filepath)))
        return this.error(new Error('Export path does not exist: ' + path.dirname(opts.filepath)));

    // Format.
    if (!opts.format) opts.format = path.extname(opts.filepath).split('.').pop();
    if (!_(['pdf', 'svg', 'png', 'mbtiles', 'upload']).include(opts.format))
        return this.error(new Error('Invalid format: ' + opts.format));

    // Convert string params into numbers.
    if (!_(opts.bbox).isUndefined())
        opts.bbox = _(opts.bbox.split(',')).map(parseFloat);
    if (!_(opts.minzoom).isUndefined())
        opts.minzoom = parseInt(opts.minzoom, 10);
    if (!_(opts.maxzoom).isUndefined())
        opts.maxzoom = parseInt(opts.maxzoom, 10);
    if (!_(opts.width).isUndefined())
        opts.width = parseInt(opts.width, 10);
    if (!_(opts.height).isUndefined())
        opts.height = parseInt(opts.height, 10);

    // Rename the output filepath using a random hash if file already exists.
    if (path.existsSync(opts.filepath) && opts.format !== 'upload') {
        var hash = crypto.createHash('md5')
            .update(+new Date + '')
            .digest('hex')
            .substring(0, 6);
        var ext = path.extname(opts.filepath);
        opts.filepath = opts.filepath.replace(ext, '_' + hash + ext);

        // Update filename in TileMill.
        this.put({ filename: path.basename(opts.filepath), status: 'processing' });
    }

    // Set process title.
    process.title = 'tm-' + path.basename(opts.filepath);

    // Catch SIGINT.
    process.on('SIGINT', function () {
        console.log('Got SIGINT. Run "kill ' + process.pid + '" to terminate.');
    });
    process.on('SIGUSR1', process.exit);

    // Upload format does not require loaded project.
    if (opts.format === 'upload') return this[opts.format](callback);

    // Load project, localize and call export function.
    var cmd = this;
    var model = new models.Project({id:opts.project});
    Step(function() {
        Bones.utils.fetch({model:model}, this);
    }, function(err) {
        if (err) throw err;
        model.localize(model.toJSON(), this);
    }, function(err) {
        if (err) return cmd.error(err, function() {
            process.exit(1);
        });

        model.mml = _(model.mml).extend({
            name: model.mml.name || model.id,
            version: model.mml.version || '1.0.0',
            minzoom: !_(opts.minzoom).isUndefined() ? opts.minzoom : model.get('minzoom'),
            maxzoom: !_(opts.maxzoom).isUndefined() ? opts.maxzoom : model.get('maxzoom'),
            bounds: !_(opts.bbox).isUndefined() ? opts.bbox : model.get('bounds')
        });

        // Unset map center if outside bounds.
        var validCenter = (function(center, bounds, minzoom, maxzoom) {
            if (center[0] < bounds[0] ||
                center[0] > bounds[2] ||
                center[1] < bounds[1] ||
                center[1] > bounds[3]) return false;
            if (center[2] < minzoom) return false;
            if (center[2] > maxzoom) return false;
            return true;
        })(model.mml.center, model.mml.bounds, model.mml.minzoom, model.mml.maxzoom);
        if (!validCenter) delete model.mml.center;

        cmd[opts.format](model, callback);
    });
};

command.prototype.error = function(err, callback) {
    this.put({
        status: 'error',
        error: err.toString(),
        updated: +new Date()
    }, callback);
};

command.prototype.remaining = function(progress, started) {
    return Math.floor(
        (Date.now() - started) * (1 / progress) -
        (Date.now() - started)
    );
};

command.prototype.put = function(data, callback) {
    callback = callback || function() {};
    data.status == 'error' ?
        console.log(JSON.stringify(data)) :
        console.error(JSON.stringify(data));

    if (!this.opts.url) return callback();
    request.put({
        uri: this.opts.url,
        headers: {
            'Host': url.parse(this.opts.url).host,
            'Cookie': 'bones.token=token'
        },
        json: _(data).extend({'bones.token': 'token'})
    }, callback);
};

command.prototype.png =
command.prototype.svg =
command.prototype.pdf = function(project, callback) {
    var mapnik = require('mapnik');
    var sm = new (require('sphericalmercator'))();
    var map = new mapnik.Map(this.opts.width, this.opts.height);

    map.fromStringSync(project.xml, {
        strict: false,
        base: path.join(this.opts.files, 'project', project.id) + '/'
    });
    map.bufferSize = this.opts.bufferSize;
    map.extent = sm.convert(project.mml.bounds, '900913');
    try {
        map.renderFileSync(this.opts.filepath, { format: this.opts.format });
        this.put({
            status: 'complete',
            progress: 1,
            updated: +new Date()
        }, process.exit);
    } catch(err) {
        this.error(err);
    }
};

command.prototype.mbtiles = function (project, callback) {
    var cmd = this;
    var tilelive = require('tilelive');
    require('mbtiles').registerProtocols(tilelive);
    require('tilelive-mapnik').registerProtocols(tilelive);

    var uri = {
        protocol: 'mapnik:',
        slashes: true,
        xml: project.xml,
        mml: project.mml,
        pathname: path.join(this.opts.files, 'project', project.id, project.id + '.xml'),
        query: { bufferSize: this.opts.bufferSize }
    };
    var source;
    var sink;

    Step(function() {
        tilelive.load(uri, this);
    }, function(err, s) {
        if (err) throw err;
        source = s;
        tilelive.load('mbtiles://' + cmd.opts.filepath, this);
    }, function(err, s) {
        if (err) throw err;
        sink = s;
        sink.startWriting(this);
    }, function(err) {
        if (err) throw err;
        sink.putInfo(project.mml, this);
    }, function(err) {
        if (err) return cmd.error(err, function() {
            process.exit(1);
        });

        var copy = tilelive.copy({
            source: source,
            sink: sink,
            bbox: project.mml.bounds,
            minZoom: project.mml.minzoom,
            maxZoom: project.mml.maxzoom,
            concurrency: 100,
            tiles: true,
            grids: !!project.mml.interactivity
        });

        var timeout = setInterval(function progress() {
            var progress = (copy.copied + copy.failed) / copy.total;
            cmd.put({
                status: progress < 1 ? 'processing' : 'complete',
                progress: progress,
                remaining: cmd.remaining(progress, copy.started),
                updated: +new Date(),
            });
        }, 5000);

        copy.on('warning', function(err) {
            console.log(err);
        });
        copy.on('finished', function() {
            clearTimeout(timeout);
            cmd.put({
                status: 'complete',
                progress: 1,
                updated: +new Date()
            }, process.exit);
        });
        copy.on('error', function(err) {
            clearTimeout(timeout);
            cmd.error(err, function() {
                process.exit(1);
            });
        });
    });
};

command.prototype.upload = function (callback) {
    var cmd = this;
    var key;
    var bucket;
    var mapURL = '';
    var freeURL = '';
    var modelURL = '';
    var hash = crypto.createHash('md5')
        .update(+new Date + '')
        .digest('hex')
        .substring(0, 6);
    var policyEndpoint = url.format({
        protocol: 'http:',
        host: this.opts.mapboxHost || 'api.tiles.mapbox.com',
        pathname: '/v2/'+ hash + '/upload.json'
    });

    // Set URLs for account uploads.
    if (this.opts.syncAccount && this.opts.syncAccessToken) {
        mapURL = _('<%=base%>/<%=account%>/map/<%=handle%>')
            .template({
                base: this.opts.syncURL,
                account: this.opts.syncAccount,
                handle: this.opts.project
            });
        modelURL = _('<%=base%>/api/Map/<%=account%>.<%=handle%>?access_token=<%=token%>')
            .template({
                base: this.opts.syncURL,
                account: this.opts.syncAccount,
                handle: this.opts.project,
                token: this.opts.syncAccessToken
            });
    }

    Step(function() {
        request.get({
            uri: policyEndpoint,
            headers: { 'Host': url.parse(policyEndpoint).host }
        }, this);
    }, function(err, resp, body) {
        if (err) throw err;
        if (resp.statusCode !== 200)
            throw new Error('MapBox Hosting is not available. Status ' + resp.statusCode + '.');

        // Let Step catch thrown errors here.
        uploadArgs = JSON.parse(body);
        key = uploadArgs.key;
        bucket = uploadArgs.bucket;
        delete uploadArgs.bucket;
        delete uploadArgs.filename;

        var stat = fs.statSync(cmd.opts.filepath);
        var boundary = '----TileMill' + crypto.createHash('md5')
            .update(+new Date + '')
            .digest('hex')
            .substring(0, 6);
        var filename = path.basename(cmd.opts.filepath);
        var multipartBody = new Buffer(_(uploadArgs).map(function(value, key) {
            return '--' + boundary + '\r\n'
                + 'Content-Disposition: form-data; name="' + key + '"\r\n'
                + '\r\n' + value + '\r\n';
            })
            .concat(['--' + boundary + '\r\n'
                + 'Content-Disposition: form-data; name="file"; filename="' + filename + '"\r\n'
                + 'Content-Type: application/octet-stream\r\n\r\n'])
            .join(''));
        var terminate = new Buffer('\r\n--' + boundary + '--', 'ascii');

        var dest = http.request({
            host: bucket + '.s3.amazonaws.com',
            path: '/',
            method: 'POST',
            headers: {
                'Content-Type': 'multipart/form-data; boundary=' + boundary,
                'Content-Length': stat.size + multipartBody.length + terminate.length,
                'X_FILE_NAME': filename
            }
        }, function(resp) {
            if (resp.statusCode !== 303)
                return this(new Error('S3 is not available. Status ' + resp.statusCode + '.'));

            freeURL = resp.headers.location.split('?')[0];
            this();
        }.bind(this))

        // Write multipart values from memory.
        dest.write(multipartBody, 'ascii');

        // Set up read for MBTiles file and start the upload.
        var bytesWritten = 0;
        var started = Date.now();
        var updated = Date.now();
        fs.createReadStream(cmd.opts.filepath)
            .on('data', function(chunk) {
                bytesWritten += chunk.length;

                if (Date.now() < updated + 5000) return;

                var progress = bytesWritten / stat.size;
                updated = Date.now();
                cmd.put({
                    status: 'complete',
                    progress: progress,
                    status: progress < 1 ? 'processing' : 'complete',
                    remaining: cmd.remaining(progress, started),
                    updated: updated
                });
            })
            .on('end', function() {
                dest.write(terminate);
                dest.end();
            })
            .pipe(dest, {end: false});
    }, function(err) {
        if (err) throw err;
        if (!modelURL) return this(); // Free

        request.get(modelURL, this);
    }, function(err, res, body) {
        if (err) throw err;
        if (!modelURL) return this(); // Free

        // Let Step catch thrown errors here.
        var model = _(res.statusCode === 404 ? {} : JSON.parse(body)).extend({
            id: cmd.opts.syncAccount + '.' + cmd.opts.project,
            _type: 'tileset',
            created: +new Date,
            status: 'pending',
            url: 'http://' + bucket + '.s3.amazonaws.com/' + key
        });
        request.put({ url:modelURL, json:model }, this);
    }, function(err, res, body) {
        if (err) return cmd.error(err, function() {
            process.exit(1);
        });
        if (modelURL && res.statusCode !== 200)
            return cmd.error('Map publish failed: ' + res.statusCode, function() {
                process.exit(1);
            });
        cmd.put({
            status: 'complete',
            progress: 1,
            url: modelURL ? mapURL : freeURL,
            updated: +new Date()
        }, process.exit);
    });
};
<|endoftext|>


<commit_before>module.exports = {
    exec: async function (msg, args) {
        if (msg.guildConfig) {
            bot.createMessage(msg.channel.id, `The actual prefix for this server is \`${msg.guildConfig.prefix}\`.\nYou can still use \`tt.\` prefix. `)
        } else {
            bot.createMessage(msg.channel.id, "This server hasn't set any prefix. You can use `tt.` prefix")
        }
    },
    isCmd: true,
    display: true,
    category: 1,
    description: "Gets a server prefix.",
}<commit_msg>check if the prefix isn't config.prefix<commit_after>module.exports = {
    exec: async function (msg, args) {
        if (msg.guildConfig && msg.guildConfig.prefix !== config.prefix) {
            bot.createMessage(msg.channel.id, `The actual prefix for this server is \`${msg.guildConfig.prefix}\`.\nYou can still use \`tt.\` prefix. `)
        } else {
            bot.createMessage(msg.channel.id, "This server hasn't set any prefix. You can use `tt.` prefix.")
        }
    },
    isCmd: true,
    display: true,
    category: 1,
    description: "Gets a server prefix.",
}<|endoftext|>


<commit_before>const config = require('../config.js')
const log = require('../util/logger.js')
const dbOps = require('../util/dbOps.js')
const helpText = guildRss => `Proper usage:

\`${guildRss.prefix || config.bot.prefix}rsspatron servers add <server id>\` - Add your patron backing to a server via server ID or \`this\` for this server
\`${guildRss.prefix || config.bot.prefix}rsspatron servers remove <server id>\` - Remove your patron backing from a server via server ID or \`this\` for this server
\`${guildRss.prefix || config.bot.prefix}rsspatron servers list\` - List servers under your patron backing, and the maximum number of servers you may have
`

async function verifyServer (bot, serverId) {
  if (bot.shard && bot.shard.count > 0) {
    const results = (await bot.shard.broadcastEval(`
      const guild = this.guilds.get('${serverId}')
      guild ? { name: guild.name, id: guild.id } : null
    `)).filter(item => item)

    if (results.length > 0) return results[0]
  } else return bot.guilds.get(serverId)
}

async function switchServerArg (bot, message, args, vipUser, guildRss) {
  try {
    const action = args.shift() // Third arg
    if (!action) return await message.channel.send('You must specify either `add` or `remove` as your third argument.')
    let server = args.shift() // Fourth arg
    if (action !== 'list' && !server) return await message.channel.send('You must specify the server ID, or `this` (to specify this server) as your fourth argument..')
    if (server === 'this') server = message.guild.id

    if (action === 'add') {
      if ((vipUser.maxServers && vipUser.servers.length >= vipUser.maxServers) || (!vipUser.maxServers && vipUser.servers.length === 1)) return await message.channel.send(`You cannot add any more servers for your patron status. Your maximum is ${vipUser.maxServers ? vipUser.maxServers : 1}.`)
      if (vipUser.servers.includes(server)) return await message.channel.send(`That server already has your patron backing.`)
      const m = await message.channel.send(`Adding server ${server}...`)
      const gotServer = await verifyServer(bot, server)
      if (!gotServer) return await m.edit(`Unable to add server \`${server}\`. Either it does not exist, or I am not in it.`)
      await dbOps.vips.addServers({ vipUser, serversToAdd: [server] })
      await m.edit(`Successfully added ${server} (${gotServer.name})`)
    } else if (action === 'remove') {
      if (!vipUser.servers.includes(server)) return await message.channel.send(`That server does not have your patron backing.`)
      const m2 = await message.channel.send(`Removing server ${server}...`)
      await dbOps.vips.removeServers({ vipUser, serversToRemove: [server] })
      await m2.edit(`Successfully removed`)
    } else if (action === 'list') {
      if (!vipUser.servers || vipUser.servers.length === 0) return await message.channel.send(`You have no servers under your patron backing. The maximum number of servers you may have under your patron backing is ${vipUser.maxServers ? vipUser.maxServers : 1}.`)
      const myServers = vipUser.servers
      let content = `The maximum number of servers you may add your patron backing to is ${vipUser.maxServers ? vipUser.maxServers : 1}. The following guilds are backed by your patron status:\n\n`
      for (const id of myServers) {
        const gotServer = await verifyServer(bot, id)
        content += gotServer ? `${gotServer.id} (${gotServer.name})\n` : `${id} (Unknown)\n`
      }

      await message.channel.send(content)
    } else {
      await message.channel.send(`Invalid command usage. ${helpText(guildRss)}`)
    }
  } catch (err) {
    log.command.warning('rsspatron servers', message.guild, err, true)
    if (err.code !== 50013) message.channel.send(err.message).catch(err => log.command.warning('rsspatron servers', message.guild, err))
  }
}

module.exports = async (bot, message) => {
  try {
    const [ guildRss, vipUser ] = await Promise.all([ dbOps.guildRss.get(message.guild.id), dbOps.vips.get(message.author.id) ])

    if (!vipUser || vipUser.invalid === true) return await message.channel.send('You must be a patron to use this command.')
    const args = message.content.toLowerCase().split(' ').map(item => item.trim())
    args.shift() // Remove prefix
    if (args.length === 0) {
      return await message.channel.send(helpText(guildRss))
    }
    const type = args.shift() // Second arg
    if (type === 'servers') switchServerArg(bot, message, args, vipUser, guildRss)
    else await message.channel.send(`Invalid command usage. ${helpText(guildRss)}`)
    // switch (type) {
    //   case 'servers':

    //     break
    // case 'refresh':
    //   if (timeLimited[message.author.id]) {
    //     log.command.warning('Blocked refresh due to time limit', message.author)
    //     return await message.channel.send(`${message.author.toString()} Please wait 5 minutes after the last use of this command before using it again.`)
    //   }
    //   timeLimited[message.author.id] = true
    //   setTimeout(() => delete timeLimited[message.author.id], 300000) // 5 minutes
    //   const m = await message.channel.send('Refreshing...')
    //   dbOps.vips.refresh(async err => {
    //     try {
    //       if (err) {
    //         log.command.error('Failed to update VIPs', message.author, err)
    //         return await m.edit(`Failed to refresh patrons: ` + err.message)
    //       }
    //       log.command.success(`Refreshed VIPs`, message.author)
    //       await m.edit(`Successfully updated patrons.`)
    //     } catch (err) {
    //       log.command.warning('rsspatron 2', err, message.author)
    //     }
    //   })
    //   break
    // default:
    //   await message.channel.send('Invalid command usage')
    // }
  } catch (err) {
    log.command.warning('rsspatron', err, message.author)
    if (err.code !== 50013) message.channel.send(err.message).catch(err => log.command.warning('rsspatron 1', message.guild, err))
  }
}
<commit_msg>Add undefined check<commit_after>const config = require('../config.js')
const log = require('../util/logger.js')
const dbOps = require('../util/dbOps.js')
const helpText = guildRss => `Proper usage:

\`${guildRss ? (guildRss.prefix || config.bot.prefix) : config.bot.prefix}rsspatron servers add <server id>\` - Add your patron backing to a server via server ID or \`this\` for this server
\`${guildRss ? (guildRss.prefix || config.bot.prefix) : config.bot.prefix}rsspatron servers remove <server id>\` - Remove your patron backing from a server via server ID or \`this\` for this server
\`${guildRss ? (guildRss.prefix || config.bot.prefix) : config.bot.prefix}rsspatron servers list\` - List servers under your patron backing, and the maximum number of servers you may have
`

async function verifyServer (bot, serverId) {
  if (bot.shard && bot.shard.count > 0) {
    const results = (await bot.shard.broadcastEval(`
      const guild = this.guilds.get('${serverId}')
      guild ? { name: guild.name, id: guild.id } : null
    `)).filter(item => item)

    if (results.length > 0) return results[0]
  } else return bot.guilds.get(serverId)
}

async function switchServerArg (bot, message, args, vipUser, guildRss) {
  try {
    const action = args.shift() // Third arg
    if (!action) return await message.channel.send('You must specify either `add` or `remove` as your third argument.')
    let server = args.shift() // Fourth arg
    if (action !== 'list' && !server) return await message.channel.send('You must specify the server ID, or `this` (to specify this server) as your fourth argument..')
    if (server === 'this') server = message.guild.id

    if (action === 'add') {
      if ((vipUser.maxServers && vipUser.servers.length >= vipUser.maxServers) || (!vipUser.maxServers && vipUser.servers.length === 1)) return await message.channel.send(`You cannot add any more servers for your patron status. Your maximum is ${vipUser.maxServers ? vipUser.maxServers : 1}.`)
      if (vipUser.servers.includes(server)) return await message.channel.send(`That server already has your patron backing.`)
      const m = await message.channel.send(`Adding server ${server}...`)
      const gotServer = await verifyServer(bot, server)
      if (!gotServer) return await m.edit(`Unable to add server \`${server}\`. Either it does not exist, or I am not in it.`)
      await dbOps.vips.addServers({ vipUser, serversToAdd: [server] })
      await m.edit(`Successfully added ${server} (${gotServer.name})`)
    } else if (action === 'remove') {
      if (!vipUser.servers.includes(server)) return await message.channel.send(`That server does not have your patron backing.`)
      const m2 = await message.channel.send(`Removing server ${server}...`)
      await dbOps.vips.removeServers({ vipUser, serversToRemove: [server] })
      await m2.edit(`Successfully removed`)
    } else if (action === 'list') {
      if (!vipUser.servers || vipUser.servers.length === 0) return await message.channel.send(`You have no servers under your patron backing. The maximum number of servers you may have under your patron backing is ${vipUser.maxServers ? vipUser.maxServers : 1}.`)
      const myServers = vipUser.servers
      let content = `The maximum number of servers you may add your patron backing to is ${vipUser.maxServers ? vipUser.maxServers : 1}. The following guilds are backed by your patron status:\n\n`
      for (const id of myServers) {
        const gotServer = await verifyServer(bot, id)
        content += gotServer ? `${gotServer.id} (${gotServer.name})\n` : `${id} (Unknown)\n`
      }

      await message.channel.send(content)
    } else {
      await message.channel.send(`Invalid command usage. ${helpText(guildRss)}`)
    }
  } catch (err) {
    log.command.warning('rsspatron servers', message.guild, err, true)
    if (err.code !== 50013) message.channel.send(err.message).catch(err => log.command.warning('rsspatron servers', message.guild, err))
  }
}

module.exports = async (bot, message) => {
  try {
    const [ guildRss, vipUser ] = await Promise.all([ dbOps.guildRss.get(message.guild.id), dbOps.vips.get(message.author.id) ])

    if (!vipUser || vipUser.invalid === true) return await message.channel.send('You must be a patron to use this command.')
    const args = message.content.toLowerCase().split(' ').map(item => item.trim())
    args.shift() // Remove prefix
    if (args.length === 0) {
      return await message.channel.send(helpText(guildRss))
    }
    const type = args.shift() // Second arg
    if (type === 'servers') switchServerArg(bot, message, args, vipUser, guildRss)
    else await message.channel.send(`Invalid command usage. ${helpText(guildRss)}`)
    // switch (type) {
    //   case 'servers':

    //     break
    // case 'refresh':
    //   if (timeLimited[message.author.id]) {
    //     log.command.warning('Blocked refresh due to time limit', message.author)
    //     return await message.channel.send(`${message.author.toString()} Please wait 5 minutes after the last use of this command before using it again.`)
    //   }
    //   timeLimited[message.author.id] = true
    //   setTimeout(() => delete timeLimited[message.author.id], 300000) // 5 minutes
    //   const m = await message.channel.send('Refreshing...')
    //   dbOps.vips.refresh(async err => {
    //     try {
    //       if (err) {
    //         log.command.error('Failed to update VIPs', message.author, err)
    //         return await m.edit(`Failed to refresh patrons: ` + err.message)
    //       }
    //       log.command.success(`Refreshed VIPs`, message.author)
    //       await m.edit(`Successfully updated patrons.`)
    //     } catch (err) {
    //       log.command.warning('rsspatron 2', err, message.author)
    //     }
    //   })
    //   break
    // default:
    //   await message.channel.send('Invalid command usage')
    // }
  } catch (err) {
    log.command.warning('rsspatron', err, message.author)
    if (err.code !== 50013) message.channel.send(err.message).catch(err => log.command.warning('rsspatron 1', message.guild, err))
  }
}
<|endoftext|>


<commit_before>export default list

import Entry from '../entry'
import PouchDB from 'pouchdb'
import {write} from './output'
import moment from 'moment'
import _ from 'lodash'

let db = new PouchDB('tickbin')

function list (yargs) {
  let argv = yargs
  .usage('tick list')
  .help('h')
  .alias('h', 'help')
  .argv

  db.query(mapDate, {
    include_docs: true,
    descending: true
  }).then(writeEntries)

}

function mapDate (doc) {
  emit(doc.fromArr)
}

function writeEntries (results) {
  let prevDate = null
  let dat = _.chain(results.rows)
  .map(row => { return Entry.fromJSON(row.doc) })
  .groupBy(e => { return moment(e.from).startOf('day').format('YYYY-MM-DD') })
  .map((group, d) => {
    return {
      ticks: group,
      date: moment(d, 'YYYY-MM-DD').toDate(),
      minutes: _.reduce(group, (sum, e) => { return sum + e.duration.minutes }, 0)
    }
  })
  .each(group => {
    console.log(moment(group.date).format('ddd, MMM DD, YYYY') + ' ' + group.minutes)
    group.ticks.forEach(write)
  })
  .value()
}
<commit_msg>display time as 5h 15m<commit_after>export default list

import Entry from '../entry'
import PouchDB from 'pouchdb'
import {write} from './output'
import moment from 'moment'
import _ from 'lodash'

let db = new PouchDB('tickbin')

function list (yargs) {
  let argv = yargs
  .usage('tick list')
  .help('h')
  .alias('h', 'help')
  .argv

  db.query(mapDate, {
    include_docs: true,
    descending: true
  }).then(writeEntries)

}

function mapDate (doc) {
  emit(doc.fromArr)
}

function writeEntries (results) {
  let prevDate = null
  let dat = _.chain(results.rows)
  .map(row => { return Entry.fromJSON(row.doc) })
  .groupBy(e => { return moment(e.from).startOf('day').format('YYYY-MM-DD') })
  .map((group, d) => {
    return {
      ticks: group,
      date: moment(d, 'YYYY-MM-DD').toDate(),
      minutes: _.reduce(group, (sum, e) => { return sum + e.duration.minutes }, 0)
    }
  })
  .each(group => {
    const date = moment(group.date).format('ddd, MMM DD, YYYY')
    const time = `${Math.floor(group.minutes / 60)}h ${group.minutes % 60}m`
    console.log(`${date} ${time}`)
    group.ticks.forEach(write)
  })
  .value()
}
<|endoftext|>


<commit_before>import React from 'react'
import Component from './component';

import AppActions from '../action/app';
import UserStore from '../store/user';

export default class UserInfo extends Component {
	/**
	 *
	 */
	constructor () {
		super();

		this._bind('onChange', 'onLogoutClick');

		this.state = UserStore.getAll();
	}

	/**
	 *
	 */
	componentDidMount () {
		UserStore.addChangeListener(this.onChange);
	}

	/**
	 *
	 */
	componentWillUnmount () {
		UserStore.removeChangeListener(this.onChange);
	}

	/**
	 *
	 */
	render () {
		let content;

		if (this.state.loggedIn) {
			content = (

					{this.state.username + ' '}
					{'(logout)'}

			);
		}

		return (

				{content}

		);
	}

	/**
	 *
	 */
	onLogoutClick () {
		AppActions.logout();
	}

	/**
	 *
	 */
	onChange () {
		this.setState(UserStore.getAll());
	}
}<commit_msg>Adding ability to click your username and jump to your user in the Users view.  Also updating getState() for consistency<commit_after>import React from 'react'
import Component from './component';

import AppActions from '../action/app';
import SessionStore from '../store/session';

function getState () {
	return {
		user: SessionStore.getUser(),
		loggedIn: SessionStore.isLoggedIn()
	};
}

export default class UserInfo extends Component {
	/**
	 *
	 */
	constructor () {
		super();

		this._bind(
			'onStoreChange',
			'onLogoutClick',
			'onUsernameClick'
		);

		this.state = getState();
	}

	/**
	 *
	 */
	componentDidMount () {
		SessionStore.addChangeListener(this.onStoreChange);
	}

	/**
	 *
	 */
	componentWillUnmount () {
		SessionStore.removeChangeListener(this.onStoreChange);
	}

	/**
	 *
	 */
	render () {
		let content;

		if (this.state.loggedIn) {
			content = (

					{this.state.user.username + ' '}
					{'(logout)'}

			);
		}

		return (

				{content}

		);
	}

	/**
	 *
	 */
	onLogoutClick () {
		AppActions.logout();
	}

	/**
	 *
	 */
	onUsernameClick () {
		AppActions.selectUser(this.state.user);
		AppActions.navigate('users');
	}

	/**
	 *
	 */
	onStoreChange () {
		this.setState(SessionStore.getAll());
	}
}<|endoftext|>


<commit_before>import React, { Component } from 'react';
import {
  Text,
  View,
  Image,
  TouchableHighlight,
  TouchableOpacity
} from 'react-native';
import styles from '../styles.ios.js';
import Nav from './Navbar.ios.js';
import SearchBar from './Searchbar.ios.js';
import { connect } from "react-redux";
import {bindActionCreators} from 'redux';
import * as app from '../actions/appActions.ios.js';
import getApp from '../reducers/appReducers.ios.js';
import camera from './Camera.ios.js';

class App extends Component {
  constructor(props) {
    super(props);
  }

  border(color) {
    return {
      borderColor: color,
      borderWidth: 5,
    }
  }

  changeNavigationHome() {
    this.props.navigator.push({
      id: 'Home'
    })
  }

  changeNavigationCamera() {
    this.props.navigator.push({
      component: camera
    })
  }

  render() {
    console.log('props', this.props.actions.default)
    const {state, actions} = this.props;
    if (state.showSearchBar) {
      return (
        <View style={[styles.container, this.border('yellow')]}>
          <View style={[styles.navigation, this.border('pink')]} >
            <Nav
              changeNavigationCamera={this.changeNavigationCamera.bind(this)}
            />
          </View>
          <View style={[styles.app, this.border('black')]} >
            <Text style={styles.welcome}>
              Welcome to Snack Time!
            </Text>
          </View>
          <View style={[styles.searchBarPictureFrame, this.border('red')]} >
            <SearchBar navigator={this.props.navigator}/>
          </View>
          <TouchableHighlight style={[styles.buttonView]} onPress={this.changeNavigationCamera.bind(this)}>
            <Image style={[styles.takePicture, this.border('green')]} source={{uri: 'https://s3.amazonaws.com/features.ifttt.com/newsletter_images/2015_February/camera512x512+(1).png'}}/>
          </TouchableHighlight>
        </View>
      );
    } else {
      return (
        <View style={[styles.container, this.border('yellow')]}>
          <View style={[styles.navigation, this.border('pink')]} >
            <Nav
              changeNavigationCamera={this.changeNavigationCamera.bind(this)}
            />
          </View>
          <View style={[styles.app, this.border('black')]} >
            <Text style={styles.welcome}>
              Welcome to Snack Time!
            </Text>
          </View>
          <TouchableHighlight style={[styles.searchBarPictureFrame, this.border('red')]} onPress={actions.default} >
            <Image style={styles.searchBarPicture} source={{uri: 'https://d30y9cdsu7xlg0.cloudfront.net/png/449337-200.png'}} />
          </TouchableHighlight>
          <TouchableHighlight style={[styles.buttonView]} onPress={this.changeNavigationCamera.bind(this)}>
            <Image style={[styles.takePicture, this.border('green')]} source={{uri: 'https://s3.amazonaws.com/features.ifttt.com/newsletter_images/2015_February/camera512x512+(1).png'}}/>
          </TouchableHighlight>
        </View>
      );
    }
  }
}

export default connect(state => ({
    state: state.app
  }),
  (dispatch) => ({
    actions: bindActionCreators(app, dispatch)
  })
)(App);
<commit_msg>fixed changes<commit_after>import React, { Component } from 'react';
import {
  Text,
  View,
  Image,
  TouchableHighlight,
  TouchableOpacity
} from 'react-native';
import styles from '../styles.ios.js';
import Nav from './Navbar.ios.js';
import SearchBar from './Searchbar.ios.js';
import { connect } from "react-redux";
import {bindActionCreators} from 'redux';
import * as app from '../actions/appActions.ios.js';
import getApp from '../reducers/appReducers.ios.js';
import camera from './Camera.ios.js';

class App extends Component {
  constructor(props) {
    super(props);
  }

  border(color) {
    return {
      borderColor: color,
      borderWidth: 5,
    }
  }

  changeNavigationHome() {
    this.props.navigator.push({
      id: 'Home'
    })
  }

  changeNavigationCamera() {
    this.props.navigator.push({
      component: camera
    })
  }

  render() {
    console.log('props', this.props.actions.default)
    const {state, actions} = this.props;
    if (state.showSearchBar) {
      return (
        <View style={[styles.container, this.border('yellow')]}>
          <View style={[styles.navigation, this.border('pink')]} >
            <Nav
              changeNavigationCamera={this.changeNavigationCamera.bind(this)}
            />
          </View>
          <View style={[styles.app, this.border('black')]} >
            <Text style={styles.welcome}>
              Welcome to Snack Time!
            </Text>
          </View>
          <View style={[styles.searchBarPictureFrame, this.border('red')]} >
            <SearchBar navigator={this.props.navigator}/>
          </View>
          <TouchableHighlight style={[styles.buttonView]} onPress={this.changeNavigationCamera.bind(this)}>
            <Image style={[styles.takePicture, this.border('green')]} source={{uri: 'https://s3.amazonaws.com/features.ifttt.com/newsletter_images/2015_February/camera512x512+(1).png'}}/>
          </TouchableHighlight>
        </View>
      );
    } else {
      return (
        <View style={[styles.container, this.border('yellow')]}>
          <View style={[styles.navigation, this.border('pink')]} >
            <Nav
              changeNavigationCamera={this.changeNavigationCamera.bind(this)}
            />
          </View>
          <View style={[styles.app, this.border('black')]} >
            <Text style={styles.welcome}>
              Welcome to Snack Time!
            </Text>
          </View>
          <TouchableHighlight style={[styles.searchBarPictureFrame, this.border('red')]} onPress={actions.default} >
            <Image style={styles.searchBarPicture} source={{uri: 'https://d30y9cdsu7xlg0.cloudfront.net/png/449337-200.png'}} />
          </TouchableHighlight>
          <TouchableHighlight style={[styles.buttonView]} onPress={this.changeNavigationCamera.bind(this)}>
            <Image style={[styles.takePicture, this.border('green')]} source={{uri: 'https://s3.amazonaws.com/features.ifttt.com/newsletter_images/2015_February/camera512x512+(1).png'}}/>
          </TouchableHighlight>
        </View>
      );
    }
  }
}

export default connect(state => ({
    state: state.app
  }),
  (dispatch) => ({
    actions: bindActionCreators(app, dispatch)
  })
)(App);
<|endoftext|>