<commit_before>// Copyright 2002-2015, University of Colorado Boulder

/**
 * Grunt configuration file for PhET projects.
 * Requires a package.json file containing project settings.
 * Tasks share information via global.phet, see individual tasks for details.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 * @author Jon Olson
 * @author Sam Reid
 * @author John Blanco
 */

// use process.env
/* jslint node: true */

// built-in node APIs
var assert = require( 'assert' );
var fs = require( 'fs' );

// 3rd-party packages
/* jshint -W079 */
var _ = require( '../../../sherpa/lib/lodash-2.4.1.min' ); // allow _ to be redefined, contrary to jshintOptions.js
/* jshint +W079 */

// PhET custom grunt tasks
var afterRequirejsBuild = require( '../../../chipper/js/grunt/afterRequirejsBuild' );
var beforeRequirejsBuild = require( '../../../chipper/js/grunt/beforeRequirejsBuild' );
var checkoutShas = require( '../../../chipper/js/grunt/checkoutShas' );
var createSim = require( '../../../chipper/js/grunt/createSim' );
var deploySimulation = require( '../../../chipper/js/grunt/deploySimulation' );
var deployDev = require( '../../../chipper/js/grunt/deployDev' );
var generateREADME = require( '../../../chipper/js/grunt/generateREADME' );
var generateThumbnails = require( '../../../chipper/js/grunt/generateThumbnails' );
var reportMedia = require( '../../../chipper/js/grunt/reportMedia' );
var reportThirdParty = require( '../../../chipper/js/grunt/reportThirdParty' );

//TODO look at why this is necessary
/*
 * In Node, global is the global namespace object.
 * Register fs as a global so it can be accessed through the requirejs build system. Text.js plugin
 * may have a superior way to handle this but I (SR) couldn't get it working after a small amount of effort.
 */
global.fs = fs;

module.exports = function( grunt ) {
  'use strict';

  // for sharing global information
  global.phet = global.phet || {};
  global.phet.chipper = global.phet.chipper || {};

  // for situations where we can't pass the grunt instance as a function argument
  global.phet.chipper.grunt = grunt;

  var FALLBACK_LOCAL = 'en';

  // read the preferences file
  var PREFERENCES_FILE = process.env.HOME + '/.phet/build-local.json';
  var preferences = {};
  if ( fs.existsSync( PREFERENCES_FILE ) ) {
    preferences = grunt.file.readJSON( PREFERENCES_FILE );
  }

  /**
   * Gets the name of brand to use, which determines which logo to show in the navbar as well as what options
   * to show in the PhET menu and what text to show in the About dialog.
   * See also the requirejs-time version of this function (which uses query-parameters) in initialize-globals.js
   * See https://github.com/phetsims/brand/issues/11
   * @returns {string}
   */
  global.phet.chipper.brand = grunt.option( 'brand' ) || preferences.brand || 'adapted-from-phet';
  assert( fs.existsSync( '../brand/' + global.phet.chipper.brand ), 'no such brand: ' + global.phet.chipper.brand );

  // Read package.json, verify that it contains properties required by all PhET repositories
  assert( fs.existsSync( 'package.json' ), 'repository must have a package.json' );
  var pkg = grunt.file.readJSON( 'package.json' );
  assert( pkg.name, 'name missing from package.json' );
  assert( pkg.version, 'version missing from package.json' );
  assert( pkg.license, 'license missing from package.json' );

  // For sims, read common phetLibs from chipper/build.json. We'll do this here since several grunt tasks (including some utilities) need phetLibs.
  assert( fs.existsSync( '../chipper/build.json' ), 'missing build.json' );
  var buildInfo = grunt.file.readJSON( '../chipper/build.json' );

  // phetLibs
  pkg.phet = pkg.phet || {};
  pkg.phet.phetLibs = pkg.phet.phetLibs || [];
  pkg.phet.phetLibs.push( pkg.name ); // add the repo that's being built
  pkg.phet.phetLibs = pkg.phet.phetLibs.concat( buildInfo.common.phetLibs ); // add phetLibs from build.json
  pkg.phet.phetLibs = _.uniq( pkg.phet.phetLibs.sort() ); // sort and remove duplicates
  grunt.log.debug( 'pkg.phet.phetLibs = ' + pkg.phet.phetLibs );

  // TODO: As a temporary means of keeping track of "together" versions, replace "-dev" with "-together" in the version
  // string. This approach has a lot of problems and should be replaced as soon as we work out a more all encompassing
  // way of tracking together-enhanced versions.  See https://github.com/phetsims/special-ops/issues/3 for more info.
  if ( global.phet.chipper.brand === 'phet-io' && pkg.version.indexOf( '-dev' ) > -1 ) {
    pkg.version = pkg.version.replace( '-dev', '-together' );
  }

  var globalDefs = {
    // global assertions
    assert: false,
    assertSlow: false,
    // scenery logging
    sceneryLog: false,
    sceneryLayerLog: false,
    sceneryEventLog: false,
    sceneryAccessibilityLog: false,
    phetAllocation: false
  };

  // Enumerate the list of files to be linted for the jshint:allFiles task
  var allFilesToLint = _.map( pkg.phet.phetLibs, function( repo ) {
    return '../' + repo + '/js/**/*.js';
  } );

  // The brand repo has a different structure, so add it explicitly
  allFilesToLint.push( '../brand/*/js/**/*.js' );

  // Don't try to lint the svgPath.js.  It was automatically generated and doesn't match convention
  allFilesToLint.push( '!../kite/js/parser/svgPath.js' );

  // Identify the repo files to lint for the single repo.
  // The brand repo is a special case since it has nested subdirectories instead of a top level js/ directory
  var repoFilesToLint = ( pkg.name === 'brand' ) ? [ '*/js/**/*.js' ] : [ 'js/**/*.js' ];

  grunt.initConfig( {
    /*
     * Read in the project settings from the package.json file into the pkg property.
     * This allows us to refer to project settings from within this config file.
     */
    pkg: pkg,

    // configure the RequireJS plugin
    requirejs: {

      // builds the minified script
      build: {
        options: {
          almond: true,
          mainConfigFile: 'js/<%= pkg.name %>-config.js',
          out: 'build/<%= pkg.name %>.min.js',
          name: '<%= pkg.name %>-config',

          // Minification strategy.  Put this to none if you want to debug a non-minified but compiled version
          optimize: 'uglify2',
          wrap: true,
//            generateSourceMaps: true, //#42 commented out this line until source maps are fixed
          preserveLicenseComments: false,
          uglify2: {
            output: {
              inline_script: true // escape </script
            },
            compress: {
              global_defs: globalDefs,
              dead_code: true
            }
          },

          //stub out the plugins so their source code won't be included in the minified file
          stubModules: [ 'string', 'audio', 'image' ]
        }
      }
    },

    // configure the JSHint plugin
    jshint: {

      // source files that are specific to this repository
      repoFiles: repoFilesToLint,

      // All source files for this repository (repository-specific and dependencies).
      // Excludes kite/js/parser/svgPath.js, which is auto-generated.
      allFiles: allFilesToLint,

      // reference external JSHint options in jshintOptions.js
      options: require( './jshintOptions' )
    }
  } );

  //---------------------------------------------------------------------------------------------------------------
  // Primary tasks
  //---------------------------------------------------------------------------------------------------------------

  // Default task ('grunt')

  grunt.registerTask( 'default', 'Builds the English HTML', [ 'build' ] );

  // Add the linting step as an pre-build step.  Can be skipped with --lint=false
  var optionalTasks = [];
  if ( grunt.option( 'lint' ) === false ) {
  }
  else {
    optionalTasks.push( 'lint-all' );
  }

  grunt.registerTask( 'build',
    'Builds the simulation:\n' +
    'with no options, builds HTML for English only\n' +
    '--locales=* : all locales in strings/ directory\n' +
    '--locales=fr : French\n' +
    '--locales=ar,fr,es : Arabic, French and Spanish (comma separated locales)\n' +
    '--localesRepo=$repo : all locales in another repository\'s strings/ directory, ignored if --locales is present\n' +
    '--lint=false : skip the lint sub-task',
    optionalTasks.concat( [
      'clean',
      'before-requirejs-build',
      'requirejs:build',
      'after-requirejs-build' ] )
  );

  grunt.registerTask( 'deploy-production',
    'Deploy a simulation. Should be run AFTER grunt build since it uses the shas from dependencies.json in the build directory.\n' +
    'with no options, deploys to phet-dev (since this is still being tested)\n' +
    '--devDeploy : deploys to spot',
    function() {
      deploySimulation( grunt, grunt.option( 'devDeploy' ) );
    }
  );

  grunt.registerTask( 'deploy-dev',
    'Deploy a dev version to spot, or optionally to the server in your preferences file\n' +
    '--buildServer : build the sim with the build server if true\n' +
    '--mkdir : set to true to create the sim dir and .htaccess file before copying the version directory\n' +
    '--test : set to true to disable commit and push, and SCP to a test directory on spot',
    function() {
      if ( grunt.option( 'buildServer' ) ) {
        deploySimulation( grunt, 'simian' );
      }
      else {
        deployDev( grunt, grunt.option( 'mkdir' ), grunt.option( 'test' ) );
      }
    }
  );

  grunt.registerTask( 'lint', 'lint js files that are specific to this repository', [ 'jshint:repoFiles' ] );

  grunt.registerTask( 'lint-all', 'lint all js files that are required to build this repository', [ 'jshint:allFiles' ] );

  grunt.registerTask( 'clean',
    'Erases the build/ directory and all its contents, and recreates the build/ directory',
    function() {
      if ( fs.existsSync( 'build' ) ) {
        grunt.file.delete( 'build' );
      }
      grunt.file.mkdir( 'build' );
    } );

  grunt.registerTask( 'before-requirejs-build', '(internal use only) Do things before the requirejs:build task',
    function() {
      beforeRequirejsBuild( grunt, pkg, FALLBACK_LOCAL );
    } );

  grunt.registerTask( 'after-requirejs-build',
    '(internal use only) Do things after the requirejs:build task',
    function() {
      afterRequirejsBuild( grunt, pkg, FALLBACK_LOCAL );
    } );

  //---------------------------------------------------------------------------------------------------------------
  // Utility tasks
  //---------------------------------------------------------------------------------------------------------------

  grunt.registerTask( 'checkout-shas',
    'Check out shas for a project, as specified in dependencies.json',
    function() {
      var buildServer = grunt.option( 'buildServer' ) ? true : false;
      checkoutShas( grunt, pkg.name, false, buildServer );
    } );

  grunt.registerTask( 'checkout-master',
    'Check out master branch for all dependencies, as specified in dependencies.json',
    function() {
      checkoutShas( grunt, pkg.name, true );
    } );

  grunt.registerTask( 'create-sim',
    'Creates a sim based on the simula-rasa template.\n' +
    '--name="string" : the repository name\n' +
    '--author="string" : the author name\n' +
    '--title="string" : (optional) the simulation title\n' +
    '--clean=true : (optional) deletes the repository directory if it exists',
    function() {
      createSim( grunt, grunt.option( 'name' ), grunt.option( 'author' ), grunt.option( 'title' ), grunt.option( 'clean' ) );
    } );

  // See reportMedia.js
  grunt.registerTask( 'report-media', '(project-wide) Report on license.json files throughout all working copies. ' +
                                      'Reports any media (such as images or audio) files that have any of the following problems:\n' +
                                      '(1) incompatible-license (resource license not approved)\n' +
                                      '(2) not-annotated (license.json missing or entry missing from license.json)\n' +
                                      '(3) missing-file (entry in the license.json but not on the file system)',
    function() {
      reportMedia( grunt );
    } );

  // see reportThirdParty.js
  grunt.registerTask( 'report-third-party', 'This task is used to create a report of third-party resources (code, ' +
                                            'images, audio, etc) used in a set of PhET simulations by reading the ' +
                                            'license information in built HTML files.\n' +
                                            '--input (required argument) the path to the directory containing HTML ' +
                                            'files which will be reported on.\n' +
                                            '--output (required argument) the path to a file where the report should be ' +
                                            'written. The file is in markdown syntax and the *.md suffix is ' +
                                            'recommended. This will silently overwrite an existing file, if there is ' +
                                            'one.\n' +
                                            '--active-runnables (optional flag, boolean) If this flag is ' +
                                            'supplied, the task iterates over the active-runnables and copies each ' +
                                            'built HTML file into the directory specified with --input before ' +
                                            'running the report. If any HTML files are missing, the report will fail. ' +
                                            'Before using this flag, the developer should run `grunt-all.sh` to make ' +
                                            'sure all of the HTML files are up-to-date. The directory is neither ' +
                                            'automatically created nor automatically cleaned, this is the ' +
                                            'responsibility of the developer. (Note that if you fail to manually ' +
                                            'clean the directory, you may end up with stale HTML files).',
    function() {

      // The input and output arguments are required but we chose not to use the grunt required argument syntax (a colon)
      // since it would cause problems for Windows developers who have colons in the pathnames.
      var input = grunt.option( 'input' );
      var output = grunt.option( 'output' );
      assert( input, 'The input path must be specified' );
      assert( output, 'The output path must be specified' );

      var activeRunnables = !!grunt.option( 'active-runnables' );
      reportThirdParty( grunt, input, output, activeRunnables );
    } );

  grunt.registerTask( 'published-README',
    'Generates README.md file for a published simulation.',
    function() {
      assert( pkg.phet.simTitleStringKey, 'simTitleStringKey missing from package.json' );
      generateREADME( grunt, pkg.name, pkg.phet.phetLibs, pkg.phet.simTitleStringKey, true /* published */ );
    } );

  grunt.registerTask( 'unpublished-README',
    'Generates README.md file for an unpublished simulation.',
    function() {
      assert( pkg.phet.simTitleStringKey, 'phet.simTitleStringKey missing from package.json' );
      generateREADME( grunt, pkg.name, pkg.phet.phetLibs, pkg.phet.simTitleStringKey, false /* published */ );
    } );

  grunt.registerTask( 'generate-thumbnails', 'Generate 128x84 and 600x394 thumbnails to be used on the website.',
    function() {
      var finished = _.after( 2, grunt.task.current.async() );
      generateThumbnails( grunt, pkg.name, 128, 84, finished );
      generateThumbnails( grunt, pkg.name, 600, 394, finished );
    } );

  /*
   * Load tasks from grunt plugins that have been installed locally using npm.
   * Put these in package.json and run 'npm install' before running grunt.
   */
  grunt.loadNpmTasks( 'grunt-requirejs' );
  grunt.loadNpmTasks( 'grunt-contrib-jshint' );
};
<commit_msg>#272 add phetLibs for phet-io<commit_after>// Copyright 2002-2015, University of Colorado Boulder

