<commit_before>import _ from 'lodash';
import dedent from 'dedent';
import moment from 'moment';
import { Observable, Scheduler } from 'rx';
import assign from 'object.assign';
import debugFactory from 'debug';
import utils from '../utils';

import {
  saveUser,
  observeMethod,
  observableQueryFromModel
} from '../utils/rx';

import {
  userMigration,
  ifNoUserSend
} from '../utils/middleware';

const debug = debugFactory('freecc:challenges');
const challengesRegex = /^(bonfire|waypoint|zipline|basejump)/i;
const firstChallenge = 'waypoint-say-hello-to-html-elements';
const challengeView = {
  0: 'coursewares/showHTML',
  1: 'coursewares/showJS',
  2: 'coursewares/showVideo',
  3: 'coursewares/showZiplineOrBasejump',
  4: 'coursewares/showZiplineOrBasejump',
  5: 'coursewares/showBonfire',
  7: 'coursewares/showStep'
};

const dasherize = utils.dasherize;
const unDasherize = utils.unDasherize;
const getMDNLinks = utils.getMDNLinks;

function makeChallengesUnique(challengeArr) {
  // clone and reverse challenges
  // then filter by unique id's
  // then reverse again
  return _.uniq(challengeArr.slice().reverse(), 'id').reverse();
}
function numberWithCommas(x) {
  return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

function updateUserProgress(user, challengeId, completedChallenge) {
  let { completedChallenges } = user;

  // migrate user challenges object to remove
  if (!user.isUniqMigrated) {
    user.isUniqMigrated = true;

    completedChallenges = user.completedChallenges =
      makeChallengesUnique(completedChallenges);
  }

  const indexOfChallenge = _.findIndex(completedChallenges, {
    id: challengeId
  });

  const alreadyCompleted = indexOfChallenge !== -1;

  if (!alreadyCompleted) {
    user.progressTimestamps.push({
      timestamp: Date.now(),
      completedChallenge: challengeId
    });
    user.completedChallenges.push(completedChallenge);
    return user;
  }

  const oldCompletedChallenge = completedChallenges[indexOfChallenge];
  user.completedChallenges[indexOfChallenge] =
    Object.assign(
      {},
      completedChallenge,
      {
        completedDate: oldCompletedChallenge.completedDate,
        lastUpdated: completedChallenge.completedDate
      }
    );
  return user;
}

module.exports = function(app) {
  const router = app.loopback.Router();

  const challengesQuery = {
    order: [
      'order ASC',
      'suborder ASC'
    ]
  };

  // challenge model
  const Challenge = app.models.Challenge;
  // challenge find query stream
  const findChallenge$ = observeMethod(Challenge, 'find');
  // create a stream of all the challenges
  const challenge$ = findChallenge$(challengesQuery)
    .doOnNext(() => debug('query challenges'))
    .flatMap(challenges => Observable.from(
      challenges,
      null,
      null,
      Scheduler.default
    ))
    .shareReplay();

  // create a stream of challenge blocks
  const blocks$ = challenge$
    .map(challenge => challenge.toJSON())
    // group challenges by block | returns a stream of observables
    .groupBy(challenge => challenge.block)
    // turn block group stream into an array
    .flatMap(block$ => block$.toArray())
    // turn array into stream of object
    .map(blockArray => ({
      name: blockArray[0].block,
      dashedName: dasherize(blockArray[0].block),
      challenges: blockArray
    }))
    .filter(({ name })=> {
      return name !== 'Hikes';
    })
    .shareReplay();

  const User = app.models.User;
  const userCount$ = observeMethod(User, 'count');

  const send200toNonUser = ifNoUserSend(true);

  router.post(
    '/completed-challenge/',
    send200toNonUser,
    completedChallenge
  );
  router.post(
    '/completed-zipline-or-basejump',
    send200toNonUser,
    completedZiplineOrBasejump
  );
  router.post(
    '/completed-bonfire',
    send200toNonUser,
    completedBonfire
  );

  // the follow routes are covered by userMigration
  router.use(userMigration);
  router.get('/map', challengeMap);
  router.get(
    '/challenges/next-challenge',
    returnNextChallenge
  );

  router.get('/challenges/:challengeName', returnIndividualChallenge);

  app.use(router);

  function returnNextChallenge(req, res, next) {
    let nextChallengeName = firstChallenge;

    const challengeId = req.query.id;

    // find challenge
    return challenge$
      .map(challenge => challenge.toJSON())
      .filter(({ block }) => block !== 'Hikes')
      .filter(({ id }) => id === challengeId)
      // now lets find the block it belongs to
      .flatMap(challenge => {
        // find the index of the block this challenge resides in
        const blockIndex$ = blocks$
          .findIndex(({ name }) => name === challenge.block);

        return blockIndex$
          .flatMap(blockIndex => {
            // could not find block?
            if (blockIndex === -1) {
              return Observable.throw(
                'could not find challenge block for ' + challenge.block
              );
            }
            const nextBlock$ = blocks$.elementAt(blockIndex + 1);
            const firstChallengeOfNextBlock$ = nextBlock$
              .map(block => block.challenges[0]);

            return blocks$
              .elementAt(blockIndex)
              .flatMap(block => {
                // find where our challenge lies in the block
                const challengeIndex$ = Observable.from(
                  block.challenges,
                  null,
                  null,
                  Scheduler.default
                )
                  .findIndex(({ id }) => id === challengeId);

                // grab next challenge in this block
                return challengeIndex$
                  .map(index => {
                    return block.challenges[index + 1];
                  })
                  .flatMap(nextChallenge => {
                    if (!nextChallenge) {
                      return firstChallengeOfNextBlock$;
                    }
                    return Observable.just(nextChallenge);
                  });
              });
          });
      })
      .map(nextChallenge => {
        nextChallengeName = nextChallenge.dashedName;
        return nextChallengeName;
      })
      .subscribe(
        function() {},
        next,
        function() {
          debug('next challengeName', nextChallengeName);
          if (!nextChallengeName || nextChallengeName === firstChallenge) {
            req.flash('info', {
              msg: dedent`
                Once you have completed all of our challenges, you should
                join our Half Way Club and start getting
                ready for our nonprofit projects.
              `.split('\n').join(' ')
            });
            return res.redirect('/map');
          }
          res.redirect('/challenges/' + nextChallengeName);
        }
      );
  }

  function returnIndividualChallenge(req, res, next) {
    const origChallengeName = req.params.challengeName;
    const solutionCode = req.query.solution;
    const unDashedName = unDasherize(origChallengeName);

    const challengeName = challengesRegex.test(unDashedName) ?
      // remove first word if matches
      unDashedName.split(' ').slice(1).join(' ') :
      unDashedName;

    const testChallengeName = new RegExp(challengeName, 'i');
    debug('looking for %s', testChallengeName);
    challenge$
      .filter((challenge) => {
        return testChallengeName.test(challenge.name);
      })
      .lastOrDefault(null)
      .flatMap(challenge => {

        // Handle not found
        if (!challenge) {
          debug('did not find challenge for ' + origChallengeName);
          req.flash('errors', {
            msg:
              '404: We couldn\'t find a challenge with the name `' +
              origChallengeName +
              '` Please double check the name.'
          });
          return Observable.just('/challenges');
        }

        if (dasherize(challenge.name) !== origChallengeName) {
          return Observable.just(
            '/challenges/' +
            dasherize(challenge.name) +
            '?solution=' +
            encodeURIComponent(solutionCode)
          );
        }

        // save user does nothing if user does not exist
        return Observable.just({
          title: challenge.name,
          dashedName: origChallengeName,
          name: challenge.name,
          details: challenge.description,
          description: challenge.description,
          tests: challenge.tests,
          challengeSeed: challenge.challengeSeed,
          verb: utils.randomVerb(),
          phrase: utils.randomPhrase(),
          compliment: utils.randomCompliment(),
          challengeId: challenge.id,
          challengeType: challenge.challengeType,
          // video challenges
          video: challenge.challengeSeed[0],
          // bonfires specific
          difficulty: Math.floor(+challenge.difficulty),
          bonfires: challenge,
          MDNkeys: challenge.MDNlinks,
          MDNlinks: getMDNLinks(challenge.MDNlinks),
          // htmls specific
          environment: utils.whichEnvironment()
        });
      })
      .subscribe(
        function(data) {
          if (typeof data === 'string') {
            debug('redirecting to %s', data);
            return res.redirect(data);
          }
          var view = challengeView[data.challengeType];
          res.render(view, data);
        },
        next,
        function() {}
      );
  }

  function completedBonfire(req, res, next) {
    debug('compltedBonfire');
    var completedWith = req.body.challengeInfo.completedWith || false;
    var challengeId = req.body.challengeInfo.challengeId;

    var challengeData = {
      id: challengeId,
      name: req.body.challengeInfo.challengeName || '',
      completedDate: Math.round(+new Date()),
      solution: req.body.challengeInfo.solution,
      challengeType: 5
    };

    observableQueryFromModel(
        User,
        'findOne',
        { where: { username: ('' + completedWith).toLowerCase() } }
      )
      .doOnNext(function(pairedWith) {
        debug('paired with ', pairedWith);
        if (pairedWith) {
          updateUserProgress(
            pairedWith,
            challengeId,
            assign({ completedWith: req.user.id }, challengeData)
          );
        }
      })
      .withLatestFrom(
        Observable.just(req.user),
        function(pairedWith, user) {
          return {
            user: user,
            pairedWith: pairedWith
          };
        }
      )
      // side effects should always be done in do's and taps
      .doOnNext(function(dats) {
        updateUserProgress(
          dats.user,
          challengeId,
          dats.pairedWith ?
            // paired programmer found and adding to data
            assign({ completedWith: dats.pairedWith.id }, challengeData) :
            // user said they paired, but pair wasn't found
            challengeData
        );
      })
      // iterate users
      .flatMap(function(dats) {
        debug('flatmap');
        return Observable.from([dats.user, dats.pairedWith]);
      })
      // save user
      .flatMap(function(user) {
        // save user will do nothing if user is falsey
        return saveUser(user);
      })
      .subscribe(
        function(user) {
          debug('onNext');
          if (user) {
            debug('user %s saved', user.username);
          }
        },
        next,
        function() {
          debug('completed');
          return res.status(200).send(true);
        }
      );
  }

  function completedChallenge(req, res, next) {

    const completedDate = Math.round(+new Date());
    const { id, name } = req.body;
    const { challengeId, challengeName } = req.body.challengeInfo || {};

    updateUserProgress(
      req.user,
      id || challengeId,
      {
        id: id || challengeId,
        completedDate: completedDate,
        name: name || challengeName || '',
        solution: null,
        githubLink: null,
        verified: true
      }
    );

    saveUser(req.user)
      .subscribe(
        function(user) {
          debug(
            'user save points %s',
            user && user.progressTimestamps && user.progressTimestamps.length
          );
        },
        next,
        function() {
          res.sendStatus(200);
        }
      );
  }

  function completedZiplineOrBasejump(req, res, next) {

    var completedWith = req.body.challengeInfo.completedWith || '';
    var completedDate = Math.round(+new Date());
    var challengeId = req.body.challengeInfo.challengeId;
    var solutionLink = req.body.challengeInfo.publicURL;

    var githubLink = req.body.challengeInfo.challengeType === '4' ?
      req.body.challengeInfo.githubURL :
      true;

    var challengeType = req.body.challengeInfo.challengeType === '4' ?
      4 :
      3;

    if (!solutionLink || !githubLink) {
      req.flash('errors', {
        msg: 'You haven\'t supplied the necessary URLs for us to inspect ' +
        'your work.'
      });
      return res.sendStatus(403);
    }

    var challengeData = {
      id: challengeId,
      name: req.body.challengeInfo.challengeName || '',
      completedDate: completedDate,
      solution: solutionLink,
      githubLink: githubLink,
      challengeType: challengeType,
      verified: false
    };

    observableQueryFromModel(
        User,
        'findOne',
        { where: { username: completedWith.toLowerCase() } }
      )
      .doOnNext(function(pairedWith) {
        if (pairedWith) {
          updateUserProgress(
            pairedWith,
            challengeId,
            assign({ completedWith: req.user.id }, challengeData)
          );
        }
      })
      .withLatestFrom(Observable.just(req.user), function(pairedWith, user) {
        return {
          user: user,
          pairedWith: pairedWith
        };
      })
      .doOnNext(function({ user, pairedWith }) {
        updateUserProgress(
          user,
          challengeId,
          pairedWith ?
            assign({ completedWith: pairedWith.id }, challengeData) :
            challengeData
        );
      })
      .flatMap(function({ user, pairedWith }) {
        return Observable.from([user, pairedWith]);
      })
      // save users
      .flatMap(function(user) {
        // save user will do nothing if user is falsey
        return saveUser(user);
      })
      .subscribe(
        function(user) {
          if (user) {
            debug('user %s saved', user.username);
          }
        },
        next,
        function() {
          return res.status(200).send(true);
        }
      );
  }

  function challengeMap({ user = {} }, res, next) {
    let lastCompleted;
    const daysRunning = moment().diff(new Date('10/15/2014'), 'days');

    // if user
    // get the id's of all the users completed challenges
    const completedChallenges = !user.completedChallenges ?
      [] :
      _.uniq(user.completedChallenges).map(({ id, _id }) => id || _id);

    const camperCount$ = userCount$()
      .map(camperCount => numberWithCommas(camperCount));

    // create a stream of an array of all the challenge blocks
    const blocks$ = challenge$
      // mark challenge completed
      .map(challengeModel => {
        const challenge = challengeModel.toJSON();
        if (completedChallenges.indexOf(challenge.id) !== -1) {
          challenge.completed = true;
        }
        return challenge;
      })
      // group challenges by block | returns a stream of observables
      .groupBy(challenge => challenge.block)
      // turn block group stream into an array
      .flatMap(block$ => block$.toArray())
      .map(blockArray => {
        const completedCount = blockArray.reduce((sum, { completed }) => {
          if (completed) {
            return sum + 1;
          }
          return sum;
        }, 0);

        return {
          name: blockArray[0].block,
          dashedName: dasherize(blockArray[0].block),
          challenges: blockArray,
          completed: completedCount / blockArray.length * 100
        };
      })
      .filter(({ name }) => name !== 'Hikes')
      // turn stream of blocks into a stream of an array
      .toArray()
      .doOnNext((blocks) => {
        const lastCompletedBlock = _.findLast(blocks, (block) => {
          return block.completed === 100;
        });
        lastCompleted = lastCompleted && lastCompletedBlock.name || null;
      });

    Observable.combineLatest(
      camperCount$,
      blocks$,
      (camperCount, blocks) => ({ camperCount, blocks })
    )
      .subscribe(
        ({ camperCount, blocks }) => {
          res.render('challengeMap/show', {
            blocks,
            daysRunning,
            camperCount,
            lastCompleted,
            title: "A map of all Free Code Camp's Challenges"
          });
        },
        next
      );
  }
};
<commit_msg>Fix last completed logic bug<commit_after>import _ from 'lodash';
import dedent from 'dedent';
import moment from 'moment';
import { Observable, Scheduler } from 'rx';
import assign from 'object.assign';
import debugFactory from 'debug';
import utils from '../utils';

import {
  saveUser,
  observeMethod,
  observableQueryFromModel
} from '../utils/rx';

import {
  userMigration,
  ifNoUserSend
} from '../utils/middleware';

const debug = debugFactory('freecc:challenges');
const challengesRegex = /^(bonfire|waypoint|zipline|basejump)/i;
const firstChallenge = 'waypoint-say-hello-to-html-elements';
const challengeView = {
  0: 'coursewares/showHTML',
  1: 'coursewares/showJS',
  2: 'coursewares/showVideo',
  3: 'coursewares/showZiplineOrBasejump',
  4: 'coursewares/showZiplineOrBasejump',
  5: 'coursewares/showBonfire',
  7: 'coursewares/showStep'
};

const dasherize = utils.dasherize;
const unDasherize = utils.unDasherize;
const getMDNLinks = utils.getMDNLinks;

function makeChallengesUnique(challengeArr) {
  // clone and reverse challenges
  // then filter by unique id's
  // then reverse again
  return _.uniq(challengeArr.slice().reverse(), 'id').reverse();
}
function numberWithCommas(x) {
  return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

function updateUserProgress(user, challengeId, completedChallenge) {
  let { completedChallenges } = user;

  // migrate user challenges object to remove
  if (!user.isUniqMigrated) {
    user.isUniqMigrated = true;

    completedChallenges = user.completedChallenges =
      makeChallengesUnique(completedChallenges);
  }

  const indexOfChallenge = _.findIndex(completedChallenges, {
    id: challengeId
  });

  const alreadyCompleted = indexOfChallenge !== -1;

  if (!alreadyCompleted) {
    user.progressTimestamps.push({
      timestamp: Date.now(),
      completedChallenge: challengeId
    });
    user.completedChallenges.push(completedChallenge);
    return user;
  }

  const oldCompletedChallenge = completedChallenges[indexOfChallenge];
  user.completedChallenges[indexOfChallenge] =
    Object.assign(
      {},
      completedChallenge,
      {
        completedDate: oldCompletedChallenge.completedDate,
        lastUpdated: completedChallenge.completedDate
      }
    );
  return user;
}

module.exports = function(app) {
  const router = app.loopback.Router();

  const challengesQuery = {
    order: [
      'order ASC',
      'suborder ASC'
    ]
  };

  // challenge model
  const Challenge = app.models.Challenge;
  // challenge find query stream
  const findChallenge$ = observeMethod(Challenge, 'find');
  // create a stream of all the challenges
  const challenge$ = findChallenge$(challengesQuery)
    .doOnNext(() => debug('query challenges'))
    .flatMap(challenges => Observable.from(
      challenges,
      null,
      null,
      Scheduler.default
    ))
    .shareReplay();

  // create a stream of challenge blocks
  const blocks$ = challenge$
    .map(challenge => challenge.toJSON())
    // group challenges by block | returns a stream of observables
    .groupBy(challenge => challenge.block)
    // turn block group stream into an array
    .flatMap(block$ => block$.toArray())
    // turn array into stream of object
    .map(blockArray => ({
      name: blockArray[0].block,
      dashedName: dasherize(blockArray[0].block),
      challenges: blockArray
    }))
    .filter(({ name })=> {
      return name !== 'Hikes';
    })
    .shareReplay();

  const User = app.models.User;
  const userCount$ = observeMethod(User, 'count');

  const send200toNonUser = ifNoUserSend(true);

  router.post(
    '/completed-challenge/',
    send200toNonUser,
    completedChallenge
  );
  router.post(
    '/completed-zipline-or-basejump',
    send200toNonUser,
    completedZiplineOrBasejump
  );
  router.post(
    '/completed-bonfire',
    send200toNonUser,
    completedBonfire
  );

  // the follow routes are covered by userMigration
  router.use(userMigration);
  router.get('/map', challengeMap);
  router.get(
    '/challenges/next-challenge',
    returnNextChallenge
  );

  router.get('/challenges/:challengeName', returnIndividualChallenge);

  app.use(router);

  function returnNextChallenge(req, res, next) {
    let nextChallengeName = firstChallenge;

    const challengeId = req.query.id;

    // find challenge
    return challenge$
      .map(challenge => challenge.toJSON())
      .filter(({ block }) => block !== 'Hikes')
      .filter(({ id }) => id === challengeId)
      // now lets find the block it belongs to
      .flatMap(challenge => {
        // find the index of the block this challenge resides in
        const blockIndex$ = blocks$
          .findIndex(({ name }) => name === challenge.block);

        return blockIndex$
          .flatMap(blockIndex => {
            // could not find block?
            if (blockIndex === -1) {
              return Observable.throw(
                'could not find challenge block for ' + challenge.block
              );
            }
            const nextBlock$ = blocks$.elementAt(blockIndex + 1);
            const firstChallengeOfNextBlock$ = nextBlock$
              .map(block => block.challenges[0]);

            return blocks$
              .elementAt(blockIndex)
              .flatMap(block => {
                // find where our challenge lies in the block
                const challengeIndex$ = Observable.from(
                  block.challenges,
                  null,
                  null,
                  Scheduler.default
                )
                  .findIndex(({ id }) => id === challengeId);

                // grab next challenge in this block
                return challengeIndex$
                  .map(index => {
                    return block.challenges[index + 1];
                  })
                  .flatMap(nextChallenge => {
                    if (!nextChallenge) {
                      return firstChallengeOfNextBlock$;
                    }
                    return Observable.just(nextChallenge);
                  });
              });
          });
      })
      .map(nextChallenge => {
        nextChallengeName = nextChallenge.dashedName;
        return nextChallengeName;
      })
      .subscribe(
        function() {},
        next,
        function() {
          debug('next challengeName', nextChallengeName);
          if (!nextChallengeName || nextChallengeName === firstChallenge) {
            req.flash('info', {
              msg: dedent`
                Once you have completed all of our challenges, you should
                join our Half Way Club and start getting
                ready for our nonprofit projects.
              `.split('\n').join(' ')
            });
            return res.redirect('/map');
          }
          res.redirect('/challenges/' + nextChallengeName);
        }
      );
  }

  function returnIndividualChallenge(req, res, next) {
    const origChallengeName = req.params.challengeName;
    const solutionCode = req.query.solution;
    const unDashedName = unDasherize(origChallengeName);

    const challengeName = challengesRegex.test(unDashedName) ?
      // remove first word if matches
      unDashedName.split(' ').slice(1).join(' ') :
      unDashedName;

    const testChallengeName = new RegExp(challengeName, 'i');
    debug('looking for %s', testChallengeName);
    challenge$
      .filter((challenge) => {
        return testChallengeName.test(challenge.name);
      })
      .lastOrDefault(null)
      .flatMap(challenge => {

        // Handle not found
        if (!challenge) {
          debug('did not find challenge for ' + origChallengeName);
          req.flash('errors', {
            msg:
              '404: We couldn\'t find a challenge with the name `' +
              origChallengeName +
              '` Please double check the name.'
          });
          return Observable.just('/challenges');
        }

        if (dasherize(challenge.name) !== origChallengeName) {
          return Observable.just(
            '/challenges/' +
            dasherize(challenge.name) +
            '?solution=' +
            encodeURIComponent(solutionCode)
          );
        }

        // save user does nothing if user does not exist
        return Observable.just({
          title: challenge.name,
          dashedName: origChallengeName,
          name: challenge.name,
          details: challenge.description,
          description: challenge.description,
          tests: challenge.tests,
          challengeSeed: challenge.challengeSeed,
          verb: utils.randomVerb(),
          phrase: utils.randomPhrase(),
          compliment: utils.randomCompliment(),
          challengeId: challenge.id,
          challengeType: challenge.challengeType,
          // video challenges
          video: challenge.challengeSeed[0],
          // bonfires specific
          difficulty: Math.floor(+challenge.difficulty),
          bonfires: challenge,
          MDNkeys: challenge.MDNlinks,
          MDNlinks: getMDNLinks(challenge.MDNlinks),
          // htmls specific
          environment: utils.whichEnvironment()
        });
      })
      .subscribe(
        function(data) {
          if (typeof data === 'string') {
            debug('redirecting to %s', data);
            return res.redirect(data);
          }
          var view = challengeView[data.challengeType];
          res.render(view, data);
        },
        next,
        function() {}
      );
  }

  function completedBonfire(req, res, next) {
    debug('compltedBonfire');
    var completedWith = req.body.challengeInfo.completedWith || false;
    var challengeId = req.body.challengeInfo.challengeId;

    var challengeData = {
      id: challengeId,
      name: req.body.challengeInfo.challengeName || '',
      completedDate: Math.round(+new Date()),
      solution: req.body.challengeInfo.solution,
      challengeType: 5
    };

    observableQueryFromModel(
        User,
        'findOne',
        { where: { username: ('' + completedWith).toLowerCase() } }
      )
      .doOnNext(function(pairedWith) {
        debug('paired with ', pairedWith);
        if (pairedWith) {
          updateUserProgress(
            pairedWith,
            challengeId,
            assign({ completedWith: req.user.id }, challengeData)
          );
        }
      })
      .withLatestFrom(
        Observable.just(req.user),
        function(pairedWith, user) {
          return {
            user: user,
            pairedWith: pairedWith
          };
        }
      )
      // side effects should always be done in do's and taps
      .doOnNext(function(dats) {
        updateUserProgress(
          dats.user,
          challengeId,
          dats.pairedWith ?
            // paired programmer found and adding to data
            assign({ completedWith: dats.pairedWith.id }, challengeData) :
            // user said they paired, but pair wasn't found
            challengeData
        );
      })
      // iterate users
      .flatMap(function(dats) {
        debug('flatmap');
        return Observable.from([dats.user, dats.pairedWith]);
      })
      // save user
      .flatMap(function(user) {
        // save user will do nothing if user is falsey
        return saveUser(user);
      })
      .subscribe(
        function(user) {
          debug('onNext');
          if (user) {
            debug('user %s saved', user.username);
          }
        },
        next,
        function() {
          debug('completed');
          return res.status(200).send(true);
        }
      );
  }

  function completedChallenge(req, res, next) {

    const completedDate = Math.round(+new Date());
    const { id, name } = req.body;
    const { challengeId, challengeName } = req.body.challengeInfo || {};

    updateUserProgress(
      req.user,
      id || challengeId,
      {
        id: id || challengeId,
        completedDate: completedDate,
        name: name || challengeName || '',
        solution: null,
        githubLink: null,
        verified: true
      }
    );

    saveUser(req.user)
      .subscribe(
        function(user) {
          debug(
            'user save points %s',
            user && user.progressTimestamps && user.progressTimestamps.length
          );
        },
        next,
        function() {
          res.sendStatus(200);
        }
      );
  }

  function completedZiplineOrBasejump(req, res, next) {

    var completedWith = req.body.challengeInfo.completedWith || '';
    var completedDate = Math.round(+new Date());
    var challengeId = req.body.challengeInfo.challengeId;
    var solutionLink = req.body.challengeInfo.publicURL;

    var githubLink = req.body.challengeInfo.challengeType === '4' ?
      req.body.challengeInfo.githubURL :
      true;

    var challengeType = req.body.challengeInfo.challengeType === '4' ?
      4 :
      3;

    if (!solutionLink || !githubLink) {
      req.flash('errors', {
        msg: 'You haven\'t supplied the necessary URLs for us to inspect ' +
        'your work.'
      });
      return res.sendStatus(403);
    }

    var challengeData = {
      id: challengeId,
      name: req.body.challengeInfo.challengeName || '',
      completedDate: completedDate,
      solution: solutionLink,
      githubLink: githubLink,
      challengeType: challengeType,
      verified: false
    };

    observableQueryFromModel(
        User,
        'findOne',
        { where: { username: completedWith.toLowerCase() } }
      )
      .doOnNext(function(pairedWith) {
        if (pairedWith) {
          updateUserProgress(
            pairedWith,
            challengeId,
            assign({ completedWith: req.user.id }, challengeData)
          );
        }
      })
      .withLatestFrom(Observable.just(req.user), function(pairedWith, user) {
        return {
          user: user,
          pairedWith: pairedWith
        };
      })
      .doOnNext(function({ user, pairedWith }) {
        updateUserProgress(
          user,
          challengeId,
          pairedWith ?
            assign({ completedWith: pairedWith.id }, challengeData) :
            challengeData
        );
      })
      .flatMap(function({ user, pairedWith }) {
        return Observable.from([user, pairedWith]);
      })
      // save users
      .flatMap(function(user) {
        // save user will do nothing if user is falsey
        return saveUser(user);
      })
      .subscribe(
        function(user) {
          if (user) {
            debug('user %s saved', user.username);
          }
        },
        next,
        function() {
          return res.status(200).send(true);
        }
      );
  }

  function challengeMap({ user = {} }, res, next) {
    let lastCompleted;
    const daysRunning = moment().diff(new Date('10/15/2014'), 'days');

    // if user
    // get the id's of all the users completed challenges
    const completedChallenges = !user.completedChallenges ?
      [] :
      _.uniq(user.completedChallenges).map(({ id, _id }) => id || _id);

    const camperCount$ = userCount$()
      .map(camperCount => numberWithCommas(camperCount));

    // create a stream of an array of all the challenge blocks
    const blocks$ = challenge$
      // mark challenge completed
      .map(challengeModel => {
        const challenge = challengeModel.toJSON();
        if (completedChallenges.indexOf(challenge.id) !== -1) {
          challenge.completed = true;
        }
        return challenge;
      })
      // group challenges by block | returns a stream of observables
      .groupBy(challenge => challenge.block)
      // turn block group stream into an array
      .flatMap(block$ => block$.toArray())
      .map(blockArray => {
        const completedCount = blockArray.reduce((sum, { completed }) => {
          if (completed) {
            return sum + 1;
          }
          return sum;
        }, 0);

        return {
          name: blockArray[0].block,
          dashedName: dasherize(blockArray[0].block),
          challenges: blockArray,
          completed: completedCount / blockArray.length * 100
        };
      })
      .filter(({ name }) => name !== 'Hikes')
      // turn stream of blocks into a stream of an array
      .toArray()
      .doOnNext((blocks) => {
        const lastCompletedBlock = _.findLast(blocks, (block) => {
          return block.completed === 100;
        });
        lastCompleted = lastCompletedBlock && lastCompletedBlock.name || null;
      });

    Observable.combineLatest(
      camperCount$,
      blocks$,
      (camperCount, blocks) => ({ camperCount, blocks })
    )
      .subscribe(
        ({ camperCount, blocks }) => {
          res.render('challengeMap/show', {
            blocks,
            daysRunning,
            camperCount,
            lastCompleted,
            title: "A map of all Free Code Camp's Challenges"
          });
        },
        next
      );
  }
};
<|endoftext|>


<commit_before>/* globals UploadFS */

import fs from 'fs';
import stream from 'stream';
import mime from 'mime-type/with-db';
import Future from 'fibers/future';
import sharp from 'sharp';
import { Cookies } from 'meteor/ostrio:cookies';

const cookie = new Cookies();

Object.assign(FileUpload, {
	handlers: {},

	configureUploadsStore(store, name, options) {
		const type = name.split(':').pop();
		const stores = UploadFS.getStores();
		delete stores[name];

		return new UploadFS.store[store](Object.assign({
			name
		}, options, FileUpload[`default${ type }`]()));
	},

	defaultUploads() {
		return {
			collection: RocketChat.models.Uploads.model,
			filter: new UploadFS.Filter({
				onCheck: FileUpload.validateFileUpload
			}),
			getPath(file) {
				return `${ RocketChat.settings.get('uniqueID') }/uploads/${ file.rid }/${ file.userId }/${ file._id }`;
			},
			onValidate: FileUpload.uploadsOnValidate,
			onRead(fileId, file, req, res) {
				if (!FileUpload.requestCanAccessFiles(req)) {
					res.writeHead(403);
					return false;
				}

				res.setHeader('content-disposition', `attachment; filename="${ encodeURIComponent(file.name) }"`);
				return true;
			}
		};
	},

	defaultAvatars() {
		return {
			collection: RocketChat.models.Avatars.model,
			// filter: new UploadFS.Filter({
			// 	onCheck: FileUpload.validateFileUpload
			// }),
			getPath(file) {
				return `${ RocketChat.settings.get('uniqueID') }/avatars/${ file.userId }`;
			},
			onValidate: FileUpload.avatarsOnValidate,
			onFinishUpload: FileUpload.avatarsOnFinishUpload
		};
	},

	defaultUserDataFiles() {
		return {
			collection: RocketChat.models.UserDataFiles.model,
			getPath(file) {
				return `${ RocketChat.settings.get('uniqueID') }/uploads/userData/${ file.userId }`;
			},
			onValidate: FileUpload.uploadsOnValidate,
			onRead(fileId, file, req, res) {
				if (!FileUpload.requestCanAccessFiles(req)) {
					res.writeHead(403);
					return false;
				}

				res.setHeader('content-disposition', `attachment; filename="${ encodeURIComponent(file.name) }"`);
				return true;
			}
		};
	},

	avatarsOnValidate(file) {
		if (RocketChat.settings.get('Accounts_AvatarResize') !== true) {
			return;
		}

		const tempFilePath = UploadFS.getTempFilePath(file._id);

		const height = RocketChat.settings.get('Accounts_AvatarSize');
		const future = new Future();

		const s = sharp(tempFilePath);
		s.rotate();
		// Get metadata to resize the image the first time to keep "inside" the dimensions
		// then resize again to create the canvas around
		s.metadata(Meteor.bindEnvironment((err, metadata) => {
			s.toFormat(sharp.format.jpeg)
				.resize(Math.min(height, metadata.width), Math.min(height, metadata.height))
				.pipe(sharp()
					.resize(height, height)
					.background('#FFFFFF')
					.embed()
				)
				// Use buffer to get the result in memory then replace the existing file
				// There is no option to override a file using this library
				.toBuffer()
				.then(Meteor.bindEnvironment(outputBuffer => {
					fs.writeFile(tempFilePath, outputBuffer, Meteor.bindEnvironment(err => {
						if (err != null) {
							console.error(err);
						}
						const size = fs.lstatSync(tempFilePath).size;
						this.getCollection().direct.update({_id: file._id}, {$set: {size}});
						future.return();
					}));
				}));
		}));

		return future.wait();
	},

	resizeImagePreview(file) {
		file = RocketChat.models.Uploads.findOneById(file._id);
		file = FileUpload.addExtensionTo(file);
		const image = FileUpload.getStore('Uploads')._store.getReadStream(file._id, file);

		const transformer = sharp()
			.resize(32, 32)
			.max()
			.jpeg()
			.blur();
		const result = transformer.toBuffer().then((out) => out.toString('base64'));
		image.pipe(transformer);
		return result;
	},

	uploadsOnValidate(file) {
		if (!/^image\/((x-windows-)?bmp|p?jpeg|png)$/.test(file.type)) {
			return;
		}

		const tmpFile = UploadFS.getTempFilePath(file._id);

		const fut = new Future();

		const s = sharp(tmpFile);
		s.metadata(Meteor.bindEnvironment((err, metadata) => {
			if (err != null) {
				console.error(err);
				return fut.return();
			}

			const identify = {
				format: metadata.format,
				size: {
					width: metadata.width,
					height: metadata.height
				}
			};

			if (metadata.orientation == null) {
				return fut.return();
			}

			s.rotate()
				.toFile(`${ tmpFile }.tmp`)
				.then(Meteor.bindEnvironment(() => {
					fs.unlink(tmpFile, Meteor.bindEnvironment(() => {
						fs.rename(`${ tmpFile }.tmp`, tmpFile, Meteor.bindEnvironment(() => {
							const size = fs.lstatSync(tmpFile).size;
							this.getCollection().direct.update({_id: file._id}, {
								$set: {
									size,
									identify
								}
							});
							fut.return();
						}));
					}));
				})).catch((err) => {
					console.error(err);
					fut.return();
				});
		}));

		return fut.wait();
	},

	avatarsOnFinishUpload(file) {
		// update file record to match user's username
		const user = RocketChat.models.Users.findOneById(file.userId);
		const oldAvatar = RocketChat.models.Avatars.findOneByName(user.username);
		if (oldAvatar) {
			RocketChat.models.Avatars.deleteFile(oldAvatar._id);
		}
		RocketChat.models.Avatars.updateFileNameById(file._id, user.username);
		// console.log('upload finished ->', file);
	},

	requestCanAccessFiles({ headers = {}, query = {} }) {
		if (!RocketChat.settings.get('FileUpload_ProtectFiles')) {
			return true;
		}

		let { rc_uid, rc_token } = query;

		if (!rc_uid && headers.cookie) {
			rc_uid = cookie.get('rc_uid', headers.cookie);
			rc_token = cookie.get('rc_token', headers.cookie);
		}
		const isAuthorizedByCookies = rc_uid && rc_token && RocketChat.models.Users.findOneByIdAndLoginToken(rc_uid, rc_token);
		const isAuthorizedByHeaders = headers['x-user-id'] && headers['x-auth-token'] && RocketChat.models.Users.findOneByIdAndLoginToken(headers['x-user-id'], headers['x-auth-token']);
		return isAuthorizedByCookies || isAuthorizedByHeaders;
	},
	addExtensionTo(file) {
		if (mime.lookup(file.name) === file.type) {
			return file;
		}

		const ext = mime.extension(file.type);
		if (ext && false === new RegExp(`\.${ ext }$`, 'i').test(file.name)) {
			file.name = `${ file.name }.${ ext }`;
		}

		return file;
	},

	getStore(modelName) {
		const storageType = RocketChat.settings.get('FileUpload_Storage_Type');
		const handlerName = `${ storageType }:${ modelName }`;

		return this.getStoreByName(handlerName);
	},

	getStoreByName(handlerName) {
		if (this.handlers[handlerName] == null) {
			console.error(`Upload handler "${ handlerName }" does not exists`);
		}
		return this.handlers[handlerName];
	},

	get(file, req, res, next) {
		const store = this.getStoreByName(file.store);
		if (store && store.get) {
			return store.get(file, req, res, next);
		}
		res.writeHead(404);
		res.end();
	},

	copy(file, targetFile) {
		const store = this.getStoreByName(file.store);
		const out = fs.createWriteStream(targetFile);

		file = FileUpload.addExtensionTo(file);

		if (store.copy) {
			store.copy(file, out);
			return true;
		}

		return false;
	}
});

export class FileUploadClass {
	constructor({ name, model, store, get, insert, getStore, copy }) {
		this.name = name;
		this.model = model || this.getModelFromName();
		this._store = store || UploadFS.getStore(name);
		this.get = get;
		this.copy = copy;

		if (insert) {
			this.insert = insert;
		}

		if (getStore) {
			this.getStore = getStore;
		}

		FileUpload.handlers[name] = this;
	}

	getStore() {
		return this._store;
	}

	get store() {
		return this.getStore();
	}

	set store(store) {
		this._store = store;
	}

	getModelFromName() {
		return RocketChat.models[this.name.split(':')[1]];
	}

	delete(fileId) {
		if (this.store && this.store.delete) {
			this.store.delete(fileId);
		}

		return this.model.deleteFile(fileId);
	}

	deleteById(fileId) {
		const file = this.model.findOneById(fileId);

		if (!file) {
			return;
		}

		const store = FileUpload.getStoreByName(file.store);

		return store.delete(file._id);
	}

	deleteByName(fileName) {
		const file = this.model.findOneByName(fileName);

		if (!file) {
			return;
		}

		const store = FileUpload.getStoreByName(file.store);

		return store.delete(file._id);
	}

	insert(fileData, streamOrBuffer, cb) {
		fileData.size = parseInt(fileData.size) || 0;

		// Check if the fileData matches store filter
		const filter = this.store.getFilter();
		if (filter && filter.check) {
			filter.check(fileData);
		}

		const fileId = this.store.create(fileData);
		const token = this.store.createToken(fileId);
		const tmpFile = UploadFS.getTempFilePath(fileId);

		try {
			if (streamOrBuffer instanceof stream) {
				streamOrBuffer.pipe(fs.createWriteStream(tmpFile));
			} else if (streamOrBuffer instanceof Buffer) {
				fs.writeFileSync(tmpFile, streamOrBuffer);
			} else {
				throw new Error('Invalid file type');
			}

			const file = Meteor.call('ufsComplete', fileId, this.name, token);

			if (cb) {
				cb(null, file);
			}

			return file;
		} catch (e) {
			if (cb) {
				cb(e);
			} else {
				throw e;
			}
		}
	}
}
<commit_msg>fixed issue that caused exception when validating avatars with invalid metadata<commit_after>/* globals UploadFS */

import fs from 'fs';
import stream from 'stream';
import mime from 'mime-type/with-db';
import Future from 'fibers/future';
import sharp from 'sharp';
import { Cookies } from 'meteor/ostrio:cookies';

const cookie = new Cookies();

Object.assign(FileUpload, {
	handlers: {},

	configureUploadsStore(store, name, options) {
		const type = name.split(':').pop();
		const stores = UploadFS.getStores();
		delete stores[name];

		return new UploadFS.store[store](Object.assign({
			name
		}, options, FileUpload[`default${ type }`]()));
	},

	defaultUploads() {
		return {
			collection: RocketChat.models.Uploads.model,
			filter: new UploadFS.Filter({
				onCheck: FileUpload.validateFileUpload
			}),
			getPath(file) {
				return `${ RocketChat.settings.get('uniqueID') }/uploads/${ file.rid }/${ file.userId }/${ file._id }`;
			},
			onValidate: FileUpload.uploadsOnValidate,
			onRead(fileId, file, req, res) {
				if (!FileUpload.requestCanAccessFiles(req)) {
					res.writeHead(403);
					return false;
				}

				res.setHeader('content-disposition', `attachment; filename="${ encodeURIComponent(file.name) }"`);
				return true;
			}
		};
	},

	defaultAvatars() {
		return {
			collection: RocketChat.models.Avatars.model,
			// filter: new UploadFS.Filter({
			// 	onCheck: FileUpload.validateFileUpload
			// }),
			getPath(file) {
				return `${ RocketChat.settings.get('uniqueID') }/avatars/${ file.userId }`;
			},
			onValidate: FileUpload.avatarsOnValidate,
			onFinishUpload: FileUpload.avatarsOnFinishUpload
		};
	},

	defaultUserDataFiles() {
		return {
			collection: RocketChat.models.UserDataFiles.model,
			getPath(file) {
				return `${ RocketChat.settings.get('uniqueID') }/uploads/userData/${ file.userId }`;
			},
			onValidate: FileUpload.uploadsOnValidate,
			onRead(fileId, file, req, res) {
				if (!FileUpload.requestCanAccessFiles(req)) {
					res.writeHead(403);
					return false;
				}

				res.setHeader('content-disposition', `attachment; filename="${ encodeURIComponent(file.name) }"`);
				return true;
			}
		};
	},

	avatarsOnValidate(file) {
		if (RocketChat.settings.get('Accounts_AvatarResize') !== true) {
			return;
		}

		const tempFilePath = UploadFS.getTempFilePath(file._id);

		const height = RocketChat.settings.get('Accounts_AvatarSize');
		const future = new Future();

		const s = sharp(tempFilePath);
		s.rotate();
		// Get metadata to resize the image the first time to keep "inside" the dimensions
		// then resize again to create the canvas around

		s.metadata(Meteor.bindEnvironment((err, metadata) => {
			if (!metadata) {
				metadata = {};
			};

			s.toFormat(sharp.format.jpeg)
				.resize(Math.min(height || 0, metadata.width || Infinity), Math.min(height || 0, metadata.height || Infinity))
				.pipe(sharp()
					.resize(height, height)
					.background('#FFFFFF')
					.embed()
				)
				// Use buffer to get the result in memory then replace the existing file
				// There is no option to override a file using this library
				.toBuffer()
				.then(Meteor.bindEnvironment(outputBuffer => {
					fs.writeFile(tempFilePath, outputBuffer, Meteor.bindEnvironment(err => {
						if (err != null) {
							console.error(err);
						}
						const size = fs.lstatSync(tempFilePath).size;
						this.getCollection().direct.update({_id: file._id}, {$set: {size}});
						future.return();
					}));
				}));
		}));

		return future.wait();
	},

	resizeImagePreview(file) {
		file = RocketChat.models.Uploads.findOneById(file._id);
		file = FileUpload.addExtensionTo(file);
		const image = FileUpload.getStore('Uploads')._store.getReadStream(file._id, file);

		const transformer = sharp()
			.resize(32, 32)
			.max()
			.jpeg()
			.blur();
		const result = transformer.toBuffer().then((out) => out.toString('base64'));
		image.pipe(transformer);
		return result;
	},

	uploadsOnValidate(file) {
		if (!/^image\/((x-windows-)?bmp|p?jpeg|png)$/.test(file.type)) {
			return;
		}

		const tmpFile = UploadFS.getTempFilePath(file._id);

		const fut = new Future();

		const s = sharp(tmpFile);
		s.metadata(Meteor.bindEnvironment((err, metadata) => {
			if (err != null) {
				console.error(err);
				return fut.return();
			}

			const identify = {
				format: metadata.format,
				size: {
					width: metadata.width,
					height: metadata.height
				}
			};

			if (metadata.orientation == null) {
				return fut.return();
			}

			s.rotate()
				.toFile(`${ tmpFile }.tmp`)
				.then(Meteor.bindEnvironment(() => {
					fs.unlink(tmpFile, Meteor.bindEnvironment(() => {
						fs.rename(`${ tmpFile }.tmp`, tmpFile, Meteor.bindEnvironment(() => {
							const size = fs.lstatSync(tmpFile).size;
							this.getCollection().direct.update({_id: file._id}, {
								$set: {
									size,
									identify
								}
							});
							fut.return();
						}));
					}));
				})).catch((err) => {
					console.error(err);
					fut.return();
				});
		}));

		return fut.wait();
	},

	avatarsOnFinishUpload(file) {
		// update file record to match user's username
		const user = RocketChat.models.Users.findOneById(file.userId);
		const oldAvatar = RocketChat.models.Avatars.findOneByName(user.username);
		if (oldAvatar) {
			RocketChat.models.Avatars.deleteFile(oldAvatar._id);
		}
		RocketChat.models.Avatars.updateFileNameById(file._id, user.username);
		// console.log('upload finished ->', file);
	},

	requestCanAccessFiles({ headers = {}, query = {} }) {
		if (!RocketChat.settings.get('FileUpload_ProtectFiles')) {
			return true;
		}

		let { rc_uid, rc_token } = query;

		if (!rc_uid && headers.cookie) {
			rc_uid = cookie.get('rc_uid', headers.cookie);
			rc_token = cookie.get('rc_token', headers.cookie);
		}
		const isAuthorizedByCookies = rc_uid && rc_token && RocketChat.models.Users.findOneByIdAndLoginToken(rc_uid, rc_token);
		const isAuthorizedByHeaders = headers['x-user-id'] && headers['x-auth-token'] && RocketChat.models.Users.findOneByIdAndLoginToken(headers['x-user-id'], headers['x-auth-token']);
		return isAuthorizedByCookies || isAuthorizedByHeaders;
	},
	addExtensionTo(file) {
		if (mime.lookup(file.name) === file.type) {
			return file;
		}

		const ext = mime.extension(file.type);
		if (ext && false === new RegExp(`\.${ ext }$`, 'i').test(file.name)) {
			file.name = `${ file.name }.${ ext }`;
		}

		return file;
	},

	getStore(modelName) {
		const storageType = RocketChat.settings.get('FileUpload_Storage_Type');
		const handlerName = `${ storageType }:${ modelName }`;

		return this.getStoreByName(handlerName);
	},

	getStoreByName(handlerName) {
		if (this.handlers[handlerName] == null) {
			console.error(`Upload handler "${ handlerName }" does not exists`);
		}
		return this.handlers[handlerName];
	},

	get(file, req, res, next) {
		const store = this.getStoreByName(file.store);
		if (store && store.get) {
			return store.get(file, req, res, next);
		}
		res.writeHead(404);
		res.end();
	},

	copy(file, targetFile) {
		const store = this.getStoreByName(file.store);
		const out = fs.createWriteStream(targetFile);

		file = FileUpload.addExtensionTo(file);

		if (store.copy) {
			store.copy(file, out);
			return true;
		}

		return false;
	}
});

export class FileUploadClass {
	constructor({ name, model, store, get, insert, getStore, copy }) {
		this.name = name;
		this.model = model || this.getModelFromName();
		this._store = store || UploadFS.getStore(name);
		this.get = get;
		this.copy = copy;

		if (insert) {
			this.insert = insert;
		}

		if (getStore) {
			this.getStore = getStore;
		}

		FileUpload.handlers[name] = this;
	}

	getStore() {
		return this._store;
	}

	get store() {
		return this.getStore();
	}

	set store(store) {
		this._store = store;
	}

	getModelFromName() {
		return RocketChat.models[this.name.split(':')[1]];
	}

	delete(fileId) {
		if (this.store && this.store.delete) {
			this.store.delete(fileId);
		}

		return this.model.deleteFile(fileId);
	}

	deleteById(fileId) {
		const file = this.model.findOneById(fileId);

		if (!file) {
			return;
		}

		const store = FileUpload.getStoreByName(file.store);

		return store.delete(file._id);
	}

	deleteByName(fileName) {
		const file = this.model.findOneByName(fileName);

		if (!file) {
			return;
		}

		const store = FileUpload.getStoreByName(file.store);

		return store.delete(file._id);
	}

	insert(fileData, streamOrBuffer, cb) {
		fileData.size = parseInt(fileData.size) || 0;

		// Check if the fileData matches store filter
		const filter = this.store.getFilter();
		if (filter && filter.check) {
			filter.check(fileData);
		}

		const fileId = this.store.create(fileData);
		const token = this.store.createToken(fileId);
		const tmpFile = UploadFS.getTempFilePath(fileId);

		try {
			if (streamOrBuffer instanceof stream) {
				streamOrBuffer.pipe(fs.createWriteStream(tmpFile));
			} else if (streamOrBuffer instanceof Buffer) {
				fs.writeFileSync(tmpFile, streamOrBuffer);
			} else {
				throw new Error('Invalid file type');
			}

			const file = Meteor.call('ufsComplete', fileId, this.name, token);

			if (cb) {
				cb(null, file);
			}

			return file;
		} catch (e) {
			if (cb) {
				cb(e);
			} else {
				throw e;
			}
		}
	}
}
<|endoftext|>


<commit_before>let roleRecyclable = {
    run: function(creep) {
        creep.memory.executingRole = "recyclable";

        let theStorage = _.get(Game.rooms, [creep.memory.roomID, "storage"], undefined);
        let recycleContainer = _.get(Game.rooms, [creep.memory.roomID, "recycleContainer"], undefined);
        let theSpawn = Game.getObjectById(creep.memory.recycleSpawnID);
        if (theSpawn == undefined) {
            let theSpawns = _.filter(Game.spawns, (s) => (
                s.room.name == _.get(Game.rooms, [creep.memory.roomID], creep.room).name
            ));
            if (recycleContainer != undefined) {
                theSpawn = _.first(recycleContainer.pos.findInRange(theSpawns, 1));
            }
            if (theSpawn == undefined) {
                theSpawn = creep.pos.findClosestByRange(theSpawns);
            }
            if (theSpawn != undefined) {
                creep.memory.recycleSpawnID = theSpawn.id;
            }
        }

        if (creep.carryTotal > 0
            && theStorage != undefined
            && _.sum(theStorage.store) < theStorage.storeCapacity
            && theStorage.my == true
            && _.get(Memory.rooms, [creep.memory.roomID, "isShuttingDown"], false) == false) {
            for (let resourceType in creep.carry) {
                let err = creep.transfer(theStorage, resourceType, Math.min(creep.carry[resourceType], (theStorage.storeCapacity - _.sum(theStorage.store))));
                if (err == ERR_NOT_IN_RANGE) {
                    creep.travelTo(theStorage);
                    creep.say(travelToIcons(creep) + ICONS[STRUCTURE_STORAGE], true);
                    break;
                }
                else if (err == OK) {
                    creep.say(ICONS["transfer"] + ICONS[STRUCTURE_STORAGE], true);
                    break;
                }
            }
        }
        else if (recycleContainer != undefined
            && theSpawn != undefined
            && recycleContainer.pos.isNearTo(theSpawn)
            && creep.pos.isEqualTo(recycleContainer) == false) {
            creep.travelTo(recycleContainer);
            creep.say(travelToIcons(creep) + ICONS[STRUCTURE_CONTAINER], true);
        }
        else if (theSpawn != undefined) {
            let err = theSpawn.recycleCreep(creep);
            if (err == ERR_NOT_IN_RANGE) {
                creep.travelTo(theSpawn);
                creep.say(travelToIcons(creep) + ICONS[STRUCTURE_SPAWN], true);
            }
            else if (err == OK) {
                creep.say(ICONS["recycle"], true);
                Memory.rooms[creep.room.name].checkForDrops = true;
            }
        }
        else {
            incrementConfusedCreepCount(creep);
            creep.say(ICONS[STRUCTURE_SPAWN] + "?", true);
            creep.suicide();
        }
    }
};

module.exports = roleRecyclable;
<commit_msg>Allowed recyclables to be recycled without a recycle container.<commit_after>let roleRecyclable = {
    run: function(creep) {
        creep.memory.executingRole = "recyclable";

        let theStorage = _.get(Game.rooms, [creep.memory.roomID, "storage"], undefined);
        let recycleContainer = _.get(Game.rooms, [creep.memory.roomID, "recycleContainer"], undefined);
        let theSpawn = Game.getObjectById(creep.memory.recycleSpawnID);
        if (theSpawn == undefined) {
            let theSpawns = _.filter(Game.spawns, (s) => (
                s.room.name == _.get(Game.rooms, [creep.memory.roomID], creep.room).name
            ));
            if (recycleContainer != undefined) {
                theSpawn = _.first(recycleContainer.pos.findInRange(theSpawns, 1));
            }
            if (theSpawn == undefined) {
                theSpawn = creep.pos.findClosestByRange(theSpawns);
            }
            if (theSpawn == undefined
                && theSpawns.length > 0) {
                theSpawn = _.first(theSpawns);
            }
            if (theSpawn != undefined) {
                creep.memory.recycleSpawnID = theSpawn.id;
            }
        }

        if (creep.carryTotal > 0
            && theStorage != undefined
            && _.sum(theStorage.store) < theStorage.storeCapacity
            && theStorage.my == true
            && _.get(Memory.rooms, [creep.memory.roomID, "isShuttingDown"], false) == false) {
            for (let resourceType in creep.carry) {
                let err = creep.transfer(theStorage, resourceType, Math.min(creep.carry[resourceType], (theStorage.storeCapacity - _.sum(theStorage.store))));
                if (err == ERR_NOT_IN_RANGE) {
                    creep.travelTo(theStorage);
                    creep.say(travelToIcons(creep) + ICONS[STRUCTURE_STORAGE], true);
                    break;
                }
                else if (err == OK) {
                    creep.say(ICONS["transfer"] + ICONS[STRUCTURE_STORAGE], true);
                    break;
                }
            }
        }
        else if (recycleContainer != undefined
            && theSpawn != undefined
            && recycleContainer.pos.isNearTo(theSpawn)
            && creep.pos.isEqualTo(recycleContainer) == false) {
            creep.travelTo(recycleContainer);
            creep.say(travelToIcons(creep) + ICONS[STRUCTURE_CONTAINER], true);
        }
        else if (theSpawn != undefined) {
            let err = theSpawn.recycleCreep(creep);
            if (err == ERR_NOT_IN_RANGE) {
                creep.travelTo(theSpawn);
                creep.say(travelToIcons(creep) + ICONS[STRUCTURE_SPAWN], true);
            }
            else if (err == OK) {
                creep.say(ICONS["recycle"], true);
                Memory.rooms[creep.room.name].checkForDrops = true;
            }
        }
        else {
            incrementConfusedCreepCount(creep);
            creep.say(ICONS[STRUCTURE_SPAWN] + "?", true);
            creep.suicide();
        }
    }
};

module.exports = roleRecyclable;
<|endoftext|>


<commit_before>s.SatelliteGame = new Class( {
    toString: 'SatelliteGame',
    extend: s.Game,

	// Models that should be loaded
	models: [
		'phobos_hifi',
		'phobos_lofi',
        'human_ship_heavy',
		'human_ship_light',

	],

    getRandomCoordinate: function(){
        var coefficient = 1;
        if (Math.random() > 0.5){
            coefficient = -1;
        }
        var coord = Math.floor(Math.random()* 30000 + 15000) * coefficient;
        return coord;
    },

	initialize: function(_super) {
		var that = this;
		_super.call(this);

		// No gravity
		this.scene.setGravity(new THREE.Vector3(0, 0, 0));

        // Ambient light
        this.ambientLight = new THREE.AmbientLight( 0x382828 );
        this.scene.add( this.ambientLight );

        // Directional light
        this.light = new THREE.DirectionalLight( 0xEEEEEE, 2 );
        this.light.position.set( -100000, 0, 0 );
        this.scene.add( this.light );

        // Add moon
        this.moon = new s.Moon( {
            game: this
        } );

        // Add a ship
        this.player = new s.Player( {
            game: this,
            shipClass: 'human_ship_light',
            position: new THREE.Vector3(this.getRandomCoordinate(),this.getRandomCoordinate(),this.getRandomCoordinate()),
            rotation: new THREE.Vector3( 0, Math.PI / 4, 0 )
        } );

        // Root camera to the player's position
        this.player.root.add( this.camera );

        //// Setup camera: Cockpit view; COMMENT OUT FOR CHASE CAM
        this.camera.position.set( 0, 0, 0 );
        //// Setup camera: Chase view
        //this.camera.position.set(0,35,350);

        // Planet camera
        // this.scene.add(this.camera);
        // this.camera.position.set(10000,2000,10000);

        // Add skybox
        this.addSkybox( );

        // lazd: Dust is kinda lame. But I want some sort of thing that shows you're moving
        this.addDust( );

        this.HUD = new s.HUD( {
            game: this
        } );

        // Fly controls
        this.controls = new s.Controls( {
            game: this,
            player: this.player,
            camera: this.camera,
            HUD: this.HUD
        } );

        // Dependent on controls; needs to be below s.Controls
        this.radar = new s.Radar( {
            game: this
            //controls: this.controls
        } );

        window.addEventListener( 'mousemove', function ( e ) {
            that.HUD.targetX = e.pageX;
            that.HUD.targetY = e.pageY;
        } );
        window.addEventListener( 'mousedown', function ( ) {
            that.controls.firing = true;
        } );
        window.addEventListener( 'mouseup', function ( ) {
            that.controls.firing = false;
        } );
        /******************
        Enemy setup
        ******************/
        this.enemies = {
            _list: [ ],
            _map: {}, // new WeakMap()
            get: function ( nameOrId ) {
                if ( typeof nameOrId == 'string' ) {
                    return this._map[ nameOrId ]; // return enemies._map.get(nameOrId);
                } else if ( typeof nameOrId == 'number' ) {
                    return this._list( nameOrId );
                }
            },
            has: function ( nameOrId ) {
                return !!this.get( nameOrId );
            },
            execute: function ( nameOrId, operation, args ) {
                var enemy = this.get( nameOrId );
                if ( enemy ) {
                    enemy[ operation ].apply( enemy, args );
                    return true;
                }
                return false;
            },
            forEach: function ( callback ) {
                this._list.forEach( callback );
            },
            list: function ( ) {
                return this._list;
            },
            delete: function ( nameOrId ) {
                var enemy = this.get( nameOrId );
                if ( enemy ) {
                    // Remove from map
                    delete this._map[ enemy.name ]; // this._map.delete(enemy.name);

                    // Remove from array
                    var enemyIndex = this._list.indexOf( enemy );
                    if ( ~enemyIndex )
                        this._list.splice( enemyIndex, 1 );

                    // destroy
                    enemy.destruct( );

                    return true;
                }
                return false;
            },
            add: function ( enemyInfo ) {
                if ( this.has( enemyInfo.name ) ) {
                    this.delete( enemyInfo.name );
                    console.error( 'Bug: Player %s added twice', enemyInfo.name );
                } else {
                    if ( enemyInfo.name === null ) {
                        console.error( 'Bug: enemyInfo contained null player name' );
                        console.log( enemyInfo );
                        console.trace( );
                    }
                    console.log( '%s has joined the fray', enemyInfo.name );
                }

                // TODO: include velocities?
                var enemyShip = new s.Ship( {
                    game: that,
                    shipClass: 'human_ship_light',
                    name: enemyInfo.name,
                    position: new THREE.Vector3( enemyInfo.pos[ 0 ], enemyInfo.pos[ 1 ], enemyInfo.pos[ 2 ] ),
                    rotation: new THREE.Vector3( enemyInfo.rot[ 0 ], enemyInfo.rot[ 1 ], enemyInfo.rot[ 2 ] ),
                } );

                this._list.push( enemyShip );
                this._map[ enemyInfo.name ] = enemyShip; // this._map.set(enemyInfo.name, otherShip);
            }
        };
        this.pilot = {};
        this.pilot.name = navigator.platform + ' ' + ~~( new Date( ).getTime( ) / 100 % 1000 ) + Math.floor( Math.random( ) * 100 );

        this.comm = new s.Comm( {
            game: that,
            pilot: that.pilot,
            player: this.player,
            server: window.location.hostname + ':1337'
        } );

        this.comm.on( 'player list', that.handlePlayerList );
        this.comm.on( 'join', that.handleJoin );
        this.comm.on( 'leave', that.handleLeave );
        this.comm.on( 'move', that.handleMove );

        this.HUD.controls = this.controls;

        this.comm.connected( );

    },

    render: function ( _super, time ) {
        _super.call( this, time );
        this.controls.update( );
    },

    addSkybox: function ( ) {
        var urlPrefix = "game/textures/skybox/Purple_Nebula_";
        var urls = [
            urlPrefix + "right1.png", urlPrefix + "left2.png",
            urlPrefix + "top3.png", urlPrefix + "bottom4.png",
            urlPrefix + "front5.png", urlPrefix + "back6.png"
        ];

        var textureCube = THREE.ImageUtils.loadTextureCube( urls );
        textureCube.format = THREE.RGBFormat;
        var shader = THREE.ShaderLib.cube;

        var uniforms = THREE.UniformsUtils.clone( shader.uniforms );
        uniforms.tCube.value = textureCube;

        var material = new THREE.ShaderMaterial( {
            fragmentShader: shader.fragmentShader,
            vertexShader: shader.vertexShader,
            uniforms: uniforms,
            side: THREE.BackSide
        } );

        this.skyboxMesh = new THREE.Mesh( new THREE.CubeGeometry( 200000, 200000, 200000, 1, 1, 1, null, true ), material );
        this.scene.add( this.skyboxMesh );
    },

    addDust: function ( ) {
        var starSprite = THREE.ImageUtils.loadTexture( 'game/textures/particle.png' );
        var geometry = new THREE.Geometry( );

        // Set to false for "dust", true for stars
        var outer = true;

        // Spec size
        var radius = 100000;
        var size = 100;
        var count = 1000;

        for ( var i = 0; i < count; i++ ) {

            var vertex = new THREE.Vector3( );

            if ( outer ) {
                // Distribute "stars" on the outer bounds of far space
                vertex.x = Math.random( ) * 2 - 1;
                vertex.y = Math.random( ) * 2 - 1;
                vertex.z = Math.random( ) * 2 - 1;
                vertex.multiplyScalar( radius );
            } else {
                // Distribute "dust" throughout near space
                vertex.x = Math.random( ) * radius - radius / 2;
                vertex.y = Math.random( ) * radius - radius / 2;
                vertex.z = Math.random( ) * radius - radius / 2;
            }

            geometry.vertices.push( vertex );

        }

        var material = new THREE.ParticleBasicMaterial( {
            size: size,
            map: starSprite,
            blending: THREE.AdditiveBlending,
            depthTest: true,
            transparent: true
        } );

        var particles = new THREE.ParticleSystem( geometry, material );

        this.scene.add( particles );
    },

    handleJoin: function ( message ) {
        s.game.enemies.add( message );
    },
    handleLeave: function ( message ) {
        if ( s.game.enemies.delete( message.name ) ) {
            console.log( '%s has left', message.name );
        }
    },
    handleMove: function ( message ) {
        if ( message.name == s.game.player.name ) {
            // server told us to move
            console.log( 'Server reset position' );

            // Return to center
            s.game.player.setPosition( message.pos, message.rot, message.aVeloc, message.lVeloc, false ); // Never interpolate our own movement
        } else {
            // Enemy moved
            if ( !s.game.enemies.execute( message.name, 'setPosition', [ message.pos, message.rot, message.aVeloc, message.lVeloc, message.interp ] ) ) {
                s.game.enemies.add( message );
            }
        }
    },
    handlePlayerList: function ( message ) {
        for ( var otherPlayerName in message ) {
            // don't add self
            if ( otherPlayerName == s.game.player.name ) continue;

            var otherPlayer = message[ otherPlayerName ];
            s.game.enemies.add( otherPlayer );
        }
    }

} );
<commit_msg>Bullet colors fixed<commit_after>s.SatelliteGame = new Class( {
    toString: 'SatelliteGame',
    extend: s.Game,

	// Models that should be loaded
	models: [
		'phobos_hifi',
		'phobos_lofi',
        'human_ship_heavy',
		'human_ship_light',

	],

    getRandomCoordinate: function(){
        var coefficient = 1;
        if (Math.random() > 0.5){
            coefficient = -1;
        }
        var coord = Math.floor(Math.random()* 30000 + 15000) * coefficient;
        return coord;
    },

	initialize: function(_super) {
		var that = this;
		_super.call(this);

		// No gravity
		this.scene.setGravity(new THREE.Vector3(0, 0, 0));

        // Ambient light
        this.ambientLight = new THREE.AmbientLight( 0x382828 );
        this.scene.add( this.ambientLight );

        // Directional light
        this.light = new THREE.DirectionalLight( 0xEEEEEE, 2 );
        this.light.position.set( -100000, 0, 0 );
        this.scene.add( this.light );

        // Add moon
        this.moon = new s.Moon( {
            game: this
        } );

        // Add a ship
        this.player = new s.Player( {
            game: this,
            alliance: 'alliance',
            shipClass: 'human_ship_light',
            position: new THREE.Vector3(this.getRandomCoordinate(),this.getRandomCoordinate(),this.getRandomCoordinate()),
            rotation: new THREE.Vector3( 0, Math.PI / 4, 0 )
        } );

        // Root camera to the player's position
        this.player.root.add( this.camera );

        //// Setup camera: Cockpit view; COMMENT OUT FOR CHASE CAM
        this.camera.position.set( 0, 0, 0 );
        //// Setup camera: Chase view
        //this.camera.position.set(0,35,350);

        // Planet camera
        // this.scene.add(this.camera);
        // this.camera.position.set(10000,2000,10000);

        // Add skybox
        this.addSkybox( );

        // lazd: Dust is kinda lame. But I want some sort of thing that shows you're moving
        this.addDust( );

        this.HUD = new s.HUD( {
            game: this
        } );

        // Fly controls
        this.controls = new s.Controls( {
            game: this,
            player: this.player,
            camera: this.camera,
            HUD: this.HUD
        } );

        // Dependent on controls; needs to be below s.Controls
        this.radar = new s.Radar( {
            game: this
            //controls: this.controls
        } );

        window.addEventListener( 'mousemove', function ( e ) {
            that.HUD.targetX = e.pageX;
            that.HUD.targetY = e.pageY;
        } );
        window.addEventListener( 'mousedown', function ( ) {
            that.controls.firing = true;
        } );
        window.addEventListener( 'mouseup', function ( ) {
            that.controls.firing = false;
        } );
        /******************
        Enemy setup
        ******************/
        this.enemies = {
            _list: [ ],
            _map: {}, // new WeakMap()
            get: function ( nameOrId ) {
                if ( typeof nameOrId == 'string' ) {
                    return this._map[ nameOrId ]; // return enemies._map.get(nameOrId);
                } else if ( typeof nameOrId == 'number' ) {
                    return this._list( nameOrId );
                }
            },
            has: function ( nameOrId ) {
                return !!this.get( nameOrId );
            },
            execute: function ( nameOrId, operation, args ) {
                var enemy = this.get( nameOrId );
                if ( enemy ) {
                    enemy[ operation ].apply( enemy, args );
                    return true;
                }
                return false;
            },
            forEach: function ( callback ) {
                this._list.forEach( callback );
            },
            list: function ( ) {
                return this._list;
            },
            delete: function ( nameOrId ) {
                var enemy = this.get( nameOrId );
                if ( enemy ) {
                    // Remove from map
                    delete this._map[ enemy.name ]; // this._map.delete(enemy.name);

                    // Remove from array
                    var enemyIndex = this._list.indexOf( enemy );
                    if ( ~enemyIndex )
                        this._list.splice( enemyIndex, 1 );

                    // destroy
                    enemy.destruct( );

                    return true;
                }
                return false;
            },
            add: function ( enemyInfo ) {
                if ( this.has( enemyInfo.name ) ) {
                    this.delete( enemyInfo.name );
                    console.error( 'Bug: Player %s added twice', enemyInfo.name );
                } else {
                    if ( enemyInfo.name === null ) {
                        console.error( 'Bug: enemyInfo contained null player name' );
                        console.log( enemyInfo );
                        console.trace( );
                    }
                    console.log( '%s has joined the fray', enemyInfo.name );
                }

                // TODO: include velocities?
                var enemyShip = new s.Ship( {
                    game: that,
                    shipClass: 'human_ship_light',
                    name: enemyInfo.name,
                    position: new THREE.Vector3( enemyInfo.pos[ 0 ], enemyInfo.pos[ 1 ], enemyInfo.pos[ 2 ] ),
                    rotation: new THREE.Vector3( enemyInfo.rot[ 0 ], enemyInfo.rot[ 1 ], enemyInfo.rot[ 2 ] ),
                } );

                this._list.push( enemyShip );
                this._map[ enemyInfo.name ] = enemyShip; // this._map.set(enemyInfo.name, otherShip);
            }
        };
        this.pilot = {};
        this.pilot.name = navigator.platform + ' ' + ~~( new Date( ).getTime( ) / 100 % 1000 ) + Math.floor( Math.random( ) * 100 );

        this.comm = new s.Comm( {
            game: that,
            pilot: that.pilot,
            player: this.player,
            server: window.location.hostname + ':1337'
        } );

        this.comm.on( 'player list', that.handlePlayerList );
        this.comm.on( 'join', that.handleJoin );
        this.comm.on( 'leave', that.handleLeave );
        this.comm.on( 'move', that.handleMove );

        this.HUD.controls = this.controls;

        this.comm.connected( );

    },

    render: function ( _super, time ) {
        _super.call( this, time );
        this.controls.update( );
    },

    addSkybox: function ( ) {
        var urlPrefix = "game/textures/skybox/Purple_Nebula_";
        var urls = [
            urlPrefix + "right1.png", urlPrefix + "left2.png",
            urlPrefix + "top3.png", urlPrefix + "bottom4.png",
            urlPrefix + "front5.png", urlPrefix + "back6.png"
        ];

        var textureCube = THREE.ImageUtils.loadTextureCube( urls );
        textureCube.format = THREE.RGBFormat;
        var shader = THREE.ShaderLib.cube;

        var uniforms = THREE.UniformsUtils.clone( shader.uniforms );
        uniforms.tCube.value = textureCube;

        var material = new THREE.ShaderMaterial( {
            fragmentShader: shader.fragmentShader,
            vertexShader: shader.vertexShader,
            uniforms: uniforms,
            side: THREE.BackSide
        } );

        this.skyboxMesh = new THREE.Mesh( new THREE.CubeGeometry( 200000, 200000, 200000, 1, 1, 1, null, true ), material );
        this.scene.add( this.skyboxMesh );
    },

    addDust: function ( ) {
        var starSprite = THREE.ImageUtils.loadTexture( 'game/textures/particle.png' );
        var geometry = new THREE.Geometry( );

        // Set to false for "dust", true for stars
        var outer = true;

        // Spec size
        var radius = 100000;
        var size = 100;
        var count = 1000;

        for ( var i = 0; i < count; i++ ) {

            var vertex = new THREE.Vector3( );

            if ( outer ) {
                // Distribute "stars" on the outer bounds of far space
                vertex.x = Math.random( ) * 2 - 1;
                vertex.y = Math.random( ) * 2 - 1;
                vertex.z = Math.random( ) * 2 - 1;
                vertex.multiplyScalar( radius );
            } else {
                // Distribute "dust" throughout near space
                vertex.x = Math.random( ) * radius - radius / 2;
                vertex.y = Math.random( ) * radius - radius / 2;
                vertex.z = Math.random( ) * radius - radius / 2;
            }

            geometry.vertices.push( vertex );

        }

        var material = new THREE.ParticleBasicMaterial( {
            size: size,
            map: starSprite,
            blending: THREE.AdditiveBlending,
            depthTest: true,
            transparent: true
        } );

        var particles = new THREE.ParticleSystem( geometry, material );

        this.scene.add( particles );
    },

    handleJoin: function ( message ) {
        s.game.enemies.add( message );
    },
    handleLeave: function ( message ) {
        if ( s.game.enemies.delete( message.name ) ) {
            console.log( '%s has left', message.name );
        }
    },
    handleMove: function ( message ) {
        if ( message.name == s.game.player.name ) {
            // server told us to move
            console.log( 'Server reset position' );

            // Return to center
            s.game.player.setPosition( message.pos, message.rot, message.aVeloc, message.lVeloc, false ); // Never interpolate our own movement
        } else {
            // Enemy moved
            if ( !s.game.enemies.execute( message.name, 'setPosition', [ message.pos, message.rot, message.aVeloc, message.lVeloc, message.interp ] ) ) {
                s.game.enemies.add( message );
            }
        }
    },
    handlePlayerList: function ( message ) {
        for ( var otherPlayerName in message ) {
            // don't add self
            if ( otherPlayerName == s.game.player.name ) continue;

            var otherPlayer = message[ otherPlayerName ];
            s.game.enemies.add( otherPlayer );
        }
    }

} );
<|endoftext|>