/**
 * Grunt configuration file for PhET projects.
 * Requires a package.json file containing project settings.
 * Tasks share information via global.phet, see individual tasks for details.
 *
 * @author Chris Malley (PixelZoom, Inc.)
 * @author Jon Olson
 * @author Sam Reid
 * @author John Blanco
 */

// use process.env
/* jslint node: true */

// built-in node APIs
var assert = require( 'assert' );
var fs = require( 'fs' );

// 3rd-party packages
/* jshint -W079 */
var _ = require( '../../../sherpa/lib/lodash-2.4.1.min' ); // allow _ to be redefined, contrary to jshintOptions.js
/* jshint +W079 */

// PhET custom grunt tasks
var afterRequirejsBuild = require( '../../../chipper/js/grunt/afterRequirejsBuild' );
var beforeRequirejsBuild = require( '../../../chipper/js/grunt/beforeRequirejsBuild' );
var checkoutShas = require( '../../../chipper/js/grunt/checkoutShas' );
var createSim = require( '../../../chipper/js/grunt/createSim' );
var deploySimulation = require( '../../../chipper/js/grunt/deploySimulation' );
var deployDev = require( '../../../chipper/js/grunt/deployDev' );
var generateREADME = require( '../../../chipper/js/grunt/generateREADME' );
var generateThumbnails = require( '../../../chipper/js/grunt/generateThumbnails' );
var reportMedia = require( '../../../chipper/js/grunt/reportMedia' );
var reportThirdParty = require( '../../../chipper/js/grunt/reportThirdParty' );

//TODO look at why this is necessary
/*
 * In Node, global is the global namespace object.
 * Register fs as a global so it can be accessed through the requirejs build system. Text.js plugin
 * may have a superior way to handle this but I (SR) couldn't get it working after a small amount of effort.
 */
global.fs = fs;

module.exports = function( grunt ) {
  'use strict';

  // for sharing global information
  global.phet = global.phet || {};
  global.phet.chipper = global.phet.chipper || {};

  // for situations where we can't pass the grunt instance as a function argument
  global.phet.chipper.grunt = grunt;

  var FALLBACK_LOCAL = 'en';

  // read the preferences file
  var PREFERENCES_FILE = process.env.HOME + '/.phet/build-local.json';
  var preferences = {};
  if ( fs.existsSync( PREFERENCES_FILE ) ) {
    preferences = grunt.file.readJSON( PREFERENCES_FILE );
  }

  /**
   * Gets the name of brand to use, which determines which logo to show in the navbar as well as what options
   * to show in the PhET menu and what text to show in the About dialog.
   * See also the requirejs-time version of this function (which uses query-parameters) in initialize-globals.js
   * See https://github.com/phetsims/brand/issues/11
   * @returns {string}
   */
  global.phet.chipper.brand = grunt.option( 'brand' ) || preferences.brand || 'adapted-from-phet';
  assert( fs.existsSync( '../brand/' + global.phet.chipper.brand ), 'no such brand: ' + global.phet.chipper.brand );

  // Read package.json, verify that it contains properties required by all PhET repositories
  assert( fs.existsSync( 'package.json' ), 'repository must have a package.json' );
  var pkg = grunt.file.readJSON( 'package.json' );
  assert( pkg.name, 'name missing from package.json' );
  assert( pkg.version, 'version missing from package.json' );
  assert( pkg.license, 'license missing from package.json' );

  // For sims, read common phetLibs from chipper/build.json. We'll do this here since several grunt tasks (including some utilities) need phetLibs.
  assert( fs.existsSync( '../chipper/build.json' ), 'missing build.json' );
  var buildInfo = grunt.file.readJSON( '../chipper/build.json' );

  // phetLibs
  pkg.phet = pkg.phet || {};
  pkg.phet.phetLibs = pkg.phet.phetLibs || [];
  pkg.phet.phetLibs.push( pkg.name ); // add the repo that's being built
  pkg.phet.phetLibs = pkg.phet.phetLibs.concat( buildInfo.common.phetLibs ); // add phetLibs from build.json
  if ( global.phet.chipper.brand === 'phet-io' && buildInfo[ 'phet-io' ] && buildInfo[ 'phet-io' ].phetLibs ) {
    pkg.phet.phetLibs = pkg.phet.phetLibs.concat( buildInfo[ 'phet-io' ].phetLibs ); // add phetLibs for phet-io brand
  }
  pkg.phet.phetLibs = _.uniq( pkg.phet.phetLibs.sort() ); // sort and remove duplicates
  grunt.log.debug( 'pkg.phet.phetLibs = ' + pkg.phet.phetLibs );

  // TODO: As a temporary means of keeping track of "together" versions, replace "-dev" with "-together" in the version
  // string. This approach has a lot of problems and should be replaced as soon as we work out a more all encompassing
  // way of tracking together-enhanced versions.  See https://github.com/phetsims/special-ops/issues/3 for more info.
  if ( global.phet.chipper.brand === 'phet-io' && pkg.version.indexOf( '-dev' ) > -1 ) {
    pkg.version = pkg.version.replace( '-dev', '-together' );
  }

  var globalDefs = {
    // global assertions
    assert: false,
    assertSlow: false,
    // scenery logging
    sceneryLog: false,
    sceneryLayerLog: false,
    sceneryEventLog: false,
    sceneryAccessibilityLog: false,
    phetAllocation: false
  };

  // Enumerate the list of files to be linted for the jshint:allFiles task
  var allFilesToLint = _.map( pkg.phet.phetLibs, function( repo ) {
    return '../' + repo + '/js/**/*.js';
  } );

  // The brand repo has a different structure, so add it explicitly
  allFilesToLint.push( '../brand/*/js/**/*.js' );

  // Don't try to lint the svgPath.js.  It was automatically generated and doesn't match convention
  allFilesToLint.push( '!../kite/js/parser/svgPath.js' );

  // Identify the repo files to lint for the single repo.
  // The brand repo is a special case since it has nested subdirectories instead of a top level js/ directory
  var repoFilesToLint = ( pkg.name === 'brand' ) ? [ '*/js/**/*.js' ] : [ 'js/**/*.js' ];

  grunt.initConfig( {
    /*
     * Read in the project settings from the package.json file into the pkg property.
     * This allows us to refer to project settings from within this config file.
     */
    pkg: pkg,

    // configure the RequireJS plugin
    requirejs: {

      // builds the minified script
      build: {
        options: {
          almond: true,
          mainConfigFile: 'js/<%= pkg.name %>-config.js',
          out: 'build/<%= pkg.name %>.min.js',
          name: '<%= pkg.name %>-config',

          // Minification strategy.  Put this to none if you want to debug a non-minified but compiled version
          optimize: 'uglify2',
          wrap: true,
//            generateSourceMaps: true, //#42 commented out this line until source maps are fixed
          preserveLicenseComments: false,
          uglify2: {
            output: {
              inline_script: true // escape </script
            },
            compress: {
              global_defs: globalDefs,
              dead_code: true
            }
          },

          //stub out the plugins so their source code won't be included in the minified file
          stubModules: [ 'string', 'audio', 'image' ]
        }
      }
    },

    // configure the JSHint plugin
    jshint: {

      // source files that are specific to this repository
      repoFiles: repoFilesToLint,

      // All source files for this repository (repository-specific and dependencies).
      // Excludes kite/js/parser/svgPath.js, which is auto-generated.
      allFiles: allFilesToLint,

      // reference external JSHint options in jshintOptions.js
      options: require( './jshintOptions' )
    }
  } );

  //---------------------------------------------------------------------------------------------------------------
  // Primary tasks
  //---------------------------------------------------------------------------------------------------------------

  // Default task ('grunt')

  grunt.registerTask( 'default', 'Builds the English HTML', [ 'build' ] );

  // Add the linting step as an pre-build step.  Can be skipped with --lint=false
  var optionalTasks = [];
  if ( grunt.option( 'lint' ) === false ) {
  }
  else {
    optionalTasks.push( 'lint-all' );
  }

  grunt.registerTask( 'build',
    'Builds the simulation:\n' +
    'with no options, builds HTML for English only\n' +
    '--locales=* : all locales in strings/ directory\n' +
    '--locales=fr : French\n' +
    '--locales=ar,fr,es : Arabic, French and Spanish (comma separated locales)\n' +
    '--localesRepo=$repo : all locales in another repository\'s strings/ directory, ignored if --locales is present\n' +
    '--lint=false : skip the lint sub-task',
    optionalTasks.concat( [
      'clean',
      'before-requirejs-build',
      'requirejs:build',
      'after-requirejs-build' ] )
  );

  grunt.registerTask( 'deploy-production',
    'Deploy a simulation. Should be run AFTER grunt build since it uses the shas from dependencies.json in the build directory.\n' +
    'with no options, deploys to phet-dev (since this is still being tested)\n' +
    '--devDeploy : deploys to spot',
    function() {
      deploySimulation( grunt, grunt.option( 'devDeploy' ) );
    }
  );

  grunt.registerTask( 'deploy-dev',
    'Deploy a dev version to spot, or optionally to the server in your preferences file\n' +
    '--buildServer : build the sim with the build server if true\n' +
    '--mkdir : set to true to create the sim dir and .htaccess file before copying the version directory\n' +
    '--test : set to true to disable commit and push, and SCP to a test directory on spot',
    function() {
      if ( grunt.option( 'buildServer' ) ) {
        deploySimulation( grunt, 'simian' );
      }
      else {
        deployDev( grunt, grunt.option( 'mkdir' ), grunt.option( 'test' ) );
      }
    }
  );

  grunt.registerTask( 'lint', 'lint js files that are specific to this repository', [ 'jshint:repoFiles' ] );

  grunt.registerTask( 'lint-all', 'lint all js files that are required to build this repository', [ 'jshint:allFiles' ] );

  grunt.registerTask( 'clean',
    'Erases the build/ directory and all its contents, and recreates the build/ directory',
    function() {
      if ( fs.existsSync( 'build' ) ) {
        grunt.file.delete( 'build' );
      }
      grunt.file.mkdir( 'build' );
    } );

  grunt.registerTask( 'before-requirejs-build', '(internal use only) Do things before the requirejs:build task',
    function() {
      beforeRequirejsBuild( grunt, pkg, FALLBACK_LOCAL );
    } );

  grunt.registerTask( 'after-requirejs-build',
    '(internal use only) Do things after the requirejs:build task',
    function() {
      afterRequirejsBuild( grunt, pkg, FALLBACK_LOCAL );
    } );

  //---------------------------------------------------------------------------------------------------------------
  // Utility tasks
  //---------------------------------------------------------------------------------------------------------------

  grunt.registerTask( 'checkout-shas',
    'Check out shas for a project, as specified in dependencies.json',
    function() {
      var buildServer = grunt.option( 'buildServer' ) ? true : false;
      checkoutShas( grunt, pkg.name, false, buildServer );
    } );

  grunt.registerTask( 'checkout-master',
    'Check out master branch for all dependencies, as specified in dependencies.json',
    function() {
      checkoutShas( grunt, pkg.name, true );
    } );

  grunt.registerTask( 'create-sim',
    'Creates a sim based on the simula-rasa template.\n' +
    '--name="string" : the repository name\n' +
    '--author="string" : the author name\n' +
    '--title="string" : (optional) the simulation title\n' +
    '--clean=true : (optional) deletes the repository directory if it exists',
    function() {
      createSim( grunt, grunt.option( 'name' ), grunt.option( 'author' ), grunt.option( 'title' ), grunt.option( 'clean' ) );
    } );

  // See reportMedia.js
  grunt.registerTask( 'report-media', '(project-wide) Report on license.json files throughout all working copies. ' +
                                      'Reports any media (such as images or audio) files that have any of the following problems:\n' +
                                      '(1) incompatible-license (resource license not approved)\n' +
                                      '(2) not-annotated (license.json missing or entry missing from license.json)\n' +
                                      '(3) missing-file (entry in the license.json but not on the file system)',
    function() {
      reportMedia( grunt );
    } );

  // see reportThirdParty.js
  grunt.registerTask( 'report-third-party', 'This task is used to create a report of third-party resources (code, ' +
                                            'images, audio, etc) used in a set of PhET simulations by reading the ' +
                                            'license information in built HTML files.\n' +
                                            '--input (required argument) the path to the directory containing HTML ' +
                                            'files which will be reported on.\n' +
                                            '--output (required argument) the path to a file where the report should be ' +
                                            'written. The file is in markdown syntax and the *.md suffix is ' +
                                            'recommended. This will silently overwrite an existing file, if there is ' +
                                            'one.\n' +
                                            '--active-runnables (optional flag, boolean) If this flag is ' +
                                            'supplied, the task iterates over the active-runnables and copies each ' +
                                            'built HTML file into the directory specified with --input before ' +
                                            'running the report. If any HTML files are missing, the report will fail. ' +
                                            'Before using this flag, the developer should run `grunt-all.sh` to make ' +
                                            'sure all of the HTML files are up-to-date. The directory is neither ' +
                                            'automatically created nor automatically cleaned, this is the ' +
                                            'responsibility of the developer. (Note that if you fail to manually ' +
                                            'clean the directory, you may end up with stale HTML files).',
    function() {

      // The input and output arguments are required but we chose not to use the grunt required argument syntax (a colon)
      // since it would cause problems for Windows developers who have colons in the pathnames.
      var input = grunt.option( 'input' );
      var output = grunt.option( 'output' );
      assert( input, 'The input path must be specified' );
      assert( output, 'The output path must be specified' );

      var activeRunnables = !!grunt.option( 'active-runnables' );
      reportThirdParty( grunt, input, output, activeRunnables );
    } );

  grunt.registerTask( 'published-README',
    'Generates README.md file for a published simulation.',
    function() {
      assert( pkg.phet.simTitleStringKey, 'simTitleStringKey missing from package.json' );
      generateREADME( grunt, pkg.name, pkg.phet.phetLibs, pkg.phet.simTitleStringKey, true /* published */ );
    } );

  grunt.registerTask( 'unpublished-README',
    'Generates README.md file for an unpublished simulation.',
    function() {
      assert( pkg.phet.simTitleStringKey, 'phet.simTitleStringKey missing from package.json' );
      generateREADME( grunt, pkg.name, pkg.phet.phetLibs, pkg.phet.simTitleStringKey, false /* published */ );
    } );

  grunt.registerTask( 'generate-thumbnails', 'Generate 128x84 and 600x394 thumbnails to be used on the website.',
    function() {
      var finished = _.after( 2, grunt.task.current.async() );
      generateThumbnails( grunt, pkg.name, 128, 84, finished );
      generateThumbnails( grunt, pkg.name, 600, 394, finished );
    } );

  /*
   * Load tasks from grunt plugins that have been installed locally using npm.
   * Put these in package.json and run 'npm install' before running grunt.
   */
  grunt.loadNpmTasks( 'grunt-requirejs' );
  grunt.loadNpmTasks( 'grunt-contrib-jshint' );
};
<|endoftext|>


<commit_before>/*!
  jQuery #hashtags v0.5.0
	(c) 2013 Simon Nussbaumer
	updated: 2016-25-10
	license: GNU LESSER GENERAL PUBLIC LICENSE
*/
(function($) {
	$.fn.hashtags = function() {
		$(this).wrap('

').unwrap().before('

').wrap('

');
		$(this).addClass("theSelector");
		autosize($(this));
		$(this).on("keyup", function() {
			var str = $(this).val();
			$(this).parent().parent().find(".highlighter").css("width",$(this).css("width"));
			str = str.replace(/\n/g, '
');
			if(!str.match(/(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?#([a-zA-Z0-9]+)/g) && !str.match(/(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?@([a-zA-Z0-9]+)/g) && !str.match(/(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?#([\u0600-\u06FF]+)/g) && !str.match(/(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?@([\u0600-\u06FF]+)/g)) {
                if(!str.match(/#(([_a-zA-Z0-9]+)|([\u0600-\u06FF]+))#/g)) { //arabic support
					str = str.replace(/#(([_a-zA-Z0-9]+)|([\u0600-\u06FF]+))/g,'#$1');
				}else{
					str = str.replace(/#(([_a-zA-Z0-9]+)|([\u0600-\u06FF]+))#(([_a-zA-Z0-9]+)|([\u0600-\u06FF]+))/g,'#$1');
				}
				if(!str.match(/@(([a-zA-Z0-9]+)|([\u0600-\u06FF]+))@/g)) {
					str = str.replace(/@(([a-zA-Z0-9]+)|([\u0600-\u06FF]+))/g,'@$1');
				}else{
					str = str.replace(/@(([a-zA-Z0-9]+)|([\u0600-\u06FF]+))@(([a-zA-Z0-9]+)|([\u0600-\u06FF]+))/g,'@$1');
				}
			}
			$(this).parent().parent().find(".highlighter").html(str);
		});
		$(this).parent().prev().on('click', function() {
			$(this).parent().find(".theSelector").focus();
		});

  	};
})(jQuery);
<commit_msg>CJK support<commit_after>/*!
  jQuery #hashtags v0.5.0
	(c) 2013 Simon Nussbaumer
	updated: 2016-25-10
	license: GNU LESSER GENERAL PUBLIC LICENSE
*/
(function($) {
	$.fn.hashtags = function() {
		$(this).wrap('

').unwrap().before('

').wrap('

');
		$(this).addClass("theSelector");
		autosize($(this));
		$(this).on("keyup", function() {
			var str = $(this).val();
			$(this).parent().parent().find(".highlighter").css("width",$(this).css("width"));
			str = str.replace(/\n/g, '
');
			if(!str.match(/(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?#([a-zA-Z0-9]+)/g) && !str.match(/(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?@([a-zA-Z0-9]+)/g) && !str.match(/(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?#([\u0600-\u06FF]+)/g) && !str.match(/(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?@([\u0600-\u06FF]+)/g)) {
                if(!str.match(/#(([_a-zA-Z0-9]+)|([\u0600-\u06FF]+)|([ㄱ-ㅎㅏ-ㅣ가-힣]+)|([ぁ-んァ-ン]+)|([\x4E00-\x9FA5]|[\xF900-\xFA2D]+))#/g)) { //arabic support, CJK support
					str = str.replace(/#(([_a-zA-Z0-9]+)|([\u0600-\u06FF]+)|([ㄱ-ㅎㅏ-ㅣ가-힣]+)|([ぁ-んァ-ン]+)|([\x4E00-\x9FA5]|[\xF900-\xFA2D]+))/g,'#$1');
				}else{
					str = str.replace(/#(([_a-zA-Z0-9]+)|([\u0600-\u06FF]+)|([ㄱ-ㅎㅏ-ㅣ가-힣]+)|([ぁ-んァ-ン]+)|([\x4E00-\x9FA5]|[\xF900-\xFA2D]+))#(([_a-zA-Z0-9]+)|([\u0600-\u06FF]+)|([ㄱ-ㅎㅏ-ㅣ가-힣]+)|([ぁ-んァ-ン]+)|([\x4E00-\x9FA5]|[\xF900-\xFA2D]+))/g,'#$1');
				}
				if(!str.match(/@(([a-zA-Z0-9]+)|([\u0600-\u06FF]+)|([ㄱ-ㅎㅏ-ㅣ가-힣]+)|([ぁ-んァ-ン]+)|([\x4E00-\x9FA5]|[\xF900-\xFA2D]+))@/g)) {
					str = str.replace(/@(([a-zA-Z0-9]+)|([\u0600-\u06FF]+)|([ㄱ-ㅎㅏ-ㅣ가-힣]+)|([ぁ-んァ-ン]+)|([\x4E00-\x9FA5]|[\xF900-\xFA2D]+))/g,'@$1');
				}else{
					str = str.replace(/@(([a-zA-Z0-9]+)|([\u0600-\u06FF]+)|([ㄱ-ㅎㅏ-ㅣ가-힣]+)|([ぁ-んァ-ン]+)|([\x4E00-\x9FA5]|[\xF900-\xFA2D]+))@(([a-zA-Z0-9]+)|([\u0600-\u06FF]+)|([ㄱ-ㅎㅏ-ㅣ가-힣]+)|([ぁ-んァ-ン]+)|([\x4E00-\x9FA5]|[\xF900-\xFA2D]+))/g,'@$1');
				}
			}
			$(this).parent().parent().find(".highlighter").html(str);
		});
		$(this).parent().prev().on('click', function() {
			$(this).parent().find(".theSelector").focus();
		});

  	};
})(jQuery);
<|endoftext|>


<commit_before>/**
 * Created by kupreychik_af on 05.03.14.
 */

var _ = require('underscore');

// helper
var createClass = function(protoProps, staticProps) {
    var child = function() {
        this.initialize && this.initialize.apply(this, arguments);
    };

    _.isObject(protoProps)  && _.extend(child.prototype, protoProps);
    _.isObject(staticProps) && _.extend(child, staticProps);

    return child;
};

var sign = function(x) {
    return x == 0 ? 0 : Math.round(x/Math.abs(x));
};

var movementMap = [
    { "x" :  0, "y" :  0 }, // Idle
    { "x" :  0, "y" : -1 }, // N
    { "x" : +1, "y" : -1 }, // NE
    { "x" : +1, "y" :  0 }, // E
    { "x" : +1, "y" : +1 }, // SE
    { "x" :  0, "y" : +1 }, // S
    { "x" : -1, "y" : +1 }, // SW
    { "x" : -1, "y" :  0 }, // W
    { "x" : -1, "y" : -1 } // NW
];

var getMovement = function(toObj) {
    for (var dir =0; dir < movementMap.length; dir++) {
        var rel = movementMap[dir];
        if (sign(rel.x) == sign(toObj.x) && sign(rel.y) == sign(toObj.y)) {
            return dir;
        }
    }

    return 0;
};

// AGENT
var StraighOne = createClass({
    direction: 1,
    'introduce': function () {
        return {
            "name"   : "Straight One",
            "author" : "Andrey F. Kupreychik",
            "email"  : "foxel@quickfox.ru"
        };
    },

    'onNewTick': function (status) {
        this.status = status;
    },

    'decision': function () {
        // Scan surrounding cells (all directions) for food and make decision to eat it if found
        for (var i = 0; i < this.status.environment.objects.length; i++) {
            var obj = this.status.environment.objects[i];
            if (obj.class !== "food") {
                continue;
            }

            return {
                "action" : (Math.abs(obj.x) <= 1 && Math.abs(obj.y) <= 1) ? 4 : 1,
                "dir"    : getMovement(obj)
            };
        }
        return {
            "action" : 1,
            "dir"    : this.direction
        };
    },

    /**
     * Is called when agent performed wrong action or action is impossible
     *
     * @param notificationCode
     *
     * Notification codes:
     * 1 - decision error (wrong format or codes)
     * 21 - movement is impossible (can not move to terrain type)
     * 22 - movement is impossible (another agent is in the cell)
     * 41 - can't eat food (no food in the cell)
     * 42 - can't eat more food (your stomach is full)
     */
    'onNotification': function (notificationCode) {
        this.direction = Math.round(Math.random()*movementMap.length) || 1;
    }
});

exports.agentClass = StraighOne;
<commit_msg>Fixed incorrect decision<commit_after>/**
 * Created by kupreychik_af on 05.03.14.
 */

var _ = require('underscore');

// helper
var createClass = function(protoProps, staticProps) {
    var child = function() {
        this.initialize && this.initialize.apply(this, arguments);
    };

    _.isObject(protoProps)  && _.extend(child.prototype, protoProps);
    _.isObject(staticProps) && _.extend(child, staticProps);

    return child;
};

var sign = function(x) {
    return x == 0 ? 0 : Math.round(x/Math.abs(x));
};

var movementMap = [
    { "x" :  0, "y" :  0 }, // Idle
    { "x" :  0, "y" : -1 }, // N
    { "x" : +1, "y" : -1 }, // NE
    { "x" : +1, "y" :  0 }, // E
    { "x" : +1, "y" : +1 }, // SE
    { "x" :  0, "y" : +1 }, // S
    { "x" : -1, "y" : +1 }, // SW
    { "x" : -1, "y" :  0 }, // W
    { "x" : -1, "y" : -1 } // NW
];

var getMovement = function(toObj) {
    for (var dir =0; dir < movementMap.length; dir++) {
        var rel = movementMap[dir];
        if (sign(rel.x) == sign(toObj.x) && sign(rel.y) == sign(toObj.y)) {
            return dir;
        }
    }

    return 0;
};

// AGENT
var StraighOne = createClass({
    direction: 1,
    'introduce': function () {
        return {
            "name"   : "Straight One",
            "author" : "Andrey F. Kupreychik",
            "email"  : "foxel@quickfox.ru"
        };
    },

    'onNewTick': function (status) {
        this.status = status;
    },

    'decision': function () {
        // Scan surrounding cells (all directions) for food and make decision to eat it if found
        for (var i = 0; i < this.status.environment.objects.length; i++) {
            var obj = this.status.environment.objects[i];
            if (obj.class !== "food") {
                continue;
            }

            return {
                "action" : (Math.abs(obj.x) <= 1 && Math.abs(obj.y) <= 1) ? 4 : 1,
                "dir"    : getMovement(obj)
            };
        }
        return {
            "action" : 1,
            "dir"    : this.direction
        };
    },

    /**
     * Is called when agent performed wrong action or action is impossible
     *
     * @param notificationCode
     *
     * Notification codes:
     * 1 - decision error (wrong format or codes)
     * 21 - movement is impossible (can not move to terrain type)
     * 22 - movement is impossible (another agent is in the cell)
     * 41 - can't eat food (no food in the cell)
     * 42 - can't eat more food (your stomach is full)
     */
    'onNotification': function (notificationCode) {
        this.direction = Math.floor(Math.random()*movementMap.length) || 1;
    }
});

exports.agentClass = StraighOne;
<|endoftext|>


<commit_before>/* append the YouTube IFRAME API script */

var youtube = document.createElement('script');
youtube.type = "text/javascript";
youtube.src = "//www.youtube.com/iframe_api";

var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(youtube, s);

$(window).load(function () {
    var i = 0;

    var startButtons = document.getElementsByClassName('pq-slim-quote-picture', i);
    var mobileStartButtons = document.getElementsByClassName('mobile-start-button', i);
    var hiddenButton;
    var audioSourceMP3 = [];
    var YouTubeIDs = [];
    var timeBegins = [];
    var timeEnds = [];

    var playerWrappers = document.getElementsByClassName('player');

    var openPlayerWrapper = false;

    for (i = 0; i < startButtons.length; ++i) {
        startButtons[i].addEventListener("click", playMedia(i));
        mobileStartButtons[i].addEventListener("click", playMedia(i));
        audioSourceMP3[i] = startButtons[i].getAttribute('audioSourceMP3');
        YouTubeIDs[i] = startButtons[i].getAttribute('YouTubeID');
        timeBegins[i] = startButtons[i].getAttribute('timeBegins');
        timeEnds[i] = startButtons[i].getAttribute('timeEnds');
    }

    window.playerWrappers = playerWrappers;
    window.openPlayerWrapper = openPlayerWrapper;
    window.startButtons = startButtons;
    window.mobileStartButtons = mobileStartButtons;
    window.hiddenButton = hiddenButton;
    window.audioSourceMP3 = audioSourceMP3;
    window.YouTubeIDs = YouTubeIDs;
    window.timeBegins = timeBegins;
    window.timeEnds = timeEnds;

    function playMedia(i) {
        return function () {
            if (openPlayerWrapper != false) {
                var openPlayer = document.getElementById('open-player');
                openPlayer.parentNode.className = "player";
                $('#open-player').remove();
                $(hiddenButton).show();
                hiddenButton = startButtons[i];
                $(startButtons[i]).hide();
            } else {
                openPlayerWrapper = true;
                hiddenButton = startButtons[i];
                $(startButtons[i]).hide();
            }
            if (YouTubeIDs[i].length == 11) {
                onYouTubeIframeAPIReady();

                function onYouTubeIframeAPIReady() {
                    var YouTubeNode = document.createElement('div');
                    YouTubeNode.setAttribute('id', 'open-player');
                    playerWrappers[i].appendChild(YouTubeNode);
                    YouTubeNode.parentNode.className = YouTubeNode.parentNode.className + " youtube-player";

                    playerWrappers[i] = new YT.Player(YouTubeNode, {
                        playerVars: {'rel': 0, 'showinfo': 0, 'hidecontrols': 1 },
                        events: {
                          'onReady': onPlayerReady,
                          'onStateChange': onPlayerStateChange,
                        }
                    });
                }

                // When the player is ready, we load the video.
                // loadVideoById starts the video automatically.
                // cueVideoById does not start the video automatically.

                function onPlayerReady(event) {
                    event.target.loadVideoById({
                        videoId: YouTubeIDs[i],
                        startSeconds: timeBegins[i],
                        endSeconds: timeEnds[i]
                    });
                  };

                  var done = false;
                  function onPlayerStateChange(event) {
                      if (event.data == 0) {
                          done = true;
                          event.target.destroy();
                          $(hiddenButton).show();
                          $('#open-player').remove();
                      }
                  };

            } else {
                var audio = document.createElement('audio');
                audio.setAttribute('controls', '');
                audio.setAttribute('preload', 'metadata');
                audio.setAttribute('id', 'open-player');
                playerWrappers[i].appendChild(audio);

                var source = document.createElement('source');
                source.setAttribute('src', audioSourceMP3[i]);
                audio.appendChild(source);

                $(playerWrappers[i]).css('margin', '5px 0 0 0');

                audio.load();
                audio.onloadedmetadata = function() {
                    audio.play();
                };

                audio.addEventListener('timeupdate', function() {
                    if (Math.floor(audio.currentTime) == timeEnds[i]) {
                        audio.pause();
                        audio.currentTime = parseInt(Math.floor(timeEnds[i])) + 1;
                    } else if (audio.currentTime < 0) {
                        audio.currentTime = timeBegins[i];
                    }
                },false);
            }
        }
    }

});

/* Sample MP3: http://traffic.libsyn.com/joeroganexp/p523.mp3 */
/* Sample OGG: http://www.tuxradar.com/files/podcast/tuxradar_s06e02.ogg */<commit_msg>Try to fix iOS Safari<commit_after>/* append the YouTube IFRAME API script */

var youtube = document.createElement('script');
youtube.type = "text/javascript";
youtube.src = "//www.youtube.com/iframe_api";

var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(youtube, s);

$(window).load(function () {
    var i = 0;

    var startButtons = document.getElementsByClassName('pq-slim-quote-picture', i);
    var mobileStartButtons = document.getElementsByClassName('mobile-start-button', i);
    var hiddenButton;
    var audioSourceMP3 = [];
    var YouTubeIDs = [];
    var timeBegins = [];
    var timeEnds = [];

    var playerWrappers = document.getElementsByClassName('player');

    var openPlayerWrapper = false;

    for (i = 0; i < startButtons.length; ++i) {
        startButtons[i].addEventListener("click", playMedia(i));
        mobileStartButtons[i].addEventListener("click", playMedia(i));
        audioSourceMP3[i] = startButtons[i].getAttribute('audioSourceMP3');
        YouTubeIDs[i] = startButtons[i].getAttribute('YouTubeID');
        timeBegins[i] = startButtons[i].getAttribute('timeBegins');
        timeEnds[i] = startButtons[i].getAttribute('timeEnds');
    }

    window.playerWrappers = playerWrappers;
    window.openPlayerWrapper = openPlayerWrapper;
    window.startButtons = startButtons;
    window.mobileStartButtons = mobileStartButtons;
    window.hiddenButton = hiddenButton;
    window.audioSourceMP3 = audioSourceMP3;
    window.YouTubeIDs = YouTubeIDs;
    window.timeBegins = timeBegins;
    window.timeEnds = timeEnds;

    function playMedia(i) {
        return function () {
            if (openPlayerWrapper != false) {
                var openPlayer = document.getElementById('open-player');
                openPlayer.parentNode.className = "player";
                $('#open-player').remove();
                $(hiddenButton).show();
                hiddenButton = startButtons[i];
                $(startButtons[i]).hide();
            } else {
                openPlayerWrapper = true;
                hiddenButton = startButtons[i];
                $(startButtons[i]).hide();
            }
            if (YouTubeIDs[i].length == 11) {
                onYouTubeIframeAPIReady();

                function onYouTubeIframeAPIReady() {
                    var YouTubeNode = document.createElement('div');
                    YouTubeNode.setAttribute('id', 'open-player');
                    playerWrappers[i].appendChild(YouTubeNode);
                    YouTubeNode.parentNode.className = YouTubeNode.parentNode.className + " youtube-player";

                    playerWrappers[i] = new YT.Player(YouTubeNode, {
                        playerVars: {'rel': 0, 'showinfo': 0, 'hidecontrols': 1 },
                        events: {
                          'onReady': onPlayerReady,
                          'onStateChange': onPlayerStateChange,
                        }
                    });
                }

                // When the player is ready, we load the video.
                // loadVideoById starts the video automatically.
                // cueVideoById does not start the video automatically.

                function onPlayerReady(event) {
                    event.target.loadVideoById({
                        videoId: YouTubeIDs[i],
                        startSeconds: timeBegins[i],
                        endSeconds: timeEnds[i]
                    });
                  };

                  var done = false;
                  function onPlayerStateChange(event) {
                      if (event.data == 0) {
                          done = true;
                          event.target.destroy();
                          $(hiddenButton).show();
                          $('#open-player').remove();
                      }
                  };

            } else {
                var audio = document.createElement('audio');
                audio.setAttribute('controls', '');
                audio.setAttribute('preload', 'metadata');
                audio.setAttribute('id', 'open-player');
                playerWrappers[i].appendChild(audio);

                var source = document.createElement('source');
                source.setAttribute('src', audioSourceMP3[i]);
                audio.appendChild(source);

                $(playerWrappers[i]).css('margin', '5px 0 0 0');

                audio.load();
                audio.onloadedmetadata = function() {
                    audio.play();
                };

                audio.addEventListener('timeupdate', function() {
                    if (Math.floor(audio.currentTime) == timeEnds[i]) {
                        audio.pause();
                        audio.currentTime = parseInt(Math.floor(timeEnds[i])) + 1;
                    } else if (audio.currentTime < 1) {
                        audio.currentTime = timeBegins[i];
                    }
                },false);
            }
        }
    }

});

/* Sample MP3: http://traffic.libsyn.com/joeroganexp/p523.mp3 */
/* Sample OGG: http://www.tuxradar.com/files/podcast/tuxradar_s06e02.ogg */<|endoftext|>


<commit_before>/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

var url = require("url");
var querystring = require("qs");
var ObjectID = require("mongodb").ObjectID;
var db;
var req;
var res;
var path;
var method;
var resource;
var action;
var params;
var format;
var verbose;

var routeRequest = function(dbSet, request, response, path, opts) {
	db = dbSet;
	req = request;
	res = response;
	path = path;
	method = request.method;
	resource = null;
	action = null;
	params = {};
	format = opts && opts["format"] ? opts["format"] : "json";
	verbose = opts && opts["verbose"] ? true : false;
	callback = opts && opts["callback"] ? opts["callback"] : null;
	var parts = path.split("/");
	resource = parts[2];
	if(!resource || !resource.match(/^[a-zA-Z_][0-9a-zA-Z\._]*[0-9a-zA-Z]$/)) {
		failInvalidParams();
  } else {

		action = parts[3];

		if(method == "POST") {
			var dataChunks = '';
			req.addListener("data", function(chunk) {
				dataChunks += chunk;
			});
		} else {
			var rawparams = url.parse(req.url).query;
			params = querystring.parse(rawparams);
		}

		req.addListener("end", function() {
			if(method == "GET") {
				action = "find";
				find();
			} else {
				params = querystring.parse(dataChunks);
				if(action == "delete") {
					remove();
				} else if(action == "create") {
					create();
				} else {
					action = "upsert";
					upsert();
				}
			}
			if(callback) { callback({resource: resource, params: params}); }
		});
	}
}

var queryMongo = function(args) {
	if(resource && args && args.action && args.params && args.params.length > 2) {
		var coll = db.collection(resource)
		if(args.params && args.params.length == 3)
			coll[args.action](args.params[0], args.params[1], args.params[2]);
		else if(args.params && args.params.length == 4)
			coll[args.action](args.params[0], args.params[1], args.params[2], args.params[3]);

		if(args.callback)
			args.callback();
	} else
		failInvalidParams();
}

var failInvalidParams = function() {
	failResponse(
		{success: false, reason: 'invalid parameters'},
		{message: 'invalid route'},
		400
	);
}

var create = function() {
	queryMongo({
		action: "save",
		params: [
			parseParams(params),
			{safe: true},
			function(err, docs) {
				if(err && err.message.indexOf('E11000 ') !== -1) failResponse({success: false, reason: "id exists"});
				if(err) failResponse({ success: false, reason: err.message}, err);
				endResponse({success: true, docs: docs}, true);
			}
		]
	});
}

var upsert = function() {
	if(params.upsert && params.set) {
		if(verbose) {
			console.log("QUERY: " + JSON.stringify(params.upsert));
			console.log("SET: " + JSON.stringify(params.set));
		}
		queryMongo({
			action: "update",
			params: [
				parseParams(params.upsert),
				{$set: parseParams(params.set)},
				{safe:true, upsert:true},
				function(err) {
					if(err) failResponse({ success: false, reason: err.message}, err);
				}
			],
			callback: function() { endResponse({success: true}, true); }
		});
	} else
		failInvalidParams();
}

var remove = function() {
	if(resource) {
		queryMongo({
			action: "remove",
			params: [parseParams(), {safe: true}, function(err) {
				if(err) failResponse({ success: false, reason: err.message}, err);
			}],
			callback: function() { endResponse({success:true}, true); }
		});
	} else
		failInvalidParams();
}

var find = function() {
	if(resource) {
		var searchParams = {};
		var fields = {};
		rawFields = params && params.fields ? params.fields : [];

		for(var i = 0; i < rawFields.length; ++i)
			fields[rawFields[i]] = true;

		for(key in params) {
			if(key != "fields")
				searchParams[key] = parseParam(key, params[key]);
		}
		var coll = new db.collection(resource);
		jsonHeader(200);
		res.write("[");
		coll.find(searchParams, fields).forEach(function(err, doc) {
			if(doc) {
				writeResponse(doc, false, {postFix: ","});
			} else {
				endResponse(null, false, {postFix: "]"});
			}
		});
	} else
		failInvalidParams();
}

var parseParam = function(k,v) {
	if(typeof(v) == "object") {
		for(kk in v) {
			v[kk] = parseParam(kk, v[kk]);
		}
	} else if(k == "_id")
		return new ObjectID(v);
	else if(v == "true")
		return true;
	else if(v == "false")
		return false;
	else if(v == "null")
		return null;
	else if(v.match && v.match(/^\-?[0-9]+$/))
		return parseInt(v);
	else if(v.match && v.match(/^\-?[0-9]+\.[0-9]+$/))
		return parseFloat(v);
	// TODO: date parse

	return v;
}

var parseParams = function(localParams) {
	if(!localParams)
		localParams = params;
	for(k in localParams)
		localParams[k] = parseParam(k,localParams[k]);

	return localParams;
}

var jsonHeader = function(code) {
	res.writeHead(code, {'Content-Type': 'application/json'});
}

var failResponse = function(responseData, err, code) {
	if(format == "json") {
		jsonHeader(code ? code : 500);
		if(verbose) {
			console.warn("Mongo failure: " + err.message);
			console.warn("Collection: " + resource);
			console.warn("Parameters: " + JSON.stringify(params));
			console.warn("Action: " + action);
		}
		res.end(JSON.stringify(responseData));
	}
}

var endResponse = function(responseData, withHeader, opts) {
	var postFix = opts && opts["postFix"] ? opts["postFix"] : "";
	if(format == "json") {
		if(withHeader)
			jsonHeader(200);
		res.end(JSON.stringify(responseData) + postFix);
	}
}

var writeResponse = function(responseData, withHeader, opts) {
	var postFix = opts && opts["postFix"] ? opts["postFix"] : "";
	if(format == "json") {
		if(withHeader)
			jsonHeader(200);
		res.write(JSON.stringify(responseData) + postFix);
	}
}
exports.routeRequest = routeRequest;
<commit_msg>adapted mongo-by-params for jsonp on find and pagination<commit_after>/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

var url = require("url");
var querystring = require("qs");
var ObjectID = require("mongodb").ObjectID;
var db;
var req;
var res;
var path;
var method;
var resource;
var action;
var params;
var format;
var verbose;

var routeRequest = function(dbSet, request, response, path, opts) {
	db = dbSet;
	req = request;
	res = response;
	path = path;
	method = request.method;
	resource = null;
	action = null;
	params = {};
	format = opts && opts["format"] ? opts["format"] : "json";
	verbose = opts && opts["verbose"] ? true : false;
	callback = opts && opts["callback"] ? opts["callback"] : null;
	var parts = path.split("/");
	resource = parts[2];
	if(!resource || !resource.match(/^[a-zA-Z_][0-9a-zA-Z\._]*[0-9a-zA-Z]$/)) {
		failInvalidParams();
  } else {

		action = parts[3];

		if(method == "POST") {
			var dataChunks = '';
			req.addListener("data", function(chunk) {
				dataChunks += chunk;
			});
		} else {
			var rawparams = url.parse(req.url).query;
			params = querystring.parse(rawparams);
		}

		req.addListener("end", function() {
			if(method == "GET") {
				action = "find";
				find();
			} else {
				params = querystring.parse(dataChunks);
				if(action == "delete") {
					remove();
				} else if(action == "create") {
					create();
				} else {
					action = "upsert";
					upsert();
				}
			}
			if(callback) { callback({resource: resource, params: params}); }
		});
	}
}

var queryMongo = function(args) {
	if(resource && args && args.action && args.params && args.params.length > 2) {
		var coll = db.collection(resource)
		if(args.params && args.params.length == 3)
			coll[args.action](args.params[0], args.params[1], args.params[2]);
		else if(args.params && args.params.length == 4)
			coll[args.action](args.params[0], args.params[1], args.params[2], args.params[3]);

		if(args.callback)
			args.callback();
	} else
		failInvalidParams();
}

var failInvalidParams = function() {
	failResponse(
		{success: false, reason: 'invalid parameters'},
		{message: 'invalid route'},
		400
	);
}

var create = function() {
	queryMongo({
		action: "save",
		params: [
			parseParams(params),
			{safe: true},
			function(err, docs) {
				if(err && err.message.indexOf('E11000 ') !== -1) failResponse({success: false, reason: "id exists"});
				if(err) failResponse({ success: false, reason: err.message}, err);
				endResponse({success: true, docs: docs}, true);
			}
		]
	});
}

var upsert = function() {
	if(params.upsert && params.set) {
		if(verbose) {
			console.log("QUERY: " + JSON.stringify(params.upsert));
			console.log("SET: " + JSON.stringify(params.set));
		}
		queryMongo({
			action: "update",
			params: [
				parseParams(params.upsert),
				{$set: parseParams(params.set)},
				{safe:true, upsert:true},
				function(err) {
					if(err) failResponse({ success: false, reason: err.message}, err);
				}
			],
			callback: function() { endResponse({success: true}, true); }
		});
	} else
		failInvalidParams();
}

var remove = function() {
	if(resource) {
		queryMongo({
			action: "remove",
			params: [parseParams(), {safe: true}, function(err) {
				if(err) failResponse({ success: false, reason: err.message}, err);
			}],
			callback: function() { endResponse({success:true}, true); }
		});
	} else
		failInvalidParams();
}

var find = function() {
	if(resource) {
		var searchParams = {};
		var fields = {};
		rawFields = params && params.fields ? params.fields : [];

		for(var i = 0; i < rawFields.length; ++i)
			fields[rawFields[i]] = true;

		for(key in params) {
			if(key != "fields" && key != "offset" && key != "limit" && key != "_" && key != "callback")
				searchParams[key] = parseParam(key, params[key]);
		}
		var coll = new db.collection(resource);
		jsonHeader(200);
		if(params["callback"])
			res.write(params["callback"] + "(");
		res.write("[");
		var cursor = coll.find(searchParams, fields);
		if(params["limit"] && params["limit"].match(/[0-9]+/))
			cursor.limit(parseInt(params["limit"]));
		if(params["offset"] && params["offset"].match(/[0-9]+/))
			cursor.skip(parseInt(params["offset"]));
		cursor.forEach(function(err, doc) {
			if(doc) {
				writeResponse(doc, false, {postFix: ","});
			} else {
				endResponse(null, false, {postFix: "]" + (params["callback"] ? ");" : "") });
			}
		});
	} else
		failInvalidParams();
}

var parseParam = function(k,v) {
	if(typeof(v) == "object") {
		for(kk in v) {
			v[kk] = parseParam(kk, v[kk]);
		}
	} else if(k == "_id")
		return new ObjectID(v);
	else if(v == "true")
		return true;
	else if(v == "false")
		return false;
	else if(v == "null")
		return null;
	else if(v.match && v.match(/^\-?[0-9]+$/))
		return parseInt(v);
	else if(v.match && v.match(/^\-?[0-9]+\.[0-9]+$/))
		return parseFloat(v);
	// TODO: date parse

	return v;
}

var parseParams = function(localParams) {
	if(!localParams)
		localParams = params;
	for(k in localParams)
		localParams[k] = parseParam(k,localParams[k]);

	return localParams;
}

var jsonHeader = function(code) {
	res.writeHead(code, {'Content-Type': 'application/json'});
}

var failResponse = function(responseData, err, code) {
	if(format == "json") {
		jsonHeader(code ? code : 500);
		if(verbose) {
			console.warn("Mongo failure: " + err.message);
			console.warn("Collection: " + resource);
			console.warn("Parameters: " + JSON.stringify(params));
			console.warn("Action: " + action);
		}
		res.end(JSON.stringify(responseData));
	}
}

var endResponse = function(responseData, withHeader, opts) {
	var postFix = opts && opts["postFix"] ? opts["postFix"] : "";
	if(format == "json") {
		if(withHeader)
			jsonHeader(200);
		res.end(JSON.stringify(responseData) + postFix);
	}
}

var writeResponse = function(responseData, withHeader, opts) {
	var postFix = opts && opts["postFix"] ? opts["postFix"] : "";
	if(format == "json") {
		if(withHeader)
			jsonHeader(200);
		res.write(JSON.stringify(responseData) + postFix);
	}
}
exports.routeRequest = routeRequest;
<|endoftext|>


<commit_before>/*
 * vim: ts=4:sw=4:expandtab
 */
(function () {
    'use strict';
    window.Whisper = window.Whisper || {};

    var Message  = window.Whisper.Message = Backbone.Model.extend({
        database  : Whisper.Database,
        storeName : 'messages',
        initialize: function() {
            this.on('change:attachments', this.updateImageUrl);
            this.on('destroy', this.revokeImageUrl);
        },
        defaults  : function() {
            return {
                timestamp: new Date().getTime(),
                attachments: []
            };
        },
        validate: function(attributes, options) {
            var required = ['conversationId', 'received_at', 'sent_at'];
            var missing = _.filter(required, function(attr) { return !attributes[attr]; });
            if (missing.length) {
                console.log("Message missing attributes: " + missing);
            }
        },
        isEndSession: function() {
            var flag = textsecure.protobuf.DataMessage.Flags.END_SESSION;
            return !!(this.get('flags') & flag);
        },
        isGroupUpdate: function() {
            return !!(this.get('group_update'));
        },
        isIncoming: function() {
            return this.get('type') === 'incoming';
        },
        getDescription: function() {
            if (this.isGroupUpdate()) {
                var group_update = this.get('group_update');
                if (group_update.left) {
                    return group_update.left + ' left the group.';
                }

                var messages = ['Updated the group.'];
                if (group_update.name) {
                    messages.push("Title is now '" + group_update.name + "'.");
                }
                if (group_update.joined) {
                    messages.push(group_update.joined.join(', ') + ' joined the group.');
                }

                return messages.join(' ');
            }
            if (this.isEndSession()) {
                return 'Secure session ended.';
            }
            if (this.isIncoming() && this.hasKeyConflicts()) {
                return 'Received message with unknown identity key.';
            }
            if (this.isIncoming() && this.hasErrors()) {
                return 'Error handling incoming message.';
            }

            return this.get('body');
        },
        getNotificationText: function() {
            var description = this.getDescription();
            if (description) {
                return description;
            }
            if (this.get('attachments').length > 0) {
                return 'Media message';
            }

            return '';
        },
        updateImageUrl: function() {
            this.revokeImageUrl();
            var attachment = this.get('attachments')[0];
            if (attachment) {
                var blob = new Blob([attachment.data], {
                    type: attachment.contentType
                });
                this.imageUrl = URL.createObjectURL(blob);
            } else {
                this.imageUrl = null;
            }
        },
        revokeImageUrl: function() {
            if (this.imageUrl) {
                URL.revokeObjectURL(this.imageUrl);
                this.imageUrl = null;
            }
        },
        getImageUrl: function() {
            if (this.imageUrl === undefined) {
                this.updateImageUrl();
            }
            return this.imageUrl;
        },
        getContact: function() {
            var conversationId = this.get('source');
            if (!this.isIncoming()) {
                conversationId = textsecure.storage.user.getNumber();
            }
            var c = ConversationController.get(conversationId);
            if (!c) {
                c = ConversationController.create({id: conversationId, type: 'private'});
                c.fetch();
            }
            return c;
        },
        isOutgoing: function() {
            return this.get('type') === 'outgoing';
        },
        hasErrors: function() {
            return _.size(this.get('errors')) > 0;
        },
        hasKeyConflicts: function() {
            return _.any(this.get('errors'), function(e) {
                return (e.name === 'IncomingIdentityKeyError' ||
                        e.name === 'OutgoingIdentityKeyError');
            });
        },
        hasKeyConflict: function(number) {
            return _.any(this.get('errors'), function(e) {
                return (e.name === 'IncomingIdentityKeyError' ||
                        e.name === 'OutgoingIdentityKeyError') &&
                        e.number === number;
            });
        },
        getKeyConflict: function(number) {
            return _.find(this.get('errors'), function(e) {
                return (e.name === 'IncomingIdentityKeyError' ||
                        e.name === 'OutgoingIdentityKeyError') &&
                        e.number === number;
            });
        },

        send: function(promise) {
            this.trigger('pending');
            return promise.then(function(result) {
                this.trigger('done');
                if (result.dataMessage) {
                    this.set({dataMessage: result.dataMessage});
                }
                this.save({sent: true});
                this.sendSyncMessage();
            }.bind(this)).catch(function(result) {
                this.trigger('done');
                if (result.dataMessage) {
                    this.set({dataMessage: result.dataMessage});
                }

                if (result instanceof Error) {
                    this.saveErrors(result);
                } else {
                    this.saveErrors(result.errors);
                    if (result.successfulNumbers.length > 0) {
                        this.set({sent: true});
                        this.sendSyncMessage();
                    }
                }

            }.bind(this));
        },

        sendSyncMessage: function() {
            this.syncPromise = this.syncPromise || Promise.resolve();
            this.syncPromise = this.syncPromise.then(function() {
                var dataMessage = this.get('dataMessage');
                if (this.get('synced') || !dataMessage) {
                    return;
                }
                return textsecure.messaging.sendSyncMessage(
                    dataMessage, this.get('sent_at'), this.get('destination')
                ).then(function() {
                    this.save({synced: true, dataMessage: null});
                }.bind(this));
            }.bind(this));
        },

        saveErrors: function(errors) {
            if (!(errors instanceof Array)) {
                errors = [errors];
            }
            errors.forEach(function(e) {
                console.log(e);
                console.log(e.reason, e.stack);
            });
            errors = errors.map(function(e) {
                if (e.constructor === Error ||
                    e.constructor === TypeError ||
                    e.constructor === ReferenceError) {
                    return _.pick(e, 'name', 'message', 'code', 'number', 'reason');
                }
                return e;
            });
            errors = errors.concat(this.get('errors') || []);

            return this.save({errors : errors});
        },

        removeConflictFor: function(number) {
            var errors = _.reject(this.get('errors'), function(e) {
                return e.number === number &&
                    (e.name === 'IncomingIdentityKeyError' ||
                     e.name === 'OutgoingIdentityKeyError');
            });
            this.set({errors: errors});
        },

        removeOutgoingErrors: function(number) {
            var errors = _.partition(this.get('errors'), function(e) {
                return e.number === number &&
                    (e.name === 'MessageError' ||
                     e.name === 'OutgoingMessageError' ||
                     e.name === 'SendMessageNetworkError');
            });
            this.set({errors: errors[1]});
            return errors[0][0];
        },

        resend: function(number) {
            var error = this.removeOutgoingErrors(number);
            if (error) {
                var promise = new textsecure.ReplayableError(error).replay();
                this.send(promise);
            }
        },

        resolveConflict: function(number) {
            var error = this.getKeyConflict(number);
            if (error) {
                this.removeConflictFor(number);
                var promise = new textsecure.ReplayableError(error).replay();
                if (this.isIncoming()) {
                    promise = promise.then(function(dataMessage) {
                        this.removeConflictFor(number);
                        this.handleDataMessage(dataMessage);
                    }.bind(this));
                } else {
                    promise = this.send(promise).then(function() {
                        this.removeConflictFor(number);
                        this.save();
                    }.bind(this));
                }
                promise.catch(function(e) {
                    this.removeConflictFor(number);
                    this.saveErrors(e);
                }.bind(this));

                return promise;
            }
        },
        handleDataMessage: function(dataMessage) {
            // This function can be called from the background script on an
            // incoming message or from the frontend after the user accepts an
            // identity key change.
            var message = this;
            var source = message.get('source');
            var type = message.get('type');
            var timestamp = message.get('sent_at');
            var conversationId = message.get('conversationId');
            if (dataMessage.group) {
                conversationId = dataMessage.group.id;
            }
            var conversation = ConversationController.create({id: conversationId});
            conversation.fetch().always(function() {
                var now = new Date().getTime();
                var attributes = { type: 'private' };
                if (dataMessage.group) {
                    var group_update = null;
                    attributes = {
                        type: 'group',
                        groupId: dataMessage.group.id,
                    };
                    if (dataMessage.group.type === textsecure.protobuf.GroupContext.Type.UPDATE) {
                        attributes = {
                            type       : 'group',
                            groupId    : dataMessage.group.id,
                            name       : dataMessage.group.name,
                            avatar     : dataMessage.group.avatar,
                            members    : dataMessage.group.members,
                        };
                        group_update = conversation.changedAttributes(_.pick(dataMessage.group, 'name', 'avatar')) || {};
                        var difference = _.difference(dataMessage.group.members, conversation.get('members'));
                        if (difference.length > 0) {
                            group_update.joined = difference;
                        }
                    }
                    else if (dataMessage.group.type === textsecure.protobuf.GroupContext.Type.QUIT) {
                        if (source == textsecure.storage.user.getNumber()) {
                            group_update = { left: "You" };
                        } else {
                            group_update = { left: source };
                        }
                        attributes.members = _.without(conversation.get('members'), source);
                    }

                    if (group_update !== null) {
                        message.set({group_update: group_update});
                    }
                }
                if (type === 'outgoing') {
                    // lazy hack - check for receipts that arrived early.
                    if (dataMessage.group && dataMessage.group.id) {  // group sync
                        var members = conversation.get('members') || [];
                        var receipts = window.receipts.where({ timestamp: timestamp });
                        for (var i in receipts) {
                            if (members.indexOf(receipts[i].get('source')) > -1) {
                                window.receipts.remove(receipts[i]);
                                message.set({
                                    delivered: (message.get('delivered') || 0) + 1
                                });
                            }
                        }
                    } else {
                        var receipt = window.receipts.findWhere({
                            timestamp: timestamp,
                            source: conversationId
                        });
                        if (receipt) {
                            window.receipts.remove(receipt);
                            message.set({
                                delivered: (message.get('delivered') || 0) + 1
                            });
                        }
                    }
                }
                attributes.active_at = now;
                if (type === 'incoming') {
                    attributes.unreadCount = conversation.get('unreadCount') + 1;
                }
                conversation.set(attributes);

                message.set({
                    body           : dataMessage.body,
                    conversationId : conversation.id,
                    attachments    : dataMessage.attachments,
                    decrypted_at   : now,
                    flags          : dataMessage.flags,
                    errors         : []
                });

                var conversation_timestamp = conversation.get('timestamp');
                if (!conversation_timestamp || message.get('sent_at') > conversation_timestamp) {
                    conversation.set({
                        timestamp: message.get('sent_at'),
                        lastMessage: message.getNotificationText()
                    });
                }
                else if (!conversation.get('lastMessage')) {
                    conversation.set({
                        lastMessage: message.getNotificationText()
                    });
                }

                message.save().then(function() {
                    conversation.save().then(function() {
                        conversation.trigger('newmessage', message);
                        conversation.notify(message);
                    });
                });
            });
        },
        markRead: function(sync) {
            this.unset('unread');
            Whisper.Notifications.remove(Whisper.Notifications.where({
                messageId: this.id
            }));
            return this.save();
        }

    });

    Whisper.MessageCollection = Backbone.Collection.extend({
        model      : Message,
        database   : Whisper.Database,
        storeName  : 'messages',
        comparator : 'received_at',
        initialize : function(models, options) {
            if (options) {
                this.conversation = options.conversation;
            }
        },
        destroyAll : function () {
            return Promise.all(this.models.map(function(m) {
                return new Promise(function(resolve, reject) {
                    m.destroy().then(resolve).fail(reject);
                });
            }));
        },

        fetchSentAt: function(timestamp) {
            return this.fetch({
                index: {
                    // 'receipt' index on sent_at
                    name: 'receipt',
                    only: timestamp
                }
            });
        },

        fetchConversation: function(conversationId, limit) {
            if (typeof limit !== 'number') {
                limit = 100;
            }
            return new Promise(function(resolve) {
                var upper;
                if (this.length === 0) {
                    // fetch the most recent messages first
                    upper = Number.MAX_VALUE;
                } else {
                    // not our first rodeo, fetch older messages.
                    upper = this.at(0).get('received_at');
                }
                var options = {remove: false, limit: limit};
                options.index = {
                    // 'conversation' index on [conversationId, received_at]
                    name  : 'conversation',
                    lower : [conversationId],
                    upper : [conversationId, upper],
                    order : 'desc'
                    // SELECT messages WHERE conversationId = this.id ORDER
                    // received_at DESC
                };
                this.fetch(options).then(resolve);
            }.bind(this));
        },

        hasKeyConflicts: function() {
            return this.any(function(m) { return m.hasKeyConflicts(); });
        }
    });
})();
<commit_msg>DRY up early receipt processing<commit_after>/*
 * vim: ts=4:sw=4:expandtab
 */
(function () {
    'use strict';
    window.Whisper = window.Whisper || {};

    var Message  = window.Whisper.Message = Backbone.Model.extend({
        database  : Whisper.Database,
        storeName : 'messages',
        initialize: function() {
            this.on('change:attachments', this.updateImageUrl);
            this.on('destroy', this.revokeImageUrl);
        },
        defaults  : function() {
            return {
                timestamp: new Date().getTime(),
                attachments: []
            };
        },
        validate: function(attributes, options) {
            var required = ['conversationId', 'received_at', 'sent_at'];
            var missing = _.filter(required, function(attr) { return !attributes[attr]; });
            if (missing.length) {
                console.log("Message missing attributes: " + missing);
            }
        },
        isEndSession: function() {
            var flag = textsecure.protobuf.DataMessage.Flags.END_SESSION;
            return !!(this.get('flags') & flag);
        },
        isGroupUpdate: function() {
            return !!(this.get('group_update'));
        },
        isIncoming: function() {
            return this.get('type') === 'incoming';
        },
        getDescription: function() {
            if (this.isGroupUpdate()) {
                var group_update = this.get('group_update');
                if (group_update.left) {
                    return group_update.left + ' left the group.';
                }

                var messages = ['Updated the group.'];
                if (group_update.name) {
                    messages.push("Title is now '" + group_update.name + "'.");
                }
                if (group_update.joined) {
                    messages.push(group_update.joined.join(', ') + ' joined the group.');
                }

                return messages.join(' ');
            }
            if (this.isEndSession()) {
                return 'Secure session ended.';
            }
            if (this.isIncoming() && this.hasKeyConflicts()) {
                return 'Received message with unknown identity key.';
            }
            if (this.isIncoming() && this.hasErrors()) {
                return 'Error handling incoming message.';
            }

            return this.get('body');
        },
        getNotificationText: function() {
            var description = this.getDescription();
            if (description) {
                return description;
            }
            if (this.get('attachments').length > 0) {
                return 'Media message';
            }

            return '';
        },
        updateImageUrl: function() {
            this.revokeImageUrl();
            var attachment = this.get('attachments')[0];
            if (attachment) {
                var blob = new Blob([attachment.data], {
                    type: attachment.contentType
                });
                this.imageUrl = URL.createObjectURL(blob);
            } else {
                this.imageUrl = null;
            }
        },
        revokeImageUrl: function() {
            if (this.imageUrl) {
                URL.revokeObjectURL(this.imageUrl);
                this.imageUrl = null;
            }
        },
        getImageUrl: function() {
            if (this.imageUrl === undefined) {
                this.updateImageUrl();
            }
            return this.imageUrl;
        },
        getContact: function() {
            var conversationId = this.get('source');
            if (!this.isIncoming()) {
                conversationId = textsecure.storage.user.getNumber();
            }
            var c = ConversationController.get(conversationId);
            if (!c) {
                c = ConversationController.create({id: conversationId, type: 'private'});
                c.fetch();
            }
            return c;
        },
        isOutgoing: function() {
            return this.get('type') === 'outgoing';
        },
        hasErrors: function() {
            return _.size(this.get('errors')) > 0;
        },
        hasKeyConflicts: function() {
            return _.any(this.get('errors'), function(e) {
                return (e.name === 'IncomingIdentityKeyError' ||
                        e.name === 'OutgoingIdentityKeyError');
            });
        },
        hasKeyConflict: function(number) {
            return _.any(this.get('errors'), function(e) {
                return (e.name === 'IncomingIdentityKeyError' ||
                        e.name === 'OutgoingIdentityKeyError') &&
                        e.number === number;
            });
        },
        getKeyConflict: function(number) {
            return _.find(this.get('errors'), function(e) {
                return (e.name === 'IncomingIdentityKeyError' ||
                        e.name === 'OutgoingIdentityKeyError') &&
                        e.number === number;
            });
        },

        send: function(promise) {
            this.trigger('pending');
            return promise.then(function(result) {
                this.trigger('done');
                if (result.dataMessage) {
                    this.set({dataMessage: result.dataMessage});
                }
                this.save({sent: true});
                this.sendSyncMessage();
            }.bind(this)).catch(function(result) {
                this.trigger('done');
                if (result.dataMessage) {
                    this.set({dataMessage: result.dataMessage});
                }

                if (result instanceof Error) {
                    this.saveErrors(result);
                } else {
                    this.saveErrors(result.errors);
                    if (result.successfulNumbers.length > 0) {
                        this.set({sent: true});
                        this.sendSyncMessage();
                    }
                }

            }.bind(this));
        },

        sendSyncMessage: function() {
            this.syncPromise = this.syncPromise || Promise.resolve();
            this.syncPromise = this.syncPromise.then(function() {
                var dataMessage = this.get('dataMessage');
                if (this.get('synced') || !dataMessage) {
                    return;
                }
                return textsecure.messaging.sendSyncMessage(
                    dataMessage, this.get('sent_at'), this.get('destination')
                ).then(function() {
                    this.save({synced: true, dataMessage: null});
                }.bind(this));
            }.bind(this));
        },

        saveErrors: function(errors) {
            if (!(errors instanceof Array)) {
                errors = [errors];
            }
            errors.forEach(function(e) {
                console.log(e);
                console.log(e.reason, e.stack);
            });
            errors = errors.map(function(e) {
                if (e.constructor === Error ||
                    e.constructor === TypeError ||
                    e.constructor === ReferenceError) {
                    return _.pick(e, 'name', 'message', 'code', 'number', 'reason');
                }
                return e;
            });
            errors = errors.concat(this.get('errors') || []);

            return this.save({errors : errors});
        },

        removeConflictFor: function(number) {
            var errors = _.reject(this.get('errors'), function(e) {
                return e.number === number &&
                    (e.name === 'IncomingIdentityKeyError' ||
                     e.name === 'OutgoingIdentityKeyError');
            });
            this.set({errors: errors});
        },

        removeOutgoingErrors: function(number) {
            var errors = _.partition(this.get('errors'), function(e) {
                return e.number === number &&
                    (e.name === 'MessageError' ||
                     e.name === 'OutgoingMessageError' ||
                     e.name === 'SendMessageNetworkError');
            });
            this.set({errors: errors[1]});
            return errors[0][0];
        },

        resend: function(number) {
            var error = this.removeOutgoingErrors(number);
            if (error) {
                var promise = new textsecure.ReplayableError(error).replay();
                this.send(promise);
            }
        },

        resolveConflict: function(number) {
            var error = this.getKeyConflict(number);
            if (error) {
                this.removeConflictFor(number);
                var promise = new textsecure.ReplayableError(error).replay();
                if (this.isIncoming()) {
                    promise = promise.then(function(dataMessage) {
                        this.removeConflictFor(number);
                        this.handleDataMessage(dataMessage);
                    }.bind(this));
                } else {
                    promise = this.send(promise).then(function() {
                        this.removeConflictFor(number);
                        this.save();
                    }.bind(this));
                }
                promise.catch(function(e) {
                    this.removeConflictFor(number);
                    this.saveErrors(e);
                }.bind(this));

                return promise;
            }
        },
        handleDataMessage: function(dataMessage) {
            // This function can be called from the background script on an
            // incoming message or from the frontend after the user accepts an
            // identity key change.
            var message = this;
            var source = message.get('source');
            var type = message.get('type');
            var timestamp = message.get('sent_at');
            var conversationId = message.get('conversationId');
            if (dataMessage.group) {
                conversationId = dataMessage.group.id;
            }
            var conversation = ConversationController.create({id: conversationId});
            conversation.fetch().always(function() {
                var now = new Date().getTime();
                var attributes = { type: 'private' };
                if (dataMessage.group) {
                    var group_update = null;
                    attributes = {
                        type: 'group',
                        groupId: dataMessage.group.id,
                    };
                    if (dataMessage.group.type === textsecure.protobuf.GroupContext.Type.UPDATE) {
                        attributes = {
                            type       : 'group',
                            groupId    : dataMessage.group.id,
                            name       : dataMessage.group.name,
                            avatar     : dataMessage.group.avatar,
                            members    : dataMessage.group.members,
                        };
                        group_update = conversation.changedAttributes(_.pick(dataMessage.group, 'name', 'avatar')) || {};
                        var difference = _.difference(dataMessage.group.members, conversation.get('members'));
                        if (difference.length > 0) {
                            group_update.joined = difference;
                        }
                    }
                    else if (dataMessage.group.type === textsecure.protobuf.GroupContext.Type.QUIT) {
                        if (source == textsecure.storage.user.getNumber()) {
                            group_update = { left: "You" };
                        } else {
                            group_update = { left: source };
                        }
                        attributes.members = _.without(conversation.get('members'), source);
                    }

                    if (group_update !== null) {
                        message.set({group_update: group_update});
                    }
                }
                if (type === 'outgoing') {
                    // lazy hack - check for receipts that arrived early.
                    var recipients;
                    if (dataMessage.group && dataMessage.group.id) {  // group sync
                        recipients = conversation.get('members') || [];
                    } else {
                        recipients = [ conversation.id ];
                    }
                    window.receipts.filter(function(receipt) {
                        return (receipt.get('timestamp') === timestamp) &&
                               (recipients.indexOf(receipt.get('source')) > -1);
                    }).forEach(function(receipt) {
                        window.receipts.remove(receipt);
                        message.set({
                            delivered: (message.get('delivered') || 0) + 1
                        });
                    });
                }
                attributes.active_at = now;
                if (type === 'incoming') {
                    attributes.unreadCount = conversation.get('unreadCount') + 1;
                }
                conversation.set(attributes);

                message.set({
                    body           : dataMessage.body,
                    conversationId : conversation.id,
                    attachments    : dataMessage.attachments,
                    decrypted_at   : now,
                    flags          : dataMessage.flags,
                    errors         : []
                });

                var conversation_timestamp = conversation.get('timestamp');
                if (!conversation_timestamp || message.get('sent_at') > conversation_timestamp) {
                    conversation.set({
                        timestamp: message.get('sent_at'),
                        lastMessage: message.getNotificationText()
                    });
                }
                else if (!conversation.get('lastMessage')) {
                    conversation.set({
                        lastMessage: message.getNotificationText()
                    });
                }

                message.save().then(function() {
                    conversation.save().then(function() {
                        conversation.trigger('newmessage', message);
                        conversation.notify(message);
                    });
                });
            });
        },
        markRead: function(sync) {
            this.unset('unread');
            Whisper.Notifications.remove(Whisper.Notifications.where({
                messageId: this.id
            }));
            return this.save();
        }

    });

    Whisper.MessageCollection = Backbone.Collection.extend({
        model      : Message,
        database   : Whisper.Database,
        storeName  : 'messages',
        comparator : 'received_at',
        initialize : function(models, options) {
            if (options) {
                this.conversation = options.conversation;
            }
        },
        destroyAll : function () {
            return Promise.all(this.models.map(function(m) {
                return new Promise(function(resolve, reject) {
                    m.destroy().then(resolve).fail(reject);
                });
            }));
        },

        fetchSentAt: function(timestamp) {
            return this.fetch({
                index: {
                    // 'receipt' index on sent_at
                    name: 'receipt',
                    only: timestamp
                }
            });
        },

        fetchConversation: function(conversationId, limit) {
            if (typeof limit !== 'number') {
                limit = 100;
            }
            return new Promise(function(resolve) {
                var upper;
                if (this.length === 0) {
                    // fetch the most recent messages first
                    upper = Number.MAX_VALUE;
                } else {
                    // not our first rodeo, fetch older messages.
                    upper = this.at(0).get('received_at');
                }
                var options = {remove: false, limit: limit};
                options.index = {
                    // 'conversation' index on [conversationId, received_at]
                    name  : 'conversation',
                    lower : [conversationId],
                    upper : [conversationId, upper],
                    order : 'desc'
                    // SELECT messages WHERE conversationId = this.id ORDER
                    // received_at DESC
                };
                this.fetch(options).then(resolve);
            }.bind(this));
        },

        hasKeyConflicts: function() {
            return this.any(function(m) { return m.hasKeyConflicts(); });
        }
    });
})();
<|endoftext|